Перейти к содержанию
Innovialabs

Multi-stage сборка в Docker: как разделить сборку и запуск

Практика3 мин

Коротко

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

Коротко: что такое multi-stage сборка

Multi-stage build — это Dockerfile с несколькими инструкциями FROM. Каждая FROM начинает новый этап (stage) со своим базовым образом. В финальный образ попадает только последний этап, а из предыдущих вы явно забираете нужные файлы через COPY --from=<этап>.

Что это даёт:

  • Меньший размер — в продакшен не едут компилятор, менеджер пакетов, исходники и тестовые зависимости.
  • Меньше поверхность атаки — чем меньше утилит в образе, тем меньше уязвимостей находят сканеры.
  • Один Dockerfile вместо связки «build-скрипт + отдельный runtime Dockerfile».
  • Кэширование по этапам — Docker переиспользует слои каждого этапа, если входные файлы не менялись.

Go: из компилятора в почти пустой образ

Go собирается в статический бинарник, поэтому финальный образ может быть минимальным.

FROM golang:1.22 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app ./cmd/server

FROM gcr.io/distroless/static-debian12
COPY --from=build /app /app
USER nonroot
ENTRYPOINT ["/app"]

Ключевые моменты: CGO_ENABLED=0 даёт бинарник без зависимости от libc, а go.mod копируется до исходников, чтобы слой с зависимостями кэшировался.

Node.js: отдельно зависимости для сборки и для запуска

У Node.js-приложения нет одного бинарника, но разделение всё равно полезно: TypeScript, сборщики и тестовые пакеты нужны только на этапе сборки.

FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-slim
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]

В финальном этапе ставятся только production-зависимости, а из этапа сборки копируется готовая папка dist.

Фронтенд: собрать в Node, отдать через Nginx

Для SPA (React, Vue, Svelte) Node нужен только чтобы собрать статику. Отдавать её лучше лёгким веб-сервером.

FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf

В итоговом образе нет ни Node.js, ни node_modules — только Nginx и файлы сборки. Путь dist зависит от сборщика: у некоторых это build.

Как измерить выигрыш

Не доверяйте чужим цифрам — размер зависит от ваших зависимостей. Сравните сами:

  1. Соберите «наивную» версию в один этап: docker build -t app:single -f Dockerfile.single .
  2. Соберите multi-stage: docker build -t app:multi .
  3. Сравните: docker images app
  4. Посмотрите, какие слои занимают место: docker history app:multi

Обычно самая большая разница у компилируемых языков (Go, Rust), где в финале остаётся один бинарник. У Node.js выигрыш скромнее, но заметен за счёт отказа от dev-зависимостей и тяжёлого базового образа.

Частые ошибки

  • Копирование всего контекста без .dockerignore — в образ и кэш попадают node_modules, .git, локальные .env.
  • COPY . . до установки зависимостей — любое изменение кода сбрасывает кэш npm ci или go mod download.
  • Динамически слинкованный бинарник в distroless или scratch — контейнер падает с ошибкой «not found».
  • Запуск от root в финальном этапе, хотя образ это позволяет изменить одной строкой USER.
  • Безымянные этапы — COPY --from=0 ломается при добавлении нового этапа. Давайте имена через AS.

Частые вопросы

Да. Флаг --target останавливает сборку на указанном этапе: docker build --target build -t app:build .. Это удобно для запуска тестов в CI на этапе со всеми инструментами.

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.