Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке multi-stage build і навіщо він потрібен?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Multi-stage Docker build** використовує кілька блоків `FROM` в одному Dockerfile. Раніші стейджі компілюють/збирають артефакт; фінальний стейдж копіює лише артефакт у тонкий runtime-image, лишаючи build-toolchain позаду. ```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 ``` **Головне:** фінальний image містить лише те, що скопіював `--from=build`. Build-deps (`npm`, source code, `node_modules`) ніколи не шиппяться. Результат: менший image, менша поверхня атаки, швидший pull.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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. Тести живуть, де живуть їх залежності; проду немає ні того, ні іншого.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.