Назад в блог
5 мин чтения

Docker multi-stage build: уменьшаем образ в 40 раз

Docker-образ вашего Go-приложения весит 900 МБ? Разбираем на практике, как с помощью multi-stage build уменьшить его до ~20 МБ, повысить безопасность и ускорить деплой.

GoPythonDocker
Docker multi-stage build: уменьшаем образ в 40 раз

Проблема

Многие пишут Dockerfile примерно так:

FROM golang:1.23
WORKDIR /app
COPY . .
RUN go build -o server .
CMD ["./server"]

Это работает. Но в итоговом образе остаются компилятор Go, кэш, исходный код и лишние утилиты. Размер легко превышает 900 МБ. К чему это приводит:

  • Медленный деплой: каждый раз сотни мегабайт загружаются в registry и скачиваются на сервер.
  • Большая поверхность атаки: каждый лишний пакет в образе — потенциальная уязвимость.
  • Лишние расходы: диск, трафик и время CI тратятся впустую.

Решение: multi-stage build

Идея простая: на одном этапе мы собираем приложение, а на другом только запускаем готовый бинарник.

# Этап 1: сборка
FROM golang:1.23 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o server .

# Этап 2: запуск
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /server
USER nonroot:nonroot
ENTRYPOINT ["/server"]

Результат: размер образа примерно 15–25 МБ.

Что изменилось?

  1. AS builder: даём имя первому этапу, чтобы потом забрать из него файлы.
  2. go.mod копируется отдельно: зависимости меняются реже кода, поэтому Docker кэширует этот слой и сборка ускоряется.
  3. CGO_ENABLED=0: получаем статический бинарник без зависимости от внешних C-библиотек.
  4. -ldflags="-s -w": убираем отладочную информацию, бинарник становится меньше.
  5. distroless: внутри нет ни shell, ни пакетного менеджера. Только ваше приложение.
  6. USER nonroot: контейнер работает без прав root.

Примечание: в distroless нет shell, поэтому docker exec -it ... sh не сработает. Для отладки используйте вариант static-debian12:debug.

А для Python это работает?

Да. На этапе сборки собираем wheel-пакеты, а для запуска берём slim-образ:

FROM python:3.12 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt

FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels
COPY . .
CMD ["python", "main.py"]

Выигрыш здесь не такой впечатляющий, как в Go. Но компиляторы и инструменты сборки не попадают в итоговый образ, а это полезно и для размера, и для безопасности.

Итог

Multi-stage build — одна из самых простых оптимизаций: вы тратите 5 минут на Dockerfile и выигрываете время и безопасность при каждом деплое. Если в вашем проекте до сих пор один FROM, попробуйте сегодня и сравните вывод docker images.