Skip to main content

Що таке multi-stage build і навіщо він потрібен?

Multi-stage builds це стандартний спосіб шиппити малі, безпечні Docker-image. Ідея: збирай важко, шиппи легко. Бери fat builder-image з усіма твоїми компіляторами і tools, копіюй лише фінальний артефакт у мінімальний runtime-стейдж. Toolchain ніколи не приземляється у проді.

Теорія

TL;DR

  • Кілька блоків FROM у одному Dockerfile. Кожен це стейдж.
  • Стейджі можна іменувати через AS <name>. Пізніші стейджі посилаються на раніші через COPY --from=<name>.
  • Лише останній стейдж це фінальний image (якщо не задаєш --target під час білду).
  • Поширений патерн: стейдж 1 = повний SDK (компілятори, package manager); стейдж 2 = мінімальний runtime (alpine, distroless, scratch).
  • Результат: фінальний image на 30 MB замість 600 MB. Менша поверхня атаки, швидший pull, безпечніше.

Швидкий приклад

dockerfile
# Стейдж 1: build (повний Node-toolchain, ~300 MB під час збірки) FROM node:22-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build # дає /app/dist # Стейдж 2: runtime (малий nginx-image) FROM nginx:1.27-alpine COPY --from=build /app/dist /usr/share/nginx/html EXPOSE 80
bash
$ docker build -t mysite . [+] Building 12.4s => [build 1/5] FROM node:22-alpine => [build 2/5] WORKDIR /app => [build 3/5] COPY package*.json ./ => [build 4/5] RUN npm ci # 200 MB node_modules => [build 5/5] RUN npm run build # дає /app/dist => [stage-1 1/2] FROM nginx:1.27-alpine => [stage-1 2/2] COPY --from=build /app/dist /usr/share/nginx/html => exporting to image # фінальний image ~25 MB

Node SDK і node_modules існували у стейджі build, але ніколи не шиппяться. Лише фінальний скомпільований вивід (/app/dist) попадає у image.

Чому це важливо: цифри

ПідхідФінальний розмір image
Single stage (node:22 + усе)600-900 MB
Multi-stage з node:22-alpine фінальним200-300 MB
Multi-stage з nginx:alpine для статики25-30 MB
Multi-stage з distroless фінальним20-50 MB
Multi-stage з FROM scratch (Go binary)5-15 MB

Зменшення розміру у 30 разів типове. Pull-час між сотнями node падає з хвилин до секунд.

Іменування і посилання на стейджі

dockerfile
FROM golang:1.23 AS build # named стейдж # ... FROM alpine:3.21 AS test # ще один named стейдж COPY --from=build /out/server /server RUN /server --self-test FROM alpine:3.21 COPY --from=build /out/server /server # ...
  • AS <name> іменує стейдж; посилаєшся по імені через --from=<name>.
  • Можна посилатися по індексу (--from=0, --from=1), але імена читабельніші.
  • COPY --from=<external-image> теж працює (COPY --from=alpine:3.21 /etc/passwd /tmp/), витягує файли з будь-якого image.

Збірка лише одного стейджу

bash
# Зібрати весь Dockerfile, отримати фінальний image docker build -t myapp . # Зібрати лише стейдж 'build' (корисно для CI-інтеграційних тестів) docker build --target build -t myapp:dev .

Флаг --target зупиняється на named-стейджі і використовує його як output image. Корисно для прогону тестів проти build-стейджу перед фінальною збіркою.

Поширені патерни

Патерн 1: компільована мова → мінімальний runtime

dockerfile
FROM golang:1.23-alpine AS build WORKDIR /src COPY . . RUN CGO_ENABLED=0 go build -o /out/server ./cmd/server FROM scratch COPY --from=build /out/server /server USER 65532:65532 ENTRYPOINT ["/server"]

Go-бінар у scratch-image. Фінальний розмір: розмір бінаря. Без shell, без libc, без поверхні.

Патерн 2: збір статики, serve через nginx

dockerfile
FROM node:22-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM nginx:1.27-alpine COPY --from=build /app/dist /usr/share/nginx/html

React, Vue, Svelte build-артефакти, що serve nginx. Node toolchain лишається у builder; фінальний image крихітний.

Патерн 3: compile, потім distroless

dockerfile
FROM node:22 AS build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build RUN npm prune --omit=dev FROM gcr.io/distroless/nodejs22 COPY --from=build /app/dist /app/dist COPY --from=build /app/node_modules /app/node_modules WORKDIR /app CMD ["dist/server.js"]

Node-застосунок на distroless-image Google. Має Node runtime, але без shell, без apt, без нічого іншого.

Типові помилки

Забути --from= і копіювати з локального build context

