Skip to content

Latest commit

 

History

History
463 lines (353 loc) · 30 KB

File metadata and controls

463 lines (353 loc) · 30 KB

← Оглавление

Docker

«Build, ship, run — собери, доставь, запусти» — слоган Docker


Главная боль разработки: «у меня на машине работает, а на сервере — нет». Причина — разное окружение: версии языка, библиотек, системных пакетов. Docker решает это, упаковывая приложение вместе со всем его окружением в изолированную «коробку» — контейнер, который одинаково запускается где угодно.

Docker — открытая платформа для сборки, доставки и запуска приложений в контейнерах: изолированных процессах, которые несут с собой все свои зависимости. Это виртуализация на уровне операционной системы — без отдельной гостевой ОС под каждое приложение.

Зачем нужен

  • Одинаковое окружение везде — конец «у меня работает». Dev, stage, prod собираются из одного образа.
  • Лёгкий старт — не нужен README на полстраницы «как поднять проект»: docker run и готово.
  • Изоляция — зависимости, версии Python/Node, ключи и env-переменные не текут в систему и не конфликтуют между проектами.
  • Воспроизводимость и стандартизация — окружение описано кодом (Dockerfile), а не «настроено руками».

Контейнер против виртуальной машины

И то, и другое изолирует приложение, но по-разному. Виртуальная машина тащит целую гостевую ОС поверх гипервизора; контейнер делит ядро хоста и несёт только само приложение и его зависимости.

Виртуальная машина Контейнер (Docker)
Изолирует через гипервизор + гостевая ОС движок Docker, общее ядро хоста
Вес гигабайты мегабайты
Старт минуты секунды
Накладные расходы высокие низкие
Изоляция полная (своё ядро) на уровне процессов и ФС

Контейнеры легче и быстрее ВМ, поэтому на одной машине их можно держать десятки — отсюда удобство для микросервисов и CI/CD.

Архитектура

Docker устроен как клиент — демон — реестр:

┌──────────┐   команды    ┌──────────────┐   pull/push   ┌─────────────┐
│  docker  │ ───────────▶ │   dockerd    │ ◀───────────▶ │   Registry  │
│ (клиент) │   REST API   │  (демон)     │               │ (Docker Hub)│
└──────────┘              └──────┬───────┘               └─────────────┘
                                 │ управляет
                    образы · контейнеры · тома · сети
  • Демон dockerd — фоновый процесс, который слушает Docker API и управляет объектами: образами, контейнерами, сетями, томами.
  • Клиент docker — командная строка, через которую ты отдаёшь команды демону.
  • Реестр (registry) — хранилище образов. Публичный по умолчанию — Docker Hub.

Три главных объекта

Образ (image)

Образ — read-only шаблон для создания контейнера: файловая система + метаданные (что запускать). Собирается из Dockerfile и состоит из слоёв — как коммиты в GIT, каждый слой наслаивается на предыдущий и кешируется.

Контейнер (container)

Контейнер — запущенный экземпляр образа; процесс на хосте со своей файловой системой, сетью и деревом процессов, изолированный от хоста. Из одного образа можно поднять сколько угодно контейнеров.

Реестр (registry)

Реестр — где образы хранятся и откуда раздаются:

  • docker push — отправить образ в реестр;
  • docker pull — забрать образ из реестра.
Dockerfile ──build──▶ Образ ──run──▶ Контейнер
                        ▲
                   pull │ push
                        ▼
                    Registry (Docker Hub)

Dockerfile — рецепт образа

Dockerfile — текстовый файл с инструкциями сборки образа. Выполняется сверху вниз, каждая инструкция — отдельный слой.

# базовый образ-основа
FROM python:3.12-slim
# рабочая директория внутри образа
WORKDIR /app
# сначала только зависимости — чтобы слой кешировался, пока они не меняются
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# затем сам код
COPY . .
# объявить порт (документирующая метка)
EXPOSE 8000
# команда запуска контейнера
CMD ["python", "main.py"]
Инструкция Что делает
FROM базовый образ-основа
WORKDIR рабочая директория для последующих команд
COPY / ADD копирование файлов с хоста в образ (ADD умеет ещё распаковку и URL)
RUN выполнить команду при сборке (установка пакетов)
CMD команда по умолчанию при запуске (легко переопределить)
ENTRYPOINT основная команда запуска (не переопределяется аргументами run)
ENV переменные окружения внутри контейнера
ARG переменные только на время сборки (--build-arg)
EXPOSE пометить порт для прослушивания
VOLUME точка монтирования тома
USER от чьего имени выполняются следующие инструкции и сам процесс
HEALTHCHECK чем проверять, что приложение внутри живо

