Les pubs vous déplaisent ? Aller Sans pub Auj.

Docker Multi-Stage Builds Shrink Your Image Without Breaking the Deploy

Mis à jour le

Une présentation pratique des builds Docker à plusieurs étapes — les tailles réelles des images avant et après, des exemples de fichiers Docker pour Node.js et Python, et les pièges qui font tomber chaque équipe au moins une fois.

Docker Multi-Stage Builds: Shrink Your Image Without Breaking the Deploy 1
ANNONCE · Supprimer ?

Your Node.js image is 1.1 GB. You’ve added .dockerignore, pruned dev dependencies, tried node:slim — it barely moved. The actual fix is multi-stage builds. If you haven’t switched yet, you’re shipping your TypeScript compiler to production.

Multi-stage builds have been in Docker since version 17.05 (2017). They’re just underused. Here’s a real walkthrough: what changes, how big the difference is, and the three pitfalls that bite teams on the first migration.

The single-stage problem

Most Dockerfiles start like this:

FROM node:20

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

EXPOSE 3000
CMD ["node", "dist/server.js"]

Build that and check with docker images: ~1.1 GB. You’re shipping the full Node 20 image with npm, your TypeScript toolchain, every dev dependency, and your complete source tree. None of that runs in production — the app just needs the compiled dist/ output and a handful of runtime packages.

Multi-stage builds: the fix

Every FROM instruction starts a new stage with a clean filesystem. Name stages with AS, then use COPY --from=stagename to pull specific files into the next stage. Intermediate stages don’t make it into the final image — they’re build artifacts, discarded after the COPY is done.

Here’s the same app as a proper multi-stage build:

# ---- Build stage ----
FROM node:20 AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build


# ---- Runtime stage ----
FROM node:20-alpine AS runtime

WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist

EXPOSE 3000
CMD ["node", "dist/server.js"]

The critical line: COPY --from=builder /app/dist ./dist. That pulls seulement the compiled output from the builder stage into the Alpine-based runtime image. The TypeScript compiler, source files, and dev dependencies never touch the final layer.

Result: ~160 MB instead of 1.1 GB. That’s roughly an 85% reduction for a typical Node app — and it’s the same built artifact, just without the scaffolding around it.

Adding a test stage

You can add a test stage between build and runtime that runs your test suite. If tests fail, the build stops before the runtime image is created. If tests pass, you skip the test stage entirely when building for production.

FROM node:20 AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build


FROM builder AS tester

RUN npm test


FROM node:20-alpine AS runtime

WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist

EXPOSE 3000
CMD ["node", "dist/server.js"]

In CI you target the tester stage explicitly: docker build --target tester .. For production images you build without a target and Docker runs all stages in order, stopping at the last FROM. The test stage runs but its filesystem is discarded — tests act as a gate, not as payload.

Python: same idea, slightly different execution

Python multi-stage builds follow the same pattern. The main difference: pip installs packages under /root/.local when you use --user, so you copy that directory into the slim runtime image.

FROM python:3.12 AS builder

WORKDIR /app
COPY requirements.txt .
RUN pip install --user -r requirements.txt
COPY . .


FROM python:3.12-slim AS runtime

WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY --from=builder /app .

ENV PATH=/root/.local/bin:$PATH

CMD ["python", "main.py"]

python:3.12 base image: ~1 GB. python:3.12-slim with only installed dependencies: ~180–250 MB depending on what’s in requirements.txt. The compiled .pyc files come along for free since they live next to the source.

Three pitfalls that catch everyone at least once

1. Copying the wrong files

The most common mistake: you COPY --from=builder /app ./ au lieu de COPY --from=builder /app/dist ./dist. You just copied everything — source files, test fixtures, node_modules, the works — into your “minimal” runtime image. It’s now bigger than the single-stage version.

Be explicit about what you’re copying. Copy only the directory or files that your production entry point actually needs. For most Node apps: the compiled output (dist/) and optionally any static assets. For Python: the installed packages and the application code, not requirements.txt, not tests, not notebooks.

2. Build secrets leaking in layers

If you pass secrets as build arguments (e.g., ARG NPM_TOKEN followed by using it in a RUN command), that secret is visible in every layer that follows — even in a multi-stage build. docker history myimage will show it.

The correct approach is Docker BuildKit’s --mount=type=secret:

RUN --mount=type=secret,id=npm_token     NPM_TOKEN=$(cat /run/secrets/npm_token) npm ci

The secret is mounted at runtime of that layer only — it never gets committed to the image history. Build with: docker build --secret id=npm_token,src=.npmrc .

The cheap workaround people use — delete the secret in the same RUN layer — doesn’t actually help with multi-stage builds, but the BuildKit approach is cleaner regardless.

3. Forgetting .dockerignore

Multi-stage builds shrink your final image, but COPY . . in the build stage still sends your entire context to the Docker daemon. Without a .dockerignore, that includes .git/, node_modules/, test fixtures, local .env files, and any secrets you’re storing in plaintext files. The build stage sees all of it.

Minimum .dockerignore for any Node project:

.git
node_modules
dist
.env
*.log
coverage
.nyc_output

Ajouter .dockerignore the same day you add the Dockerfile. The build context size shows up in the first line of docker build output (Sending build context to Docker daemon X MB) — if that number is suspiciously large, check what’s being included.

Useful tools for Dockerfile work

If you want a starting point before writing your own, the Générateur de Dockerfile on IO Tools will scaffold a multi-stage Dockerfile for common stacks. Once you have something written, run it through the Linter et formateur Dockerfile to catch common mistakes before they hit CI — things like missing WORKDIR, using latest tags, or running as root unnecessarily.

The takeaway

Multi-stage builds are a two-step change: add a named build stage, copy the compiled output into a fresh minimal image. The size reduction is almost always worth it — 80–90% is typical for Node and Python apps. The main gotchas are being too broad with COPY --from, leaking secrets as build args, and skipping .dockerignore. Fix those and you’ve got a production image that’s actually sized for production.

Envie d'une expérience sans pub ? Passez à la version sans pub

Installez nos extensions

Ajoutez des outils IO à votre navigateur préféré pour un accès instantané et une recherche plus rapide

Sur Extension Chrome Sur Extension de bord Sur Extension Firefox Sur Extension de l'opéra

Le Tableau de Bord Est Arrivé !

Tableau de Bord est une façon amusante de suivre vos jeux, toutes les données sont stockées dans votre navigateur. D'autres fonctionnalités arrivent bientôt !

ANNONCE · Supprimer ?
ANNONCE · Supprimer ?
ANNONCE · Supprimer ?

Coin des nouvelles avec points forts techniques

Impliquez-vous

Aidez-nous à continuer à fournir des outils gratuits et précieux

Offre-moi un café
ANNONCE · Supprimer ?