dockerfile
# НЕПРАВИЛЬНО: копіює з build context (твій лептоп), не з build-стейджу FROM nginx:1.27-alpine COPY dist/ /usr/share/nginx/html/ # потребує dist/ у build context # ПРАВИЛЬНО: копіюй з named-стейджу FROM nginx:1.27-alpine COPY --from=build /app/dist /usr/share/nginx/html/

Без --from COPY бере build context. З --from=<stage> COPY бере filesystem того стейджу.

Роздути runtime-стейдж build-tools «про всяк»

dockerfile
# НЕПРАВИЛЬНО: знищує сенс FROM nginx:1.27-alpine RUN apk add --no-cache curl jq vim git # додає 50+ MB COPY --from=build /app/dist /usr/share/nginx/html

Якщо тягнешся додавати tools у runtime-стейдж, спитай чому. Debug-tools у :debug-варіанті, не у проді. Build-tools у build-стейджі.

Не prune dev-залежності

dockerfile
# НЕПРАВИЛЬНО: копіює всі node_modules включно з dev COPY --from=build /app/node_modules /app/node_modules # ПРАВИЛЬНО: спершу prune у build-стейджі RUN npm prune --omit=dev # у build-стейджі # АБО: встанови лише prod-deps в окремому стейджі

Dev-deps (TypeScript-компілятор, eslint, jest) можуть подвоїти розмір node_modules. Pruneй.

Використання latest tag у build-стейджі

dockerfile
# НЕПРАВИЛЬНО: білд не відтворюваний FROM node:latest AS build # яка версія цього білду? # ПРАВИЛЬНО: пін на версію FROM node:22.11-alpine AS build

Multi-stage не звільняє тебе від проблеми latest. Пінься на кожному FROM.

Реальне застосування

  • Усі великі cloud-native проекти: Kubernetes, Prometheus, Grafana, кожен офіційний image multi-stage. Фінальні image крихітні.
  • CI/CD пайплайни: docker build --target test крутить test-стейдж; docker build --target prod дає deploy-артефакт. Один Dockerfile, кілька output.
  • Cross-compilation: стейдж 1 cross-compile під ARM, стейдж 2 тонкий ARM-base. У комбінації з docker buildx для multi-arch.
  • Мінімізація supply-chain ризику: кожен пакет, що не у фінальному image, це менше CVE для сканування. Fortune-500 security-команди вимагають multi-stage для прод-image.

Питання для поглиблення

Q: Як Docker знає, який стейдж фінальний?


A: Це останній стейдж у Dockerfile, якщо не передаєш --target <stage> під час білду. Усе після --target стейджу ігнорується.

Q: Чи можуть стейджі крутитися паралельно?


A: Так, з BuildKit. Стейджі без залежності один від одного крутяться конкурентно. Стейджі з COPY --from=other чекають, поки other завершиться.

Q: Яка різниця між multi-stage і використанням кількох Dockerfile?


A: Multi-stage = один файл, один виклик docker build, внутрішні стейджі. Кілька Dockerfile = один файл на стейдж, ручна координація. Multi-stage простіший, чистіший cache і це сучасна норма.

Q: Чи кожен стейдж дає окремий image?


A: Внутрішньо так, але лише фінальний стейдж отримує tag і потрапляє у твій image-list. Проміжні стейджі тримаються у build-cache і можуть бути referenced або перебудовані.

Q: (Senior) Як використати multi-stage для security-сканування?


A: Додай scan-стейдж між build і runtime: FROM aquasec/trivy AS scan + RUN trivy fs --severity HIGH,CRITICAL --exit-code 1 /from-build. Білд падає, якщо знайдено HIGH/CRITICAL CVE. У комбінації з --target scan у CI отримуєш необхідний security-gate. Runtime-стейдж не зачеплено.

Приклади

Go-сервіс у scratch

dockerfile
FROM golang:1.23-alpine AS build WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /out/server ./cmd/server FROM scratch COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=build /out/server /server EXPOSE 8080 USER 65532:65532 ENTRYPOINT ["/server"]

Фінальний image: ~10 MB (лише Go-бінар плюс CA-certs для HTTPS). Менший за source code.

Three-stage: build → test → runtime

dockerfile
# Стейдж 1: build FROM node:22-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build # Стейдж 2: test (використовує build-стейдж) FROM build AS test RUN npm test # завалити весь білд, якщо тести впали # Стейдж 3: runtime (тонкий фінальний image) FROM nginx:1.27-alpine COPY --from=build /app/dist /usr/share/nginx/html
bash
# CI: проганяємо тести; fail-fast $ docker build --target test . # CI: збираємо прод-артефакт (пропускає test-стейдж, якщо не залежність) $ docker build -t mysite .

Той самий Dockerfile, кілька workflow. Тести живуть, де живуть їх залежності; проду немає ні того, ні іншого.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Коментарі

Ще немає коментарів