← Retour au blog

Docker en production : 10 erreurs qui m'ont coûté des nuits blanches

Best practices Docker pour la production. Sécurité, optimisation des images, orchestration et monitoring.

Pourquoi cet article

3h du matin, pager qui sonne : “Production down”. Container OOMKilled, image de 2.8GB qui ne build plus, secrets en clair dans les logs.

6 ans d’expérience Docker résumés en 10 erreurs à ne jamais reproduire.

Erreur #1 : Images énormes (2GB+)

Problème

Dockerfile naïf

FROM node:20

WORKDIR /app

COPY . .
RUN npm install
RUN npm run build

CMD ["npm", "start"]

Résultat : image de 1.8GB

Problèmes

Solution : multi-stage build

# Stage 1 : Build
FROM node:20-alpine AS builder

WORKDIR /app

# Dependencies en premier (cache Docker)
COPY package*.json ./
RUN npm ci --only=production

COPY . .
RUN npm run build

# Stage 2 : Production
FROM node:20-alpine

WORKDIR /app

# Copier uniquement les fichiers nécessaires
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./

# User non-root
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nextjs -u 1001
USER nextjs

EXPOSE 3000

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

Résultat : 180MB (−90%)

Optimisations supplémentaires

# .dockerignore (comme .gitignore)
node_modules
.git
.env
*.md
tests/
.github/
coverage/

Gain total : 1.8GB → 85MB après compression

Erreur #2 : Secrets dans l’image

❌ JAMAIS faire ça

# Secrets hardcodés
ENV DATABASE_URL="postgresql://user:password123@db.prod.com/mydb"

# Ou pire
COPY .env .

Problème : docker history expose tout

$ docker history myapp:latest
...
ENV DATABASE_URL=postgresql://user:password123@...

✅ Solutions

1. BuildKit secrets (build-time)

# syntax=docker/dockerfile:1

FROM node:20-alpine

RUN --mount=type=secret,id=npm_token \
    echo "//registry.npmjs.org/:_authToken=$(cat /run/secrets/npm_token)" > .npmrc && \
    npm install && \
    rm .npmrc
docker build --secret id=npm_token,src=.npm_token -t myapp .

2. Environment variables (runtime)

docker run -e DATABASE_URL="$DATABASE_URL" myapp

3. Docker secrets (Swarm/K8s)

# docker-compose.yml
services:
  app:
    image: myapp
    secrets:
      - db_password

secrets:
  db_password:
    external: true
echo "mypassword" | docker secret create db_password -

Erreur #3 : Tout en root

Problème

FROM node:20

# Runs as root par défaut
CMD ["node", "server.js"]

Risque : si l’app est compromise, attaquant = root dans le container

Solution

FROM node:20-alpine

WORKDIR /app

# Créer user non-root
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nextjs -u 1001

COPY --chown=nextjs:nodejs . .

USER nextjs

CMD ["node", "server.js"]

Vérifier

docker exec mycontainer whoami
# nextjs (pas root)

Erreur #4 : Pas de health check

Problème

Container “running” mais app crashée

$ docker ps
CONTAINER ID   STATUS
abc123         Up 2 hours   # Mais l'app ne répond plus !

Solution

Dockerfile

HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
  CMD node healthcheck.js || exit 1

healthcheck.js

const http = require('http');

const options = {"{"}
  host: 'localhost',
  port: 3000,
  path: '/health',
  timeout: 2000
{"}"};

const req = http.request(options, (res) => {"{"}
  process.exit(res.statusCode === 200 ? 0 : 1);
{"}"});

req.on('error', () => process.exit(1));
req.end();

Docker Compose

services:
  app:
    healthcheck:
      test: ["CMD", "node", "healthcheck.js"]
      interval: 30s
      timeout: 3s
      retries: 3
      start_period: 40s

Résultat

$ docker ps
CONTAINER ID   STATUS
abc123         Up 2 hours (healthy)

Erreur #5 : Un seul process par layer

❌ Anti-pattern

RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
RUN apt-get install -y vim
# 4 layers inutiles

✅ Pattern

