Guide

Docker j'y suis allé à reculons et j'ai eu tort

Dockerfile optimisé, multi-stage builds, BuildKit secrets, Compose et sécurité - le guide Docker pragmatique pour les DevOps.

Sommaire

Pendant longtemps, j'ai regardé Docker avec la méfiance qu'on réserve aux technologies qu'on sent venir comme une vague et qu'on sait devoir adopter tôt ou tard - mais qu'on préfère éviter tant que possible parce qu'on a déjà assez de choses à apprendre et que ça marche très bien comme ça, merci.

Mon argument principal : les VMs font la même chose, les serveurs configurés à la main ont fait leurs preuves, et cette histoire d'"immuabilité" et de "reproductibilité" c'est du baratin de consultants qui facturent à la journée.

J'avais tort. Pas complètement - certaines de mes critiques étaient valides - mais suffisamment tort pour avoir perdu du temps à résister à un outil qui réglait des problèmes que j'avais bel et bien.

Ce guide est écrit du point de vue de quelqu'un qui a résisté, cédé, et compris pourquoi il aurait dû céder plus tôt. Pas du point de vue de quelqu'un qui vous vend Docker comme la solution à tous vos problèmes - parce que ce n'est pas le cas non plus.

Un peu d'histoire - pourquoi Docker est apparu

En 2008, les LXC (Linux Containers) existaient déjà. L'idée d'isoler des processus dans des namespaces Linux et des cgroups était là. Ce qui manquait : une interface utilisable par des humains.

Solomon Hykes a démarré Docker en tant que projet interne chez dotCloud, une société PaaS. En mars 2013, il a soumis un lightning talk à PyCon. Il s'attendait à dix ou trente personnes dans une arrière-salle pour parler de conteneurs Linux, le sujet le moins sexy d'une conférence Python. Les lightning talks de PyCon se font sur la scène principale. Il y avait 800 personnes.

Après le talk, la vidéo est devenue virale. Quelqu'un a posté leur site non terminé sur Hacker News en le qualifiant de "vaporware" puisqu'il n'était pas encore open source. Ils ont décidé de sortir Docker en deux semaines, en sprint.

Ce qui rendait Docker révolutionnaire en 2013 n'était pas la technologie sous-jacente - les namespaces Linux existaient depuis 2002, les cgroups depuis 2006. C'était l'expérience utilisateur. Docker rendait les conteneurs accessibles à des développeurs qui n'avaient jamais entendu parler de LXC. docker run hello-world et vous aviez un conteneur qui tournait. Sans lire 200 pages de documentation.

La croissance a été spectaculaire. En 2014, Google, Microsoft et Amazon annonçaient le support de Docker. En 2015, la Cloud Native Computing Foundation (CNCF) était créée avec Kubernetes comme projet fondateur. En 2017, Kubernetes s'imposait comme l'orchestrateur de facto.

Puis les choses se sont compliquées. Docker Inc. a eu du mal à monétiser. La spécification OCI (Open Container Initiative) a standardisé le format des images - elles peuvent maintenant tourner sur n'importe quel runtime compatible, pas seulement Docker. Podman, containerd, et d'autres alternatives ont émergé.

Aujourd'hui Docker le produit et Docker le concept sont deux choses distinctes. Docker le concept - les conteneurs OCI, le format d'image, le workflow de build - a gagné et est devenu le standard de l'industrie. Ce guide parle de Docker le concept, avec Docker le produit comme implémentation de référence.

Ce que Docker résout vraiment

"Ça marche sur ma machine"

Vous avez tous vécu ça. Le dev livre son code, ça tourne chez lui, ça ne tourne pas en prod. Pourquoi ? Python 3.9 en dev, Python 3.7 en prod. Une lib système installée localement mais pas sur le serveur. Une variable d'environnement oubliée. Un fichier de config qui traîne dans le home directory.

Docker règle ça en embarquant tout dans l'image - le code, les dépendances, les libs système, la version du runtime. L'image qui tourne en dev est la même qui tourne en prod. Pas "similaire". La même.

Les serveurs qui accumulent de la dette

Vous avez un serveur qui tourne depuis 3 ans. Il a Python 2.7 pour une vieille app, Python 3.6 pour une moins vieille, et vous avez besoin de Python 3.11 pour la nouvelle. Sans Docker : virtualenv, pyenv, des conflits de libs système, et la crainte permanente de casser quelque chose en mettant à jour.

