← Retour aux tutoriels

Durcir les Dockerfiles : réduire la taille des images et les risques de sécurité sans casser les builds

dockerfiledockersecuritehardeningdevopsci-cdimages-dockeroptimisationmulti-stage-buildsupply-chain

Durcir les Dockerfiles : réduire la taille des images et les risques de sécurité sans casser les builds

Ce tutoriel explique comment réduire la taille des images Docker et diminuer la surface d’attaque (et donc les risques de sécurité) tout en préservant la reproductibilité et la stabilité des builds. L’objectif n’est pas de “faire petit à tout prix”, mais de construire des images prévisibles, minimales, scannables, et maintenables.


1) Pourquoi “durcir” un Dockerfile ?

Un Dockerfile “durci” vise généralement :

En pratique, on cherche le meilleur compromis entre :


2) Diagnostiquer l’existant : taille, couches, contenu

Avant d’optimiser, mesurez.

2.1 Taille d’image et historique des couches

docker images
docker history --no-trunc monimage:latest

docker history permet de repérer les couches qui gonflent : apt-get install, npm install, pip install, copies de gros artefacts, etc.

2.2 Explorer le contenu d’une image

Pour inspecter rapidement :

docker run --rm -it --entrypoint sh monimage:latest
# ou bash si présent

Puis :

du -h -d 1 / | sort -h
du -h -d 1 /usr | sort -h

2.3 Scanner les vulnérabilités (exemples)

Avec Docker Scout (si disponible) :

docker scout quickview monimage:latest
docker scout cves monimage:latest

Ou avec Trivy :

trivy image monimage:latest

Ces outils ne “réparent” pas, mais ils vous indiquent vous êtes exposé (OS packages, libs, etc.).


3) Choisir une base d’image adaptée (et la figer)

3.1 Distroless, Alpine, Debian slim : que choisir ?

Recommandation pragmatique :

3.2 Figer la base : tag + digest

Les tags bougent (:latest, :slim), ce qui nuit à la reproductibilité. Vous pouvez figer par digest :

FROM debian:bookworm-slim@sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Pour obtenir le digest :

docker pull debian:bookworm-slim
docker inspect --format='{{index .RepoDigests 0}}' debian:bookworm-slim

Avantage : build identique dans le temps (à dépendances identiques).
Inconvénient : il faut mettre à jour volontairement pour intégrer des patches de sécurité.


4) Réduire la taille : principes concrets qui marchent

4.1 Utiliser le multi-stage build

Le multi-stage permet de séparer :

Exemple Node.js (build + runtime) :

# syntax=docker/dockerfile:1.7
FROM node:20-bookworm AS build
WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

FROM node:20-bookworm-slim AS runtime
WORKDIR /app

ENV NODE_ENV=production
COPY --from=build /app/dist ./dist
COPY --from=build /app/package*.json ./

RUN npm ci --omit=dev && npm cache clean --force

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

Points importants :

4.2 Minimiser les fichiers copiés : .dockerignore

Un build lent et une image gonflée viennent souvent d’un contexte énorme (node_modules local, logs, build artifacts, .git).

Créez un .dockerignore :

.git
node_modules
dist
coverage
*.log
.env
Dockerfile
docker-compose.yml

Adaptez selon votre projet. L’objectif : ne pas envoyer au daemon Docker des milliers de fichiers inutiles.

4.3 Réduire le nombre de couches (sans perdre en lisibilité)

Chaque RUN crée une couche. Combinez les commandes liées, surtout pour apt.

Mauvais (cache et taille) :

RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

Mieux :

