Большой образ — это медленная загрузка, больше места на диске и большая поверхность атаки. Multi-stage builds решают эту проблему.
Проблема: образ тащит лишнее
Типичный Dockerfile для Python-приложения:
FROM python:3.12
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "main.py"]
Образ python:3.12 весит ~1 ГБ. В нём — компилятор, заголовочные файлы, весь toolchain. В рантайме всё это не нужно. Нужен только итоговый код.
Идея Multi-stage builds
Разделить сборку на этапы:
- Builder stage — устанавливаем зависимости, компилируем, готовим артефакты
- Runtime stage — копируем только готовый результат в минимальный образ
Промежуточные образы не попадают в финальный результат.
Синтаксис
# Этап 1: сборка
FROM python:3.12 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --prefix=/install -r requirements.txt
# Этап 2: рантайм
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /install /usr/local
COPY . .
CMD ["python", "main.py"]
Ключевые конструкции:
- FROM ... AS builder — даём имя этапу
- COPY --from=builder — копируем файлы из другого этапа
- Можно ссылаться по имени этапа или по его номеру (--from=0)
Пример для Python приложения
Приложение с компилируемыми зависимостями (например, psycopg2, Pillow):
# Этап сборки: нужен компилятор для C-расширений
FROM python:3.12 AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# Финальный образ: без компилятора
FROM python:3.12-slim
WORKDIR /app
# Копируем установленные пакеты
COPY --from=builder /install /usr/local
# Копируем только исходный код
COPY src/ ./src/
COPY main.py .
# Создаём непривилегированного пользователя
RUN adduser --disabled-password --no-create-home appuser
USER appuser
CMD ["python", "main.py"]
Разница в размере:
python:3.12 → ~1.0 ГБ (базовый образ)
python:3.12 + deps → ~1.3 ГБ (с зависимостями)
python:3.12-slim + deps → ~200 МБ (multi-stage результат)
Пример для Node.js: сборка фронтенда
Node.js-проекты часто компилируют TypeScript или бандлят React. В рантайме нужен только dist/ и node_modules (только prod).
# Этап 1: установка всех зависимостей и сборка
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json .
RUN npm ci # устанавливаем все зависимости
COPY . .
RUN npm run build # компилируем TypeScript / бандлим
# Этап 2: только prod-зависимости
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json .
RUN npm ci --omit=dev # только production
# Этап 3: финальный образ
FROM node:20-alpine
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
USER node
CMD ["node", "dist/index.js"]
Для фронтенда, который раздаётся через nginx:
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json .
RUN npm ci
COPY . .
RUN npm run build # результат в /app/dist
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
Финальный образ — только nginx + статика. Node.js отсутствует полностью.
Размер:
node:20-alpine + devDeps + build → ~800 МБ
nginx:alpine + static dist → ~25 МБ
Реальные размеры до и после
| Приложение | До | После |
|---|---|---|
| Python API (с psycopg2) | 1.3 ГБ | 190 МБ |
| React SPA | 850 МБ | 25 МБ |
| Node.js API | 600 МБ | 120 МБ |
| Go сервис | 800 МБ | 12 МБ |
Go — отдельная история: статический бинарник копируется в scratch (пустой образ):
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o server .
FROM scratch
COPY --from=builder /app/server /server
CMD ["/server"]
# Размер образа: ~8 МБ
Выборочная сборка этапов
# Собрать только до конкретного этапа
docker build --target builder -t myapp:builder .
# Полезно для отладки: войти в builder-образ
docker run -it myapp:builder sh
Итог
Multi-stage builds — стандарт для production-образов. Меньший образ:
- быстрее скачивается и деплоится
- занимает меньше места в registry
- имеет меньшую поверхность атаки (нет компилятора, отладчиков)
💬 Комментарии (0)
Комментариев пока нет
Станьте первым, кто поделится мнением об этой статье!