RUN apt-get update && apt-get install -y \
    curl \
    git \
    vim \
  && rm -rf /var/lib/apt/lists/*
# 1 seul layer + cleanup

Aussi important : ordre des COPY

# ❌ Mauvais : rebuild à chaque changement
COPY . .
RUN npm install

# ✅ Bon : cache des dependencies
COPY package*.json ./
RUN npm ci
COPY . .

Erreur #6 : Logs perdus

Problème

Logs écrits dans des fichiers → perdus quand container restart

Solution : STDOUT/STDERR

❌ Mauvais

const fs = require('fs');
const logStream = fs.createWriteStream('/var/log/app.log');
logger.add(new transports.File({"{"} filename: '/var/log/app.log' {"}"}));

✅ Bon

// Logs sur stdout (Docker capture automatiquement)
console.log('Server started');
console.error('Error occurred');

// Ou Winston sur stdout
logger.add(new transports.Console());

Centraliser avec un log driver

services:
  app:
    logging:
      driver: "fluentd"
      options:
        fluentd-address: "localhost:24224"
        tag: "docker.app"

Ou envoyer à un service

services:
  app:
    logging:
      driver: "awslogs"
      options:
        awslogs-region: "eu-west-1"
        awslogs-group: "/ecs/myapp"

Erreur #7 : Limites de ressources non configurées

Problème

Container bouffe toute la RAM du host

$ docker stats
CONTAINER   CPU %   MEM USAGE / LIMIT
myapp       45%     7.8GB / 8GB    # Oops

Solution

docker run

docker run \
  --memory="512m" \
  --memory-swap="512m" \
  --cpus="0.5" \
  --pids-limit=100 \
  myapp

docker-compose.yml

services:
  app:
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M

Kubernetes

resources:
  limits:
    cpu: "500m"
    memory: "512Mi"
  requests:
    cpu: "250m"
    memory: "256Mi"

Erreur #8 : Rebuild complet à chaque fois

Problème

Petit changement → 15 minutes de rebuild

Solution : layer caching stratégique

# Dependencies changent rarement
COPY package*.json ./
RUN npm ci

# Source code change souvent
COPY src/ ./src/

# Config change parfois
COPY config/ ./config/

BuildKit cache mounts

# syntax=docker/dockerfile:1

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

Registry cache

docker build \
  --cache-from myregistry.com/myapp:latest \
  --tag myregistry.com/myapp:new \
  .

Erreur #9 : Ignorer la sécurité

Scans de vulnérabilités

# Trivy (gratuit, excellent)
trivy image myapp:latest

# Ou
docker scan myapp:latest

CI/CD

# .github/workflows/docker.yml
- name: Run Trivy vulnerability scanner
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: 'myapp:${"$"}{"{"}"{"{"} github.sha {"}"}"}"}
    format: 'sarif'
    severity: 'CRITICAL,HIGH'

Base images officielles minimales

# ❌ Image complète (vulnérabilités)
FROM ubuntu:latest

# ✅ Image minimale
FROM node:20-alpine

# ✅✅ Distroless (encore mieux)
FROM gcr.io/distroless/nodejs20-debian11

Read-only filesystem

# docker-compose.yml
services:
  app:
    read_only: true
    tmpfs:
      - /tmp
      - /app/.next/cache

Erreur #10 : Pas de monitoring

Metrics essentielles

Prometheus + cAdvisor

# docker-compose.yml
services:
  cadvisor:
    image: gcr.io/cadvisor/cadvisor:latest
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
    ports:
      - 8080:8080

  prometheus:
    image: prom/prometheus
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - 9090:9090

prometheus.yml

scrape_configs:
  - job_name: 'cadvisor'
    static_configs:
      - targets: ['cadvisor:8080']

Grafana dashboards

Docker Compose pour production

❌ Ne PAS utiliser docker-compose en prod

✅ Utiliser

Exemple Swarm

# docker-stack.yml
version: '3.8'

services:
  app:
    image: myregistry.com/myapp:latest
    deploy:
      replicas: 3
      update_config:
        parallelism: 1
        delay: 10s
        failure_action: rollback
      restart_policy:
        condition: on-failure
        max_attempts: 3
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
    networks:
      - app-network
    secrets:
      - db_password

networks:
  app-network:
    driver: overlay

secrets:
  db_password:
    external: true
docker stack deploy -c docker-stack.yml myapp

Checklist production

Images
[ ] Multi-stage build
[ ] Alpine ou Distroless
[ ] < 500MB
[ ] .dockerignore configuré
[ ] Scan de vulnérabilités

Sécurité
[ ] User non-root
[ ] Read-only filesystem si possible
[ ] Secrets via environment ou secrets
[ ] Base image à jour

Runtime
[ ] Health checks configurés
[ ] Resource limits
[ ] Logging vers stdout
[ ] Restart policy

Monitoring
[ ] Prometheus + Grafana
[ ] Alertes configurées
[ ] Logs centralisés (ELK, Datadog, etc.)

CI/CD
[ ] Build automatique
[ ] Tests dans container
[ ] Tag sémantique
[ ] Registry privé

Outils recommandés

Développement

Production

Monitoring


Questions sur Docker en production ? Contactez-moi pour un audit de votre infrastructure.