Порядок имеет значение для кеша. Копируй requirements.txt и ставь зависимости до копирования кода — тогда при правке кода слой с зависимостями берётся из кеша, и пересборка идёт за секунды. Лишнее в образ не тащи — исключай через .dockerignore.

База образа

Исходным образом служит ядро ОС: Ubuntu, Debian, Alpine и др. Alpine Linux — популярная основа за лёгкость и безопасность: базовый размер всего ~5 МБ (без ядра). Поверх собирают свой образ с нужными зависимостями — например, backend на Python или готовый веб-сервер (Nginx / Apache).

Многоступенчатая сборка (multi-stage)

Проблема: чтобы собрать зависимости, нужны компилятор и заголовочные файлы, а чтобы запустить приложение — нет. В однослойной сборке весь этот инструментарий остаётся в финальном образе и раздувает его в разы.

Решение — несколько FROM в одном Dockerfile. Каждый начинает новую стадию, а в итоговый образ попадает только последняя; из предыдущих забирают готовые артефакты через COPY --from:

# ── стадия 1: собираем зависимости, тут можно быть грязным ──
FROM python:3.12 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt

# ── стадия 2: то, что реально поедет в прод ────────────────
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /wheels /wheels          # берём из стадии builder только колёса
RUN pip install --no-cache-dir --no-index --find-links=/wheels /wheels/* \
    && rm -rf /wheels
COPY . .
CMD ["python", "main.py"]

Компилятор, кеш pip и заголовки остаются в builder и до прода не доезжают. Для Go и C выигрыш ещё нагляднее: финальный образ может быть вообще scratch — один бинарник без операционной системы.

Побочная выгода, о которой спрашивают отдельно: в прод-образ не попадают секреты сборки — токены приватного репозитория, ключи. Если они использовались в builder, в финальном образе их слоёв нет, а значит, их не вытащить через docker history.

Не запускать от root

По умолчанию процесс в контейнере идёт от root. Изоляция контейнера не абсолютна: пробой в приложении плюс неудачно смонтированный том или неверная настройка — и права хоста оказываются шире, чем предполагалось.

RUN useradd --create-home --shell /bin/bash app
COPY --chown=app:app . .     # файлы сразу с нужным владельцем
USER app                     # всё, что ниже, и сам процесс — от app
CMD ["python", "main.py"]

⚠️ USER действует на всё, что идёт после него, поэтому apt-get install ставят до него. И порты ниже 1024 непривилегированному пользователю недоступны — приложение слушает 8000, а наружу отдаётся -p 80:8000.

HEALTHCHECK — жив ли процесс внутри

Запущенный контейнер и работающее приложение — разное: процесс может висеть, потеряв соединение с базой, а Docker будет считать его исправным, потому что он не завершился. HEALTHCHECK даёт базе критерий:

HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
  CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1

Состояние видно в docker ps как starting → healthy / unhealthy. --start-period — фора на запуск: неудачи в это окно не считаются.

⚠️ Docker сам по себе не перезапускает unhealthy-контейнер — он только меняет статус. restart: always реагирует на завершение процесса, а не на здоровье. Перезапуск по здоровью делают оркестраторы (Kubernetes с его liveness probe, Swarm). Главная практическая польза — в Compose: depends_on с condition: service_healthy заставляет приложение ждать, пока база действительно начнёт принимать запросы, а не просто пока её контейнер стартует.

Тома (volumes) — хранение данных

Контейнер эфемерен: удалил — данные внутри исчезли. Чтобы данные жили дольше контейнера (базы, загрузки, логи), их выносят в том.

docker volume create mydata
docker run -d --name app --mount source=mydata,target=/app/data nginx:latest

Команды для томов:

  • docker volume create — создать том;
  • docker volume ls — список томов;
  • docker volume inspect — подробности тома;
  • docker volume rm — удалить том;
  • docker volume prune — удалить все неиспользуемые тома.

Тома можно шарить между контейнерами, бэкапить и подключать внешние хранилища.

Сети (networks)

Контейнеры объединяются в сети; поведение задаёт драйвер:

Драйвер Когда
bridge по умолчанию: изолированная сеть на одном хосте
host убрать изоляцию, общая сеть с хостом
overlay связать несколько демонов Docker (Swarm), общение между узлами
macvlan контейнеру свой MAC — выглядит как физическое устройство в сети
none сеть отключена
docker network create --driver bridge test-net
docker run -dit --name a1 --network test-net alpine
docker run -it  --name a2 --network test-net alpine
# внутри a2: ping a1  → контейнеры в одной сети видят друг друга по имени

Команды для сетей:

  • docker network create — создать сеть;
  • docker network ls — список сетей;
  • docker network inspect — подробности;
  • docker network connect / disconnect — подключить / отключить контейнер;
  • docker network rm — удалить;
  • docker network prune — удалить неиспользуемые.

Основные команды

Образы

docker build -t my-app:1.0 .          # собрать образ из Dockerfile в текущей папке
docker images                         # список образов
docker pull nginx                     # скачать образ из реестра
docker push my-app:1.0                # отправить образ в реестр (после docker login)
docker tag my-app:1.0 user/my-app:1.0 # дать образу новое имя/тег
docker inspect my-app:1.0             # подробные метаданные (JSON)
docker history my-app:1.0             # из каких слоёв собран
docker save -o app.tar my-app:1.0     # выгрузить образ в архив
docker load -i app.tar                # загрузить образ из архива
docker rmi my-app:1.0                 # удалить образ

Контейнеры

docker run -d -p 8000:8000 --name app my-app:1.0  # создать и запустить (фон, проброс порта)
docker ps                     # запущенные контейнеры (-a — все, включая остановленные)
docker start / stop / restart app   # запустить / остановить / перезапустить
docker kill app               # принудительно убить
docker pause / unpause app    # заморозить / разморозить процессы
docker exec -it app bash      # войти внутрь работающего контейнера
docker logs -f app            # смотреть логи (поток)
docker cp app:/path ./local   # копировать файлы контейнер ↔ хост
docker inspect app            # метаданные контейнера (JSON)
docker stats                  # потребление ресурсов в реальном времени
docker top app                # процессы внутри контейнера
docker rename app web         # переименовать
docker rm app                 # удалить контейнер (-f — остановить и удалить)

Ключевые флаги docker run:

  • -d — запуск в фоне (detached);
  • -p хост:контейнер — проброс порта;
  • --name имя — имя контейнера;
  • -v том:путь — подключить том или папку хоста;
  • -e VAR=val — переменная окружения (--env-file .env — из файла);
  • --network имя — подключить к сети;
  • --restart unless-stopped — политика перезапуска;
  • --rm — удалить контейнер после остановки.

Реестр и система

docker login / docker logout  # вход / выход в реестр (Docker Hub или свой)
docker search nginx           # искать образы в Docker Hub
docker info                   # информация о системе Docker
docker version                # версии клиента и демона
docker system df              # сколько занято места
docker system prune           # убрать мусор (-a — агрессивно, --volumes — и тома)

Docker Compose — оркестрация нескольких контейнеров

Один контейнер запускают через docker run. Но реальное приложение — это несколько сервисов: веб-сервер + база + кеш. Поднимать и связывать их руками неудобно. Docker Compose — инструмент для описания и запуска многоконтейнерных приложений: вся конфигурация в одном YAML-файле (compose.yaml / docker-compose.yml), запуск — одной командой.

Оркестрация — автоматизация жизненного цикла контейнеров: развёртывание, масштабирование, балансировка нагрузки, доступность и сети между контейнерами. Compose покрывает её для одного хоста (для кластера — Kubernetes или Docker Swarm).

Файл состоит из трёх секций верхнего уровня:

  • services — сами контейнеры: какой образ или сборка, порты, перезапуск, сети, тома, зависимости;
  • volumes — тома; Compose создаёт их сам по описанию;
  • networks — сети между сервисами.
services:
  web:
    build: .                 # собрать из локального Dockerfile
    ports: ["8000:8000"]
    env_file: .env
    depends_on: [db, redis]  # стартовать после базы и кеша
  db:
    image: postgres:16
    volumes: ["pgdata:/var/lib/postgresql/data"]
    restart: unless-stopped  # перезапускать, если упал
  redis:
    image: redis:7

volumes:
  pgdata:

Ключи сервиса: image или build, ports, volumes, depends_on, env_file / environment, restart, networks.

Ловушка depends_on. По умолчанию он ждёт только запуска контейнера, а не готовности сервиса принимать запросы. База может быть ещё в процессе инициализации, а приложение уже стартовало и упало с ошибкой подключения. Чтобы дождаться именно готовности, нужен healthcheck у зависимости и condition: service_healthy:

depends_on:
  db:
    condition: service_healthy

Команды Compose:

docker compose up -d        # собрать (если надо) и поднять всё в фоне
docker compose up --build   # пересобрать образы и поднять
docker compose down         # остановить и удалить контейнеры и сети (-v — и тома)
docker compose start / stop / restart   # управлять уже созданными
docker compose build        # собрать / пересобрать образы
docker compose ps           # статус сервисов
docker compose logs -f      # логи всех сервисов (поток)
docker compose exec web bash   # войти в сервис
docker compose run web pytest  # разовый запуск команды в новом контейнере сервиса
docker compose pull         # скачать образы сервисов
docker compose config       # проверить и показать итоговый конфиг
docker compose top          # процессы в сервисах

Когда одних и тех же сервисов нужно несколько вариантов — прод без открытых портов, разработка с портами и веб-интерфейсами — файлы не копируют, а переиспользуют через include и extends. Как именно: Docker Compose — несколько окружений.

Docker против Docker Compose

Docker (docker run) Docker Compose
Масштаб один контейнер много связанных сервисов
Конфигурация флаги в командной строке один YAML-файл
Запуск команда на каждый контейнер docker compose up для всех
Когда разовый запуск, простой образ приложение из веб + БД + кеш и т.п.

Сквозной пример: контейнеризируем веб-приложение

Соберём типичный backend на Python (FastAPI) с базой PostgreSQL и кешем Программирование/BackEnd/Python/Библиотеки/Сторонние/Redis — от пустой папки до рабочего http://localhost:8000. Делаем по порядку.

Шаг 1. Структура проекта.

myapp/
├── app/
│   └── main.py          # код приложения
├── requirements.txt     # зависимости Python
├── Dockerfile           # как собрать образ приложения
├── .dockerignore        # что НЕ копировать в образ
├── docker-compose.yml   # как поднять все сервисы вместе
└── .env                 # секреты и настройки (НЕ в git!)

Шаг 2. requirements.txt — что ставим внутрь образа:

fastapi
uvicorn[standard]
psycopg2-binary
redis

Шаг 3. app/main.py — минимальное приложение:

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def root():
    return {"status": "ok"}

Шаг 4. Dockerfile — рецепт образа (читается сверху вниз; цифры = порядок выполнения):

FROM python:3.12-slim        # 1. базовый образ с Python
WORKDIR /app                 # 2. рабочая папка внутри контейнера
COPY requirements.txt .      # 3. сначала зависимости — ради кеша
RUN pip install --no-cache-dir -r requirements.txt   # 4. установка
COPY . .                     # 5. затем код приложения
EXPOSE 8000                  # 6. порт, который слушаем
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]   # 7. запуск

Почему --host 0.0.0.0, а не 127.0.0.1. У контейнера свой сетевой стек, и -p пробрасывает порт на его интерфейс. Приложение, слушающее внутри контейнера петлю 127.0.0.1, останется недоступным даже с правильным -p — подробно про интерфейсы и привязку.

Шаг 5. .dockerignore — чтобы не тащить лишнее в образ (легче и безопаснее):

.git
__pycache__/
*.pyc
.env
venv/

Шаг 6. docker-compose.yml — поднимаем приложение + базу + кеш одной командой:

services:
  web:
    build: .                 # собрать из Dockerfile рядом
    ports: ["8000:8000"]     # наружу на localhost:8000
    env_file: .env           # переменные из .env
    depends_on: [db, redis]  # стартовать после базы и кеша
    restart: unless-stopped
  db:
    image: postgres:16       # готовый образ Postgres
    environment:
      POSTGRES_PASSWORD: secret
    volumes: ["pgdata:/var/lib/postgresql/data"]  # данные переживут пересоздание
    restart: unless-stopped
  redis:
    image: redis:7

volumes:
  pgdata:                    # именованный том для базы

Шаг 7. .env — настройки и секреты отдельно от кода:

DATABASE_URL=postgresql://postgres:secret@db:5432/postgres
REDIS_URL=redis://redis:6379/0

Внутри Compose сервисы видят друг друга по имени — db, redis, а не localhost. Это и есть встроенная сеть Compose.

Шаг 8. Запуск по порядку:

docker compose up -d --build   # 1. собрать образ web и поднять все три сервиса
docker compose ps              # 2. проверить, что web, db, redis подняты
docker compose logs -f web     # 3. смотреть логи приложения
#    открыть http://localhost:8000  → {"status": "ok"}
docker compose down            # 4. остановить и удалить (том pgdata с данными останется)

Как это читать. Docker собирает образ web по Dockerfile, скачивает готовые postgres и redis, создаёт между ними сеть и том pgdata под базу. depends_on задаёт порядок старта, web ходит в базу и кеш по их именам. Изменил код — docker compose up -d --build пересоберёт только web.

Связи

Источники