Avec Docker : chaque app dans son conteneur, chaque conteneur avec sa version de Python. Le serveur n'accumule pas de dette. Il fait tourner des conteneurs.

Ce que Docker ne règle pas

Docker ne règle pas les problèmes d'architecture. Un mauvais service reste mauvais dans un conteneur. Docker ne règle pas la sécurité - un conteneur mal configuré est aussi dangereux qu'un processus mal configuré. Et Docker n'est pas une solution d'orchestration - c'est là que Kubernetes ou Docker Swarm entrent en jeu.

Les concepts - une fois pour toutes

Image - un template immuable. Elle contient le système de fichiers, les dépendances, le code, et les métadonnées. Une image est construite une fois, distribuée, et peut instancier autant de conteneurs que vous voulez. Elle ne change jamais - si vous modifiez quelque chose, vous construisez une nouvelle image.

Conteneur - une instance d'une image en cours d'exécution. Chaque conteneur a son propre filesystem en écriture au-dessus des layers read-only de l'image. Quand le conteneur s'arrête, ce layer est perdu sauf si vous utilisez des volumes.

Layer - les images sont construites en couches. Chaque instruction du Dockerfile crée un layer. Les layers sont mis en cache et réutilisés si rien n'a changé. C'est pour ça que l'ordre des instructions dans le Dockerfile compte.

Registry - le stockage des images. Docker Hub est le registry public par défaut. Une image est identifiée par registry/organisation/nom:tag.

Volume - le mécanisme de persistance. Les données écrites dans le filesystem d'un conteneur disparaissent avec lui. Les volumes persistent indépendamment du cycle de vie des conteneurs.

Network - comment les conteneurs communiquent entre eux et avec l'extérieur. Par défaut, Docker crée un réseau bridge. Les conteneurs sur le même réseau custom se trouvent par leur nom de conteneur.

Le Dockerfile - écrire des images qui ne font pas honte

La structure de base

FROM node:20-slim

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

EXPOSE 3000

USER node

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

L'ordre des instructions - le cache est votre ami

Docker cache chaque layer. Si un layer n'a pas changé depuis le dernier build, il est réutilisé. Si un layer change, tous les layers suivants sont reconstruits.

La règle : mettez ce qui change le moins souvent en premier, ce qui change le plus souvent en dernier.

# Mauvais ordre - le cache est invalidé à chaque changement de code
FROM node:20-slim
WORKDIR /app
COPY . .                    # copie tout - invalide le cache à chaque commit
RUN npm ci                  # réinstalle tout à chaque commit
CMD ["node", "src/index.js"]

# Bon ordre - les dépendances sont cachées séparément du code
FROM node:20-slim
WORKDIR /app
COPY package*.json ./       # change rarement
RUN npm ci                  # exécuté seulement si package.json change
COPY . .                    # change souvent - mais les layers précédents sont cachés
CMD ["node", "src/index.js"]

Sur une app avec beaucoup de dépendances, la différence entre un build de 2 minutes et un build de 5 secondes.

RUN en une seule instruction

Chaque RUN crée un layer. Les données supprimées dans un RUN ultérieur restent dans les layers précédents - elles occupent de l'espace dans l'image finale même si elles ne sont plus visibles.

