Що таке 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, безпечніше.
Швидкий приклад
# Стейдж 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 /app/dist /usr/share/nginx/html
EXPOSE 80$ 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 MBNode 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 падає з хвилин до секунд.
Іменування і посилання на стейджі
FROM golang:1.23 AS build # named стейдж
# ...
FROM alpine:3.21 AS test # ще один named стейдж
COPY /out/server /server
RUN /server --self-test
FROM alpine:3.21
COPY /out/server /server
# ...AS <name>іменує стейдж; посилаєшся по імені через--from=<name>.- Можна посилатися по індексу (
--from=0,--from=1), але імена читабельніші. COPY --from=<external-image>теж працює (COPY --from=alpine:3.21 /etc/passwd /tmp/), витягує файли з будь-якого image.
Збірка лише одного стейджу
# Зібрати весь Dockerfile, отримати фінальний image
docker build -t myapp .
# Зібрати лише стейдж 'build' (корисно для CI-інтеграційних тестів)
docker build --target build -t myapp:dev .Флаг --target зупиняється на named-стейджі і використовує його як output image. Корисно для прогону тестів проти build-стейджу перед фінальною збіркою.
Поширені патерни
Патерн 1: компільована мова → мінімальний runtime
FROM golang:1.23-alpine AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /out/server ./cmd/server
FROM scratch
COPY /out/server /server
USER 65532:65532
ENTRYPOINT ["/server"]Go-бінар у scratch-image. Фінальний розмір: розмір бінаря. Без shell, без libc, без поверхні.
Патерн 2: збір статики, serve через nginx
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 /app/dist /usr/share/nginx/htmlReact, Vue, Svelte build-артефакти, що serve nginx. Node toolchain лишається у builder; фінальний image крихітний.
Патерн 3: compile, потім distroless
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 /app/dist /app/dist
COPY /app/node_modules /app/node_modules
WORKDIR /app
CMD ["dist/server.js"]Node-застосунок на distroless-image Google. Має Node runtime, але без shell, без apt, без нічого іншого.
Типові помилки
Забути --from= і копіювати з локального build context
# НЕПРАВИЛЬНО: копіює з build context (твій лептоп), не з build-стейджу
FROM nginx:1.27-alpine
COPY dist/ /usr/share/nginx/html/ # потребує dist/ у build context
# ПРАВИЛЬНО: копіюй з named-стейджу
FROM nginx:1.27-alpine
COPY /app/dist /usr/share/nginx/html/Без --from COPY бере build context. З --from=<stage> COPY бере filesystem того стейджу.
Роздути runtime-стейдж build-tools «про всяк»
# НЕПРАВИЛЬНО: знищує сенс
FROM nginx:1.27-alpine
RUN apk add --no-cache curl jq vim git # додає 50+ MB
COPY /app/dist /usr/share/nginx/htmlЯкщо тягнешся додавати tools у runtime-стейдж, спитай чому. Debug-tools у :debug-варіанті, не у проді. Build-tools у build-стейджі.
Не prune dev-залежності
# НЕПРАВИЛЬНО: копіює всі node_modules включно з dev
COPY /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-стейджі
# НЕПРАВИЛЬНО: білд не відтворюваний
FROM node:latest AS build # яка версія цього білду?
# ПРАВИЛЬНО: пін на версію
FROM node:22.11-alpine AS buildMulti-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
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 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY /out/server /server
EXPOSE 8080
USER 65532:65532
ENTRYPOINT ["/server"]Фінальний image: ~10 MB (лише Go-бінар плюс CA-certs для HTTPS). Менший за source code.
Three-stage: build → test → runtime
# Стейдж 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 /app/dist /usr/share/nginx/html# CI: проганяємо тести; fail-fast
$ docker build --target test .
# CI: збираємо прод-артефакт (пропускає test-стейдж, якщо не залежність)
$ docker build -t mysite .Той самий Dockerfile, кілька workflow. Тести живуть, де живуть їх залежності; проду немає ні того, ні іншого.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.
Коментарі
Ще немає коментарів