Servery a DevOps

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ů.

12. 10. 2026 · 4 min čtení

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=0 vytvoří 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 -slim nebo -alpine místo plné varianty.
  • --no-install-recommends u apt-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.