Multi-stage build: drámaian kisebb image-ek
A multi-stage build segítségével a build eszközöket leválaszthatod a végső image-ről, így az tört része lesz az eredeti méretnek. Megnézzük, hogyan működik egy valódi példán.
Ha valaha is meglepődtél azon, hogy egy egyszerű kis alkalmazás Docker image-e hirtelen 800 MB-ra vagy akár 1,5 GB-ra hízott, akkor nem vagy egyedül. A leggyakoribb ok, hogy a végső image-be belekerülnek a fordításhoz, csomagtelepítéshez és buildeléshez használt eszközök is — fordítók, csomagkezelők, fejlesztői függőségek —, amelyekre a futó alkalmazásnak már egyáltalán nincs szüksége. A multi-stage build pontosan ezt a problémát oldja meg elegánsan: szétválasztja a “hogyan építem meg” és a “mi fut élesben” lépéseket. Ebben a cikkben megnézzük, hogyan működik, és egy valódi példán keresztül látni fogod, hogyan zsugorodhat egy image tört részére.
Mi a baj a build eszközökkel?
Képzeld el, hogy egy Go vagy egy Node.js alkalmazást buildelsz. A fordításhoz vagy a függőségek telepítéséhez szükséged van egy csomó eszközre:
- a Go fordítóra (több száz megabájt),
- a teljes Node.js + npm csomagkezelőre és a
devDependenciescsomagokra, - esetleg
git-re,make-re, build-essential csomagokra, - ideiglenes cache-fájlokra és köztes artefaktumokra.
A gond az, hogy ha mindezt egyetlen Dockerfile-ban, egyetlen image-en belül csinálod, akkor ezek mind ott maradnak a kész image-ben. Pedig a lefordított bináris vagy a legenerált statikus fájlok futtatásához ezekre már nincs szükség. Minden felesleges réteg lassítja a docker pull és docker push műveleteket, több tárhelyet foglal, és — ami biztonsági szempontból a legfontosabb — növeli a támadási felületet, hiszen több szoftver van az image-ben, amiben sebezhetőség lehet.
💡 Tipp: A legjobb biztonság az, ami nincs is jelen. Ha egy fordító nincs benne a futó image-edben, akkor azon keresztül nem is támadhatnak meg.
Hogyan működik a multi-stage build?
A multi-stage build lényege, hogy egyetlen Dockerfile-ban több FROM utasítást használhatsz. Minden FROM egy új építési fázist (stage) kezd. A fázisokat elnevezheted az AS <név> szintaxissal, és a COPY --from=<név> paranccsal egy korábbi fázisból átmásolhatod a kész artefaktumokat egy következő, tisztább fázisba.
A trükk a következő: az utolsó fázis lesz a végső image. Minden korábbi fázis csak segédeszközként szolgál a buildhez, és nem kerül bele a kész image-be. Így a build eszközök ott maradnak a “buildelő” fázisban, és csak a valóban szükséges fájlokat viszed tovább a kicsi, “futtató” fázisba.
Egy Go példa: 800 MB-ról 15 MB-ra
Nézzük a klasszikus, naiv megközelítést egy Go alkalmazásra:
FROM golang:1.22
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o szerver .
CMD ["./szerver"]
Ez működik, de a golang:1.22 alapkép önmagában több száz megabájt, és a teljes Go toolchain benne marad. Most nézzük ugyanezt multi-stage buildel:
# 1. fázis: build
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o szerver .
# 2. fázis: a végső, minimális image
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/szerver /szerver
USER nonroot:nonroot
CMD ["/szerver"]
Mi történt itt? Az első fázis (builder) elvégzi a teljes fordítást a teljes Go környezetben. A második fázis egy minimális distroless image-ből indul, amelyben semmi más nincs, csak a futtatáshoz feltétlenül szükséges fájlok — se shell, se csomagkezelő. A COPY --from=builder csak a lefordított binárist viszi át. Az eredmény egy 10-20 MB körüli image a 800 MB helyett.
⚠️ Figyelem: A
CGO_ENABLED=0itt fontos: így statikusan linkelt binárist kapsz, amely libc nélkül is fut a distroless static image-ben. CGO-t igénylő kódnál válassz olyan alapképet, amely tartalmazza a szükséges rendszerkönyvtárakat.
Egy Node.js példa: frontend build
A multi-stage build nem csak fordított nyelveknél hasznos. Egy tipikus eset egy frontend (például React vagy Vue) alkalmazás, ahol a build során node_modules és devDependencies kell, de élesben már csak a legenerált statikus fájlokat akarod kiszolgálni:
# 1. fázis: a frontend buildelése
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# 2. fázis: statikus fájlok kiszolgálása nginxszel
FROM nginx:1.27-alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
A végső image-ben már nyoma sincs a node_modules mappának vagy az npm-nek — csak az nginx és a legenerált HTML/CSS/JS fájlok. Ez gyakran a több száz megabájtos build image helyett egy 40-50 MB-os, kompakt eredményt ad.
Méret-összehasonlítás a gyakorlatban
Nézzük meg, mekkora különbségről beszélünk. A docker images paranccsal magad is ellenőrizheted az eredményt:
docker images
| Megközelítés | Tipikus image-méret |
|---|---|
Go, naiv (egyetlen golang image) | ~800 MB |
| Go, multi-stage + distroless | ~10-20 MB |
Node frontend, egyetlen node image | ~1,1 GB |
| Node frontend, multi-stage + nginx-alpine | ~45 MB |
A különbség nem kozmetikai. Egy CI/CD pipeline-ban, ahol naponta tucatszor pusholsz és pullolsz image-eket, a kisebb méret jelentősen gyorsítja a deployt, csökkenti a regisztrációs tárhelyköltséget, és gyorsítja a skálázódást is, hiszen az új konténerek hamarabb indulnak el.
Haladó trükkök
A multi-stage build ennél többre is képes. Néhány hasznos minta:
- Csak egy adott fázisig buildelni: a
docker build --target builder -t myapp:dev .paranccsal megállíthatod a buildet egy köztes fázisnál — ez kiváló fejlesztői image-ek készítéséhez. - Külső image mint fázis: a
COPY --from=nginx:latest /etc/nginx/nginx.conf ./segítségével akár egy publikus image-ből is másolhatsz fájlt, nem csak a saját fázisaidból. - Közös base fázis: definiálhatsz egy közös
basefázist, amelyből több későbbi fázis is származik, így csökkented az ismétlést. - Párhuzamos buildek: a BuildKit a független fázisokat párhuzamosan tudja építeni, ami gyorsítja a teljes buildet.
Ha most kezdesz ismerkedni a Dockerfile felépítésével, érdemes először a Dockerfile írása lépésről lépésre cikket átnézni, hogy a fázisokon belüli rétegezést is jól értsd.
Mikor használd?
A multi-stage build szinte mindig megéri, ha:
- fordítást vagy build lépést igényel az alkalmazásod (Go, Rust, Java, C/C++, TypeScript, frontend bundle),
- vannak csak buildhez kellő függőségeid (
devDependencies, build-essential), - fontos a kis image-méret, a gyors deploy vagy a kisebb támadási felület.
Egyetlen helyzet, amikor kevésbé hoz hasznot: ha eleve egy értelmezett nyelvű, minimális függőségű alkalmazásod van, ahol nincs igazi build lépés. De még ott is hasznos lehet a tiszta szétválasztás kedvéért.
Összefoglalás
A multi-stage build az egyik leghatékonyabb eszköz a kompakt, biztonságos Docker image-ekhez. A lényeg, hogy több FROM utasítással szétválasztod a buildelő és a futtató környezetet, majd a COPY --from paranccsal csak a kész artefaktumot viszed át a minimális végső image-be. Az eredmény gyakran tört része az eredeti méretnek, gyorsabb deploy és kisebb támadási felület.
Most te jössz: vedd elő a legnagyobb image-edet, és próbáld ki rajta a multi-stage megközelítést! Ha még az alapoknál tartasz, nézd meg a Kezdő lépések útmutatót, vagy ismerkedj meg azzal, hogyan működik a Docker a motorháztető alatt. A következő lépésként pedig érdemes elmélyedni a Docker Compose bevezető cikkben is.