Multi-stage build: jak zmenšit Docker image o stovky MB
Jak zmenšit Docker image pomocí multi-stage buildu – příklady pro Node.js, Go a Python, distroless a scratch, analýza vrstev nástrojem dive a srovnání přístupů.
Image s Node.js aplikací má běžně přes 1 GB, přitom samotná aplikace zabere pár megabajtů. Zbytek tvoří kompilátory, dev závislosti a cache, které za běhu nepotřebujete. Multi-stage build je odstraní jednoduše a bez triků.
Proč na velikosti záleží
- Rychlejší deploy – menší image se rychleji stáhne na server i do CI.
- Méně místa na disku VPS.
- Menší útočná plocha – co v image není (kompilátor, shell, curl), nemůže útočník zneužít a skener v tom nenajde zranitelnosti.
Jak zjistit, co image zabírá
docker images moje-app
docker history moje-app:latest
Podrobnější pohled po vrstvách nabízí nástroj dive:
docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock wagoodman/dive moje-app:latest
Ukáže, které soubory která vrstva přidala, a kolik místa je „promarněno“.
Princip multi-stage buildu
V jednom Dockerfile je několik FROM. Každý začíná novou fází (stage). Do finální fáze zkopírujete jen výsledek předchozích fází pomocí COPY --from=.... Vše ostatní (zdrojáky, kompilátory, dev závislosti) do výsledného image vůbec nevstoupí.
Příklad 1: Node.js frontend (React/Vite) servírovaný Nginxem
# Fáze 1: build
FROM node:20-bookworm-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Fáze 2: jen statické soubory
FROM nginx:1.27-alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
Výsledek obsahuje Nginx a hotové HTML/JS/CSS. node_modules ani Node.js v něm nejsou, takže image má typicky desítky MB.
Příklad 2: Node.js backend
FROM node:20-bookworm-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
FROM node:20-bookworm-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-bookworm-slim
ENV NODE_ENV=production
WORKDIR /app
COPY --from=deps --chown=node:node /app/node_modules ./node_modules
COPY --from=build --chown=node:node /app/dist ./dist
COPY --chown=node:node package.json ./
USER node
CMD ["node", "dist/server.js"]
Finální image má jen produkční závislosti (--omit=dev) a zkompilovaný kód. TypeScript, testovací knihovny a bundler zůstaly v mezifázích.
Příklad 3: Go – image o velikosti binárky
Go kompiluje do jednoho statického souboru, takže finální image může být prakticky prázdný:
FROM golang:1.23 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 gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/server /server
ENTRYPOINT ["/server"]
CGO_ENABLED=0vytvoří staticky linkovanou binárku bez závislosti na libc.-ldflags="-s -w"odstraní ladicí symboly.- Distroless image neobsahuje shell ani balíčkovací systém, ale má CA certifikáty, časová pásma a uživatele
nonroot.
Ještě menší je FROM scratch – úplně prázdný image. Pak si ale musíte CA certifikáty pro HTTPS zkopírovat sami a nemáte žádného uživatele.
Příklad 4: Python s kompilovanými závislostmi
Některé balíčky (např. psycopg2 ze zdrojů) potřebují při instalaci kompilátor. Ve finálním image ho nechcete:
FROM python:3.12-slim AS build
RUN apt-get update && apt-get install -y --no-install-recommends build-essential libpq-dev \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt
FROM python:3.12-slim
RUN apt-get update && apt-get install -y --no-install-recommends libpq5 \
&& rm -rf /var/lib/apt/lists/*
RUN useradd --create-home app
WORKDIR /app
COPY --from=build /wheels /wheels
RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels
COPY --chown=app:app . .
USER app
CMD ["gunicorn", "-b", "0.0.0.0:8000", "app:app"]
Ve finální fázi je jen runtime knihovna libpq5, ne vývojové hlavičky a GCC.
Užitečné triky
Build jen jedné fáze – třeba pro spuštění testů v CI:
docker build --target build -t app:test .
docker run --rm app:test npm test
Kopírování z cizího image:
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
Cache závislostí mezi buildy (BuildKit, výchozí v aktuálních verzích Dockeru):
RUN --mount=type=cache,target=/root/.npm npm ci
Co ještě zmenšuje image
- Base image
-slimnebo-alpinemísto plné varianty. --no-install-recommendsuapt-get.pip install --no-cache-dir.- Pořádný
.dockerignore(viz Dockerfile best practices). - Smazání dočasných souborů ve stejném
RUN, kde vznikly.
Nevýhoda minimálních image
V distroless nebo scratch image nemáte shell, takže docker exec -it app sh nefunguje. Pro ladění existují varianty :debug u distroless image, případně docker debug v novějších verzích Docker Desktop. Pro většinu webových aplikací je -slim + multi-stage rozumný kompromis mezi velikostí a pohodlím.