RUN apt-get update \
 && apt-get install -y --no-install-recommends curl \
 && rm -rf /var/lib/apt/lists/*

--no-install-recommends évite l’installation de paquets “recommandés” souvent inutiles.

4.4 Nettoyer les caches (au bon endroit)

Pour Debian/Ubuntu :

Pour Python/pip :

Pour npm :

Pour Go :


5) Durcissement sécurité : utilisateurs, permissions, capabilities

5.1 Ne pas exécuter en root

Par défaut, Docker exécute en root. Créez un utilisateur dédié.

Exemple Debian slim :

RUN useradd -r -u 10001 -g nogroup appuser
USER 10001

Ou si vous voulez un home :

RUN useradd -m -u 10001 appuser
USER appuser

Ensuite, assurez-vous que les fichiers copiés appartiennent au bon user :

COPY --chown=10001:0 ./dist /app/dist

5.2 Répertoires en écriture : limiter

Beaucoup d’applications n’ont besoin d’écrire que dans /tmp ou un dossier précis.

5.3 Réduire les outils disponibles en runtime

Plus votre image contient d’outils (shell, curl, package manager), plus un attaquant a de “jouets” en cas d’exécution de code.

5.4 Read-only filesystem et options d’exécution (côté runtime)

Même avec un Dockerfile propre, vous pouvez durcir au lancement :

docker run --rm \
  --read-only \
  --tmpfs /tmp:rw,noexec,nosuid,size=64m \
  --cap-drop ALL \
  --security-opt no-new-privileges \
  -p 3000:3000 \
  monimage:latest

Notes :

Attention : certaines apps ont besoin d’écrire (logs, caches). Ajustez avec des volumes ou tmpfs.


6) Gestion des secrets : ne jamais les “baker” dans l’image

6.1 Anti-patterns fréquents

Tout ce qui est dans une layer peut être extrait via docker save / inspection.

6.2 Utiliser BuildKit secrets (build-time)

Avec BuildKit, vous pouvez monter un secret sans l’écrire dans l’image.

Exemple : token npm pendant npm ci.

Dockerfile :

# syntax=docker/dockerfile:1.7
FROM node:20-bookworm AS build
WORKDIR /app

COPY package*.json ./

RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
    npm ci

COPY . .
RUN npm run build

Build :

DOCKER_BUILDKIT=1 docker build \
  --secret id=npmrc,src=$HOME/.npmrc \
  -t monimage:build .

Le fichier .npmrc n’est pas conservé dans l’image finale.

6.3 Secrets runtime

Pour l’exécution, passez plutôt via :


7) Reproductibilité : versions figées, lockfiles, dépôts stables

7.1 OS packages : éviter l’aléatoire

Sur Debian/Ubuntu, apt-get install dépend de l’état des dépôts au moment du build. Pour des besoins forts de reproductibilité, vous pouvez :

Dans tous les cas, gardez une trace des versions :

dpkg -l | head

7.2 Langages : utilisez les lockfiles

Exemple Python (avec venv dans builder) :

# syntax=docker/dockerfile:1.7
FROM python:3.12-slim AS build
WORKDIR /app

RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

FROM python:3.12-slim AS runtime
WORKDIR /app

COPY --from=build /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

COPY . .

RUN useradd -r -u 10001 -g nogroup appuser
USER 10001

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

Ici, on évite d’installer build-essential si vos wheels sont disponibles. Si vous compilez des dépendances natives, installez les toolchains uniquement dans build.


8) Build rapide et stable : cache BuildKit, ordre des instructions

8.1 L’ordre des COPY est crucial

Le cache Docker se base sur les instructions. Si vous faites :

COPY . .
RUN npm ci

Alors le moindre changement de code invalide le cache de npm ci, ce qui est coûteux.

Préférez :

COPY package*.json ./
RUN npm ci
COPY . .

Ainsi, tant que les manifestes ne changent pas, l’installation reste cachée.

8.2 Cache BuildKit pour les gestionnaires de paquets

Exemple pour apt :

RUN --mount=type=cache,target=/var/cache/apt \
    apt-get update && apt-get install -y --no-install-recommends curl \
    && rm -rf /var/lib/apt/lists/*

Exemple pour pip :

RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

Exemple pour npm :

RUN --mount=type=cache,target=/root/.npm \
    npm ci

Ces caches accélèrent les builds répétés sans gonfler l’image finale (le cache est côté build).


9) Santé et robustesse : HEALTHCHECK, signaux, PID 1

9.1 HEALTHCHECK (optionnel mais utile)

HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
  CMD wget -qO- http://127.0.0.1:3000/health || exit 1

Attention : ajouter wget/curl juste pour healthcheck peut augmenter la surface d’attaque. Alternatives :

9.2 PID 1 et gestion des signaux

Certaines apps gèrent mal SIGTERM si elles sont PID 1. Pour des runtimes comme Node, Python, etc., considérez tini (init minimal) :

Debian :

RUN apt-get update && apt-get install -y --no-install-recommends tini \
 && rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["tini", "--"]
CMD ["node", "dist/server.js"]

Mais encore une fois : installer tini ajoute un paquet. Dans certains environnements, l’orchestrateur gère déjà bien ce point, ou vous pouvez utiliser une image qui inclut déjà un init minimal.


10) Exemples complets : avant / après (cas réalistes)

10.1 Exemple “avant” (Node) : gros et risqué

FROM node:20

WORKDIR /app
COPY . .
RUN npm install
RUN npm run build

EXPOSE 3000
CMD ["npm", "start"]

Problèmes :

10.2 Exemple “après” : multi-stage, non-root, cache-friendly

# syntax=docker/dockerfile:1.7
FROM node:20-bookworm AS build
WORKDIR /app

COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci

COPY . .
RUN npm run build

FROM node:20-bookworm-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production

COPY --from=build /app/dist ./dist
COPY --from=build /app/package.json /app/package-lock.json ./

RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev \
 && npm cache clean --force

# Utilisateur non-root présent dans l'image node officielle
USER node

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

Gains typiques :


11) Distroless : durcissement maximal (avec contraintes)

Distroless supprime shell et package manager. Exemple pour une app Go statique.

11.1 Dockerfile Go statique + distroless

# syntax=docker/dockerfile:1.7
FROM golang:1.22-bookworm AS build
WORKDIR /src

COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod go mod download

COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build \
    CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" -o /out/app ./cmd/app

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/app /app
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/app"]

Points clés :


12) Bonnes pratiques supplémentaires (souvent négligées)

12.1 Labels OCI (traçabilité)

Ajoutez des labels utiles :

LABEL org.opencontainers.image.source="https://example.com/monrepo" \
      org.opencontainers.image.revision="$VCS_REF" \
      org.opencontainers.image.created="$BUILD_DATE"

Build :

docker build \
  --build-arg VCS_REF="$(git rev-parse HEAD)" \
  --build-arg BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
  -t monimage:latest .

Et dans le Dockerfile :

ARG VCS_REF
ARG BUILD_DATE
LABEL org.opencontainers.image.revision=$VCS_REF \
      org.opencontainers.image.created=$BUILD_DATE

12.2 COPY vs ADD

12.3 Éviter les “curl | sh” non vérifiés

Anti-pattern :

RUN curl -sSL https://example.com/install.sh | sh

Préférez :

12.4 Tests dans le pipeline

Un Dockerfile durci peut casser des hypothèses (écriture disque, user non-root). Ajoutez des tests :

docker build -t monimage:test .
docker run --rm -p 3000:3000 monimage:test

Testez aussi en mode durci :

docker run --rm --read-only --cap-drop ALL --security-opt no-new-privileges monimage:test

Si ça casse, corrigez l’app (répertoire writable explicite, gestion de /tmp, etc.) plutôt que de revenir à root.


13) Checklist “durcissement sans casser les builds”

Utilisez cette checklist comme garde-fou :

  1. Base
    • Image de base adaptée (slim/distroless si possible)
    • Tag stable, idéalement digest
  2. Contexte
    • .dockerignore strict
  3. Cache et reproductibilité
    • COPY des manifests avant le code
    • lockfiles et commandes déterministes (npm ci, etc.)
    • BuildKit cache mounts si pertinent
  4. Réduction de surface
    • Multi-stage (pas de toolchain en runtime)
    • --no-install-recommends et nettoyage apt
    • pas d’outils “debug” en prod
  5. Privilèges
    • USER non-root
    • permissions minimales (dossiers writable explicitement)
  6. Secrets
    • aucun secret dans le Dockerfile ou le contexte
    • BuildKit secrets pour accès privé au build
  7. Runtime
    • options --read-only, --cap-drop ALL, no-new-privileges testées
    • healthcheck/probes cohérents (sans gonfler l’image inutilement)

14) Conclusion : une approche progressive

Durcir un Dockerfile est un processus itératif :

  1. Mesurez (taille, couches, CVE).
  2. Corrigez le plus rentable : .dockerignore, ordre des COPY, multi-stage.
  3. Réduisez la surface : base slim, suppression des outils inutiles.
  4. Sécurisez l’exécution : non-root, FS read-only, capabilities minimales.
  5. Renforcez la reproductibilité : digests, lockfiles, secrets BuildKit.

Si vous me donnez un Dockerfile réel (et le langage/projet), je peux proposer une version “durcie” avec des compromis explicites (compatibilité vs minimalisme) et des commandes de test pour vérifier que le build et le runtime ne cassent pas.