📖 Теория: Multistage Dockerfile и Docker Compose

⚡ Теория в двух абзацах

Multistage build — это несколько секций FROM в одном Dockerfile. Каждая секция — отдельная стадия. Финальная стадия копирует из предыдущих только то, что нужно для запуска (COPY --from=build). Инструменты сборки (gcc, maven, pip install) остаются в промежуточных слоях и не попадают в финальный образ.

Docker Compose — инструмент для запуска нескольких контейнеров как единого приложения. Вы описываете все сервисы, их образы, порты, тома, переменные окружения и сети в одном YAML-файле, а затем поднимаете всё командой docker compose up. Compose v2 встроен в Docker и не требует отдельной установки.

Часть 1: Multi Stage Build

Зачем нужен Multistage build

Типичная проблема: чтобы собрать приложение, нам нужны компиляторы, пакетные менеджеры, тестовые инструменты. Но в продакшне они не нужны — они только увеличивают образ и расширяют поверхность атаки.

До Multistage build разработчики решали это скриптами-оберёртками: собирали в одном контейнере, копировали артефакт наружу, создавали новый образ. Это было неудобно и сложно в CI/CD.

Multistage build решает проблему внутри одного Dockerfile:

  • Промежуточные стадии содержат все инструменты сборки — они кешируются Docker и не попадают в финальный образ.
  • Финальная стадия копирует только скомпилированные артефакты (COPY --from=).
  • Финальный образ маленький, без лишних зависимостей, с минимальной поверхностью атаки.
Результат на практике: образ Python-приложения с multistage может быть в 3 раза меньше одностадийного. Java-приложение (Maven + JRE) — в 5–10 раз меньше.

Синтаксис: FROM … AS и COPY --from

Каждая стадия начинается с инструкции FROM. Чтобы можно было сослаться на стадию позже, ей дают имя через AS:

# Dockerfile

# Стадия 1: сборка (имя "builder")
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user -r requirements.txt
COPY . .

# Стадия 2: финальный образ
FROM python:3.12-slim
WORKDIR /app
# Копируем только установленные пакеты из стадии builder
COPY --from=builder /root/.local /root/.local
COPY --from=builder /app/app.py .
ENV PATH=/root/.local/bin:$PATH
EXPOSE 5000
CMD ["python", "app.py"]

Ключевые моменты:

  • FROM … AS builder — имя стадии (произвольное, обычно build, builder, test)
  • COPY --from=builder /путь/в/стадии /путь/в/финальном — копируем артефакты
  • Финальная стадия не знает ничего о предыдущих, кроме скопированных файлов
  • Инструменты сборки (apt, pip, gcc) остаются только в промежуточных слоях

Пример: Java-приложение (Maven + OpenJDK)

Из лекции: классический пример — собрать JAR с Maven, запустить только через JRE:

# Dockerfile

# Стадия сборки: нужен Maven и JDK
FROM maven:3.6.0-jdk-8 AS build
COPY src /home/app/src
COPY pom.xml /home/app
RUN mvn -f /home/app/pom.xml clean package -Dmaven.test.skip=true

# Стадия запуска: нужен только JRE (образ намного меньше)
FROM openjdk:8-jdk-slim
COPY --from=build /home/app/target/project-0.0.1-SNAPSHOT.jar \
     /usr/local/lib/accounting.jar
EXPOSE 8099
ENTRYPOINT ["java", "-jar", "/usr/local/lib/accounting.jar"]

Maven-образ весит ~500 МБ. OpenJDK slim — ~200 МБ. Финальный образ не содержит Maven вообще.

Пример: Python-приложение с build-зависимостями

Если пакеты требуют компиляции C-расширений (psycopg2, cryptography и т. д.), стадия сборки нужна:

# Dockerfile

# Стадия builder: компилируем зависимости
FROM python:3.12-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y build-essential
COPY requirements.txt .
RUN pip install --user -r requirements.txt
COPY . .

# Финальный образ: только runtime
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY --from=builder /app/app.py .
ENV PATH=/root/.local/bin:$PATH
EXPOSE 5000
CMD ["python", "app.py"]

build-essential (gcc, make) и кеш apt остаются в стадии builder и не попадают в финальный образ.

Часть 2: Docker Compose

Что такое Docker Compose

Docker Compose — инструмент для определения и запуска многоконтейнерных Docker-приложений. Входит в состав Docker (начиная с Docker Desktop и Docker Engine 20.10+).

Вместо того чтобы запускать каждый контейнер отдельной командой docker run с десятками флагов, вы описываете всё приложение в одном YAML-файле (docker-compose.yml) и управляете им одной командой.

Типичный сценарий: веб-приложение (Python/Node/Go) + база данных (PostgreSQL/MySQL) + кеш (Redis). Без Compose нужно запустить 3 команды с флагами и не забыть подключить их в одну сеть. С Compose — одна команда.

Структура docker-compose.yml

Compose-файл содержит три основных раздела:

  • services — описание каждого контейнера (образ, порты, тома, переменные, зависимости)
  • volumes — именованные тома для хранения данных
  • networks — пользовательские сети для изоляции и связи сервисов
# docker-compose.yml

services:
  web:
    image: nginx:latest
    ports:
      - "8080:80"
    volumes:
      - ./html:/usr/share/nginx/html
    networks:
      - webnet

  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: mydb
    volumes:
      - db_data:/var/lib/mysql
    networks:
      - webnet

volumes:
  db_data:

networks:
  webnet:

Ключевые поля сервиса

Поле Описание Пример
image Готовый образ из реестра image: postgres:16
build Путь к Dockerfile для сборки build: ./app
ports Проброс порта хост:контейнер - "8080:80"
volumes Тома или bind mounts - db_data:/var/lib/mysql
environment Переменные окружения POSTGRES_PASSWORD: secret
depends_on Зависимость от других сервисов depends_on: [db]
networks Сети, к которым подключён сервис - webnet
restart Политика перезапуска restart: always
command Переопределить CMD образа command: python manage.py runserver

Типы сетей в Docker

Docker поддерживает несколько типов сетей. В Docker Compose чаще всего используется bridge:

bridge (мостовая сеть)
По умолчанию. Создаёт изолированный сетевой мост. Контейнеры в одной bridge-сети видят друг друга по имени сервиса. Внешний доступ — только через ports.
host (сеть хоста)
Контейнер использует сетевой стек хоста напрямую. Нет изоляции — порты контейнера = порты хоста. Используется для снижения сетевой задержки.
none (без сети)
Контейнер полностью изолирован от сети. Используется для batch-задач без сетевого взаимодействия.

depends_on и healthcheck

depends_on задаёт порядок запуска: web стартует после db. Но «запущен» ≠ «готов принимать подключения»: MySQL после старта ещё несколько секунд инициализируется.

Решение — использовать healthcheck совместно с depends_on: condition: service_healthy:

# docker-compose.yml

services:
  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: mydb
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 30s
      timeout: 5s
      retries: 5

  web:
    build: ./app
    depends_on:
      db:
        condition: service_healthy

Теперь web стартует только когда MySQL ответит на ping — то есть реально готов к подключениям.

Сборка из исходников: поле build

Если сервис нужно собрать из Dockerfile, используйте build вместо image:

# docker-compose.yml

services:
  web:
    build: ./web          # путь к директории с Dockerfile
    ports:
      - "8000:8000"
    depends_on:
      - db

  db:
    image: postgres:16
    environment:
      POSTGRES_DB: mydb
      POSTGRES_USER: user
      POSTGRES_PASSWORD: secret
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

При docker compose up --build Compose сначала соберёт образ из Dockerfile в ./web, затем запустит контейнер.