# Mauvais - 3 layers, le cache apt reste dans l'image
RUN apt-get update
RUN apt-get install -y curl wget
RUN rm -rf /var/lib/apt/lists/*

# Bon - 1 layer, le cache apt est nettoyé dans le même layer
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl wget && \
    rm -rf /var/lib/apt/lists/*

COPY vs ADD

# ADD peut extraire des archives et fetcher des URLs
ADD archive.tar.gz /app/

# COPY fait une seule chose - copier des fichiers locaux
COPY fichier /app/
COPY dossier/ /app/dossier/

Utilisez COPY par défaut. ADD uniquement pour l'extraction d'archive - c'est sa seule utilité qui vaille vraiment. Le fetch d'URL dans ADD est à éviter - utilisez curl dans un RUN avec nettoyage.

L'utilisateur non-root

Par défaut, les processus dans un conteneur tournent en root. Root dans un conteneur n'est pas root sur l'hôte - les namespaces protègent partiellement - mais c'est une mauvaise pratique.

FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN chown -R node:node /app
USER node
EXPOSE 3000
CMD ["node", "src/index.js"]

Pour les images sans utilisateur intégré :

FROM python:3.11-slim
RUN groupadd -r appuser && useradd -r -g appuser appuser
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN chown -R appuser:appuser /app
USER appuser
CMD ["python", "app.py"]

ARG vs ENV

# ARG - disponible seulement au build
ARG VERSION=1.0.0

# ENV - disponible au build ET au runtime
ENV NODE_ENV=production
ENV PORT=3000

# Pattern classique
ARG VERSION
ENV APP_VERSION=$VERSION

ARG pour les valeurs de build. ENV pour les variables d'environnement du runtime. Ne mettez jamais de secrets dans ARG ou ENV - ils sont visibles dans les métadonnées de l'image avec docker inspect et docker history.

Le .dockerignore

Sans .dockerignore, COPY . . copie tout votre répertoire dans le contexte de build - node_modules, .git, les fichiers de test, les logs. Le contexte est envoyé au daemon Docker avant le build.

# .dockerignore
node_modules/
.git/
*.log
*.md
.env
.env.*
tests/
coverage/
.DS_Store
Dockerfile
docker-compose*.yml

Multi-stage builds - la vraie révolution

Compiler une app nécessite des outils qui ne sont pas nécessaires à l'exécution. Sans multi-stage builds, vous incluez tout ça dans votre image finale.

# Sans multi-stage - image de ~800MB
FROM golang:1.22
WORKDIR /app
COPY . .
RUN go build -o server .
CMD ["./server"]

# Avec multi-stage - image de ~10MB
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server .

FROM scratch
COPY --from=builder /app/server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
CMD ["/server"]

FROM scratch : l'image vide. Zéro OS, zéro shell, zéro surface d'attaque. Possible pour Go avec CGO_ENABLED=0 parce que le binaire est statiquement lié.

Node.js

FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-slim AS production
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY --from=builder /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]

Python

FROM python:3.11 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

FROM python:3.11-slim AS production
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
USER 1000
CMD ["python", "app.py"]

Java

FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests

FROM eclipse-temurin:21-jre-jammy
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
EXPOSE 8080
USER 1000
CMD ["java", "-jar", "app.jar"]

BuildKit - le builder moderne

BuildKit est activé par défaut depuis Docker 23.0. Sur les versions antérieures :

export DOCKER_BUILDKIT=1
docker build .

Les secrets au build

# Sans BuildKit - le token est dans les métadonnées de l'image
ARG NPM_TOKEN
RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc && \
    npm ci && \
    rm ~/.npmrc
# Visible avec docker history

# Avec BuildKit - le secret est monté temporairement, jamais dans les layers
# syntax=docker/dockerfile:1
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN --mount=type=secret,id=npm_token \
    NPM_TOKEN=$(cat /run/secrets/npm_token) \
    echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc && \
    npm ci && \
    rm ~/.npmrc
docker build --secret id=npm_token,src=$HOME/.npmrc .

Cache mounts

# syntax=docker/dockerfile:1

FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci

FROM debian:bookworm-slim
RUN --mount=type=cache,target=/var/cache/apt \
    --mount=type=cache,target=/var/lib/apt \
    apt-get update && apt-get install -y curl wget

Build multi-plateforme

docker buildx create --name multiarch --use
docker buildx build --platform linux/amd64,linux/arm64 \
  -t registry/image:latest --push .

Indispensable si vous déployez sur des machines ARM (AWS Graviton) et développez sur Apple Silicon.

Les images de base - choisir intelligemment

ImageTailleCompatibilitéSécuritéQuand l'utiliser
ubuntu~70MBExcellenteMoyenneDebug, outils complexes
debian:bookworm-slim~75MBTrès bonneBonneCas général
alpine~5MBBonne mais piégéeBonneBinaires statiques, outils simples
distroless~2MBLimitéeExcellenteGo, Java avec JRE
scratch0MBAucuneMaximaleBinaires statiques uniquement

Alpine - le piège

Alpine utilise musl libc au lieu de glibc. La plupart des binaires Linux sont compilés pour glibc. Résultat : certaines dépendances ne fonctionnent pas sur Alpine, ou fonctionnent avec des comportements subtils différents.

# Peut casser avec Alpine
FROM python:3.11-alpine
RUN pip install numpy  # peut échouer ou nécessiter 20 minutes de compilation

# Plus sûr
FROM python:3.11-slim
RUN pip install numpy  # fonctionne directement

Utilisez Alpine pour les images Go avec binaires statiques ou les outils système simples. Évitez-le pour Python avec des extensions C ou n'importe quoi qui dépend de glibc.

Distroless

Pas de shell, pas d'utilitaires, pas de gestionnaire de paquets. Surface d'attaque minimale.

FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o server .

FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /server
CMD ["/server"]

Pas de shell signifie pas de docker exec ... /bin/sh pour débugger. En prod c'est une feature. En dev c'est contraignant.

Healthchecks - ne pas déployer des conteneurs zombies

Un conteneur qui tourne n'est pas forcément un conteneur qui fonctionne. Sans healthcheck, Docker considère le conteneur comme sain tant que le processus tourne.

HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1
  • --interval : fréquence des vérifications (défaut : 30s)
  • --timeout : temps max pour la vérification (défaut : 30s)
  • --start-period : délai avant les premières vérifications - le temps que l'app démarre (défaut : 0s)
  • --retries : nombre d'échecs consécutifs avant de marquer unhealthy (défaut : 3)

L'endpoint /health

app.get('/health', async (req, res) => {
  try {
    await db.query('SELECT 1')
    await redis.ping()
    res.json({ status: 'ok' })
  } catch (err) {
    res.status(503).json({ status: 'error', error: err.message })
  }
})

Un healthcheck qui retourne toujours 200 sans vérifier les dépendances est inutile. C'est l'équivalent d'un test qui passe toujours - rassurant mais mensonger.

Networking - comprendre ce qui se passe

Les types de réseaux

# Bridge (défaut) - réseau privé isolé
docker network create mon-reseau
docker run --network mon-reseau mon-image

# Host - pas d'isolation réseau
docker run --network host mon-image

# None - pas de réseau
docker run --network none mon-image

La confusion localhost

Depuis un conteneur, localhost désigne le conteneur lui-même, pas l'hôte.

# macOS et Windows - host.docker.internal est disponible nativement
# Linux - nécessite --add-host depuis Docker 20.10
docker run --add-host host.docker.internal:host-gateway mon-image

# Mettre le service dans un conteneur sur le même réseau (recommandé)
docker network create app
docker run --network app --name postgres postgres:16
docker run --network app --env DB_HOST=postgres mon-app

DNS interne

Sur un réseau custom Docker, les conteneurs se trouvent par leur nom :

docker network create app
docker run -d --network app --name postgres postgres:16
docker run -d --network app --name redis redis:7
docker run -d --network app \
  --env DB_HOST=postgres \
  --env REDIS_HOST=redis \
  mon-app

mon-app accède à postgres:5432 et redis:6379 par leur nom. Pas d'IP à hardcoder.

Ports exposés vs publiés

# EXPOSE dans le Dockerfile - documentation uniquement, ne publie rien
# EXPOSE 3000

# Publier un port - accessible depuis l'hôte
docker run -p 3000:3000 mon-image             # toutes les interfaces
docker run -p 127.0.0.1:3000:3000 mon-image  # localhost seulement
docker run -p 3000 mon-image                  # port aléatoire sur l'hôte

Volumes - la persistance des données

Bind mounts vs volumes nommés

# Bind mount - un chemin de l'hôte monté dans le conteneur
docker run -v /host/path:/container/path mon-image
docker run -v $(pwd):/app mon-image

# Volume nommé - géré par Docker, identifiable
docker volume create mes-donnees
docker run -v mes-donnees:/data mon-image

Bind mounts : développement local (sync du code), accès aux fichiers de config de l'hôte. Dépend du chemin de la machine - non portable.

Volumes nommés : données persistantes en prod (bases de données, uploads). Gérés par Docker - portables, backupables.

Les permissions

# Le conteneur tourne en UID 1000
# Le volume est créé par root - permission denied sans ça

RUN mkdir -p /data && chown -R 1000:1000 /data
VOLUME /data

Backups

# Backup
docker run --rm \
  -v mes-donnees:/data \
  -v $(pwd):/backup \
  alpine tar czf /backup/backup.tar.gz /data

# Restaurer
docker run --rm \
  -v mes-donnees:/data \
  -v $(pwd):/backup \
  alpine tar xzf /backup/backup.tar.gz -C /

Docker Compose - dev local et au-delà

Un Compose file bien écrit

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
      target: production
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: production
      DB_HOST: postgres
      REDIS_HOST: redis
    env_file:
      - .env
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped
    networks:
      - app-network

  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped
    networks:
      - app-network

  redis:
    image: redis:7-alpine
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3
    restart: unless-stopped
    networks:
      - app-network

volumes:
  postgres-data:
  redis-data:

networks:
  app-network:
    driver: bridge

depends_on avec healthchecks - la vraie dépendance

# Sans condition - démarre après le conteneur, pas après qu'il soit prêt
depends_on:
  - postgres

# Avec condition - attend que le healthcheck passe
depends_on:
  postgres:
    condition: service_healthy

Sans condition: service_healthy, votre app démarre pendant que PostgreSQL initialise encore. Résultat : erreur de connexion au démarrage, crash, restart, ça finit par marcher - mais avec des logs d'erreur qui polluent votre monitoring et vous font perdre du temps.

Les overrides - plusieurs environnements

# docker-compose.yml - base commune
services:
  app:
    build: .
# docker-compose.override.yml - dev (chargé automatiquement)
services:
  app:
    volumes:
      - .:/app
      - /app/node_modules
    ports:
      - "3000:3000"
    environment:
      DEBUG: "app:*"
# docker-compose.prod.yml - prod (chargé explicitement)
services:
  app:
    image: registry/app:${VERSION}
    restart: unless-stopped
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
docker compose up                                                    # dev
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d  # prod

Compose en prod - la question honnête

Compose en prod est tentant. La réalité : pour des setups simples sur un seul serveur, avec des contraintes de budget ou de complexité, ça fait le job. Ce n'est pas Kubernetes.

Ce que Compose ne fait pas en prod : haute disponibilité, scaling horizontal, rolling deploy propre, gestion des secrets sérieuse.

Un seul serveur, une app sans besoin de HA : Compose. Besoin de scaler, de HA, ou de déploiements complexes : Kubernetes ou une PaaS.

Optimisation - images légères et builds rapides

Analyser la taille

# Taille des layers
docker image history mon-image

# Analyse détaillée et interactive
docker run --rm -it \
  -v /var/run/docker.sock:/var/run/docker.sock \
  wagoodman/dive mon-image

Nettoyer après chaque RUN

RUN pip install --no-cache-dir -r requirements.txt
RUN npm ci && npm cache clean --force
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl && \
    rm -rf /var/lib/apt/lists/*

Cache en CI/CD

# GitHub Actions
- name: Build and push
  uses: docker/build-push-action@v5
  with:
    context: .
    push: true
    tags: registry/app:${{ github.sha }}
    cache-from: type=registry,ref=registry/app:cache
    cache-to: type=registry,ref=registry/app:cache,mode=max

Sécurité - le minimum vital

Scanner les images

docker run --rm \
  -v /var/run/docker.sock:/var/run/docker.sock \
  aquasec/trivy image mon-image

# En CI - échec si vulnérabilité HIGH ou CRITICAL
trivy image --exit-code 1 --severity HIGH,CRITICAL mon-image

Pas de secrets dans les layers

# JAMAIS
ENV DATABASE_PASSWORD=supersecret
ARG API_KEY=mykey

Visible avec docker history et docker inspect. Pour le build : BuildKit secrets. Pour le runtime : variables d'environnement injectées, gestionnaire de secrets (Vault, AWS Secrets Manager).

Read-only filesystem

docker run --read-only \
  --tmpfs /tmp \
  --tmpfs /run \
  mon-image

Capabilities Linux

docker run \
  --cap-drop ALL \
  --cap-add NET_BIND_SERVICE \
  mon-image

Les pièges classiques

latest en prod

# Ce qu'il ne faut pas faire
docker run mon-image:latest
# latest change à chaque nouveau build
# rollback impossible sans tags explicites

# Ce qu'il faut faire
docker run mon-image:1.2.3
docker run mon-image:${GIT_SHA}      # le meilleur
docker run mon-image:2026-05-13-01

Le PID 1 et les signaux

Dans un conteneur, votre processus est PID 1. PID 1 a la responsabilité de gérer les signaux et reaper les processus zombies. La plupart des applications ne sont pas écrites pour ça.

# Problème : node ne gère pas bien SIGTERM en PID 1
CMD ["node", "server.js"]

# Solution : tini comme init léger
RUN apt-get install -y tini
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["node", "server.js"]
# Ou au runtime
docker run --init mon-image

Sans init, docker stop envoie SIGTERM à PID 1. Si votre app ne le gère pas, Docker attend 10 secondes puis envoie SIGKILL. Vos requêtes en cours sont coupées brutalement.

Les logs dans les conteneurs

# Mauvais - logs dans un fichier dans le conteneur
# Ils disparaissent au restart, le conteneur grossit

# Bon - stdout/stderr
# Docker capture tout ce qui va sur stdout/stderr
docker logs mon-conteneur
docker logs -f mon-conteneur
docker logs --since 1h mon-conteneur
docker logs --tail 100 mon-conteneur

# Configurer la rotation
docker run \
  --log-driver json-file \
  --log-opt max-size=10m \
  --log-opt max-file=3 \
  mon-image

lazydocker - parce que taper docker ps 40 fois par jour c'est non

Je suis fan de lazydocker depuis longtemps - j'en parlais déjà dans la newsletter de mars 2023 en le qualifiant d'"UI génialissime". Je n'ai pas changé d'avis.

lazydocker est une interface terminal (TUI) pour Docker et Docker Compose. Vous voyez tous vos conteneurs, leurs logs, leur consommation de ressources, et vous pouvez les démarrer, arrêter, redémarrer, exec, inspecter - tout ça sans retaper les commandes. Le genre d'outil qui, une fois installé, rend la ligne de commande nue insupportable.

# Installation
brew install jesseduffield/lazydocker/lazydocker  # macOS
go install github.com/jesseduffield/lazydocker@latest  # Go

# Lancement
lazydocker

C'est écrit en Go, ça démarre instantanément, et ça fonctionne aussi bien avec Docker qu'avec Docker Compose. Quand vous avez un incident en prod et que votre boss est debout derrière vous, c'est exactement ce qu'il vous faut pour diagnostiquer en un coup d'œil sans vous souvenir de quelle option de docker inspect affiche les variables d'environnement.

À retenir

PratiquePourquoi
Multi-stage buildsImages 10x à 100x plus légères
Ordre des instructionsBuilds rapides grâce au cache
.dockerignoreContexte de build léger
Utilisateur non-rootSécurité de base
HealthchecksDétecter les conteneurs non fonctionnels
BuildKit secretsSecrets sans leak dans les layers
Tags explicites en prodRollback possible, traçabilité
Volumes pour les donnéesPersistance entre restarts
Réseaux customIsolation et DNS entre conteneurs
stdout/stderr pour les logsIntégration avec docker logs

FAQ

Docker vs Podman - lequel choisir ?

Podman est sans daemon, rootless par défaut, compatible OCI. Si vous êtes sur RHEL/Fedora, Podman est le choix naturel. Sur Debian/Ubuntu ou macOS, Docker reste plus simple à démarrer. Les deux produisent des images OCI compatibles - choisir l'un n'est pas un engagement à vie.

Docker Compose v1 vs v2 ?

V2 est le défaut depuis Docker Desktop 3.4. La commande passe de docker-compose à docker compose. V1 est déprécié. Migrez.

Quand Docker Compose en prod vs Kubernetes ?

Compose pour un seul serveur, une app simple, budget limité, équipe petite. Kubernetes pour la haute disponibilité, le scaling, plusieurs serveurs. La complexité de Kubernetes a un coût réel qu'il faut justifier.

Comment débugger un conteneur qui crashe immédiatement ?
docker logs mon-conteneur
docker run -it --entrypoint /bin/sh mon-image
docker inspect mon-conteneur
CMD vs ENTRYPOINT ?

ENTRYPOINT définit le process principal - difficile à surcharger. CMD définit les arguments par défaut - facilement surchargés au runtime. Pattern classique : ENTRYPOINT pour le binaire, CMD pour les arguments par défaut.

Pour aller plus loin