Devops
Difficulte: Advanced
22 min de lecture

Docker Avancé : Multi-stage builds, Healthchecks et Sécurité

Guide avancé Docker pour la production : multi-stage builds pour des images 10x plus légères, healthchecks fiables, sécurité des images et du runtime, Docker Compose production-ready, et pipeline CI/CD multi-arch.

Retour aux tutoriels
Prérequis
Ce tutoriel suppose une connaissance de base de Docker (images, conteneurs, Dockerfile, docker compose). Si vous débutez, consultez d'abord le guide d'installation et configuration de Docker et le guide d'installation Docker.

Pourquoi Docker "basique" ne suffit pas en production

La grande majorité des équipes qui adoptent Docker commencent par un Dockerfile simple : une image officielle, quelques RUN, un CMD, et c'est en production. Ça marche. Jusqu'au jour où ça ne marche plus.

Les problèmes typiques des Dockerfiles naïfs en production sont toujours les mêmes. Une image de 2 Go qui met 8 minutes à builder en CI. Un conteneur qui tourne en root et qui expose des binaires système inutiles. Un service qui déclare être "up" alors qu'il ne répond plus aux requêtes. Des secrets passés en ARG visibles dans docker history. Un docker compose up qui démarre l'application avant que la base de données soit prête.

Ces problèmes ont des solutions éprouvées et documentées. Ce guide les couvre de manière exhaustive :

  • Multi-stage builds : images 5 à 100x plus légères selon le langage
  • Optimisation des layers : builds rapides grâce au cache
  • Healthchecks production-ready : ordonnancement fiable des services
  • Sécurité des images : utilisateur non-root, images minimales, scan de vulnérabilités
  • Sécurité du runtime : capabilities, seccomp, isolation réseau
  • Docker Compose production : override files, secrets, limites de ressources
  • Registry et CI/CD : multi-arch, signing, pipeline GitHub Actions

1. Multi-stage builds

Le principe des multi-stage builds est simple : utiliser plusieurs images successives dans un seul Dockerfile pour séparer la phase de compilation de la phase d'exécution. L'image finale ne contient que ce qui est strictement nécessaire pour faire tourner l'application.

Exemple complet : application Go

Go est l'exemple parfait pour illustrer les gains : le SDK Go pèse environ 850 Mo. Un binaire compilé en statique pèse quelques mégaoctets.

# syntax=docker/dockerfile:1

# ── Stage 1 : Build ────────────────────────────────────────────────────
FROM golang:1.22-alpine AS builder

WORKDIR /app

# Copier les fichiers de dépendances en premier pour le cache
COPY go.mod go.sum ./
RUN go mod download

# Copier le code source et compiler un binaire statique
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -ldflags="-s -w" -trimpath -o /app/server ./cmd/server

# ── Stage 2 : Runtime ──────────────────────────────────────────────────
FROM scratch

# Copier les certificats TLS (requis pour les appels HTTPS)
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

# Copier uniquement le binaire compilé
COPY --from=builder /app/server /server

EXPOSE 8080

ENTRYPOINT ["/server"]

Résultat : l'image finale pèse entre 8 et 15 Mo au lieu de 850 Mo. Les options -s -w de ldflags suppriment les symboles de debug et les informations DWARF. -trimpath retire les chemins locaux du binaire pour la reproductibilité.

Exemple complet : application Node.js

Pour Node.js, l'enjeu est de séparer les devDependencies (outils de build, TypeScript, etc.) des dependencies de runtime.

# syntax=docker/dockerfile:1

# ── Stage 1 : Installation de toutes les dépendances ──────────────────
FROM node:20-alpine AS deps

WORKDIR /app
COPY package*.json ./

# Installer TOUTES les dépendances (dev + prod) pour le build
RUN --mount=type=cache,target=/root/.npm \
    npm ci

# ── Stage 2 : Build TypeScript ─────────────────────────────────────────
FROM deps AS builder

COPY . .
RUN npm run build

# ── Stage 3 : Runtime minimal ──────────────────────────────────────────
FROM node:20-alpine AS runner

RUN addgroup -S nodejs && adduser -S nextjs -G nodejs

WORKDIR /app

# Copier uniquement les artefacts de build et les dépendances de production
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./

USER nextjs

EXPOSE 3000
ENV NODE_ENV=production

CMD ["node", "dist/index.js"]

Pattern avec cible spécifique --target

Un Dockerfile peut servir plusieurs environnements grâce aux targets. Chaque stage hérite du précédent et y ajoute sa couche spécifique.

# syntax=docker/dockerfile:1

FROM python:3.12-slim AS base
WORKDIR /app
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install --no-compile -r requirements.txt

# ── Target dev : outils de debug ───────────────────────────────────────
FROM base AS dev
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install debugpy pytest ipdb watchfiles
COPY . .
CMD ["python", "-m", "uvicorn", "main:app", "--reload", "--host", "0.0.0.0"]

# ── Target test : exécution des tests ──────────────────────────────────
FROM base AS test
COPY requirements-test.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements-test.txt
COPY . .
CMD ["pytest", "-v", "--cov=app", "--cov-report=xml"]

# ── Target prod : image minimale sécurisée ─────────────────────────────
FROM python:3.12-slim AS prod
COPY --from=base /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
WORKDIR /app
COPY . .
RUN groupadd -r appuser && useradd -r -g appuser -s /sbin/nologin appuser
USER appuser
EXPOSE 8000
CMD ["gunicorn", "main:app", "-w", "4", "-k", "uvicorn.workers.UvicornWorker", "-b", "0.0.0.0:8000"]
# Builder une target spécifique
docker buildx build --target dev -t monapp:dev .
docker buildx build --target test -t monapp:test .
docker buildx build --target prod -t monapp:prod .

# En CI/CD : builder uniquement la target prod
docker buildx build --target prod --push -t registry.example.com/monapp:1.2.3 .
Gains typiques par langage
Go : de 850 Mo à 8-15 Mo (binaire statique sur scratch). Node.js : de 1,1 Go à 150-250 Mo. Python : de 900 Mo à 180-250 Mo. Java : de 700 Mo à 80-120 Mo avec jlink. Les gains sont proportionnellement plus importants pour les langages compilés.

Contenu Premium

Ce tutoriel avancé est réservé aux membres premium.

9,90€ / mois
  • Tous les tutoriels avancés
  • Nouveaux contenus chaque semaine
  • Suivi de progression
  • Annulation à tout moment
Morgann Riu

Écrit par

Morgann Riu

Expert en cybersécurité et administration Linux. Je partage mes connaissances à travers des tutoriels gratuits et des formations pour aider les administrateurs systèmes et développeurs à sécuriser leurs infrastructures.

Partager ce tutoriel

Cet article vous a plu ?

Commentaires

Checklist Sécurité Linux

30 points essentiels pour sécuriser un serveur Linux. Recevez aussi les nouveaux tutoriels par email.

Pas de spam. Désabonnement en 1 clic.