La nuit de l'ouverture. Le système doit tenir. Pas de panne, pas de fuite, pas d'erreur. En production, il n'y a pas de seconde chance. Les conteneurs doivent être solides, surveillés, prêts à tout.
Introduction : La Différence Entre Jouer et Travailler
En développement, un conteneur qui plante, tu le redémarres.
En production, c'est des milliers d'utilisateurs en colère.
Des pertes d'argent. Des réputations qui s'effondrent.
Ce module, c'est le passage à l'âge adulte.
Des jouets aux outils.
Du garage à l'usine.
5.1 Sécurité - Les Verrous de la Ville
Le Conteneur n'est pas une VM
Mythe : "Les conteneurs sont isolés comme des VMs."
Réalité : Ils partagent le noyau. Une faille dans le noyau = tous les conteneurs.
Règles de base :
- Jamais root dans le conteneur
- Images signées et vérifiées
- Réseaux séparés
- Limites de ressources strictes
Hardening des Conteneurs
Dockerfile sécurisé :
# Utiliser des images minimales
FROM alpine:3.18 # 5 Mo vs 200 Mo pour Ubuntu
# Utilisateur non-root
RUN addgroup -g 1000 -S appgroup && \
adduser -u 1000 -S appuser -G appgroup
USER appuser
# Copier avec les bonnes permissions
COPY --chown=appuser:appgroup app /app
# Nettoyer les permissions sensibles
RUN find / -perm /6000 -type f -exec chmod a-s {} \; 2>/dev/null || true
# Signature de l'image (avec Docker Content Trust)
# export DOCKER_CONTENT_TRUST=1
docker-compose.prod.yml :
services:
api:
security_opt:
- no-new-privileges:true # Empêche l'élévation de privilèges
- seccomp:unconfined # Ou un profil custom
cap_drop:
- ALL # Enlève toutes les capabilities
cap_add:
- NET_BIND_SERVICE # Ajoute seulement celle nécessaire
read_only: true # Système de fichiers en lecture seule
tmpfs:
- /tmp # Sauf /tmp en RAM
Gestion des Secrets - Les Codes dans le Coffre
Méthodes (du pire au meilleur) :
- À éviter absolument :
# Fichier .env commité (NON !)
# Variables dans le Dockerfile (NON !)
# Secrets dans docker-compose.yml (NON !)
- Acceptable pour débuter :
# .env non-commité + docker-compose
environment:
DB_PASSWORD: ${DB_PASSWORD}
# .env.production (GITIGNORE !)
DB_PASSWORD=SuperSecret123!
- Professionnel : Docker Secrets (Swarm)
version: '3.8'
services:
db:
image: postgres
secrets:
- db_password
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
db_password:
external: true # Créé avec docker secret create
- Cloud Native : Vault, AWS Secrets Manager, Azure Key Vault
# Container init qui récupère les secrets avant le démarrage
ENTRYPOINT ["./init.sh"]
# init.sh :
#!/bin/sh
# Récupère les secrets depuis Vault
# Les écrit dans des fichiers
# Passe la main à l'application
Scanner les Images - Le Détecteur de Bombes
# Trivy (gratuit, open-source)
trivy image mon_image:latest
# Output :
# ✗ HIGH: Vulnerable package found
# - CVE-2023-12345 in libssl
# - Fix version: 1.1.1k
# Dans le CI/CD
trivy image --exit-code 1 --severity HIGH,CRITICAL mon_image:latest
# Si vulnérabilité critique, le build échoue
# Docker Scout (intégré à Docker)
docker scout quickview mon_image:latest
docker scout cves mon_image:latest
# Snyk, Aqua Security, Clair...
5.2 Monitoring et Logging - Les Yeux et les Oreilles
Métriques à Surveiller
Au niveau conteneur :
- CPU usage (%)
- Memory usage (absolu et %)
- Network I/O
- Disk I/O
- Restart count
Au niveau application :
- Latence (p95, p99)
- Error rate
- Throughput (req/s)
- Business metrics (commandes/s, etc.)
Stack de Monitoring Prometheus/Grafana
docker-compose.monitoring.yml :
version: '3.8'
services:
# Collecteur de métriques
prometheus:
image: prom/prometheus:latest
container_name: prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.retention.time=30d'
- '--web.enable-lifecycle'
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
networks:
- monitoring
ports:
- "9090:9090"
restart: unless-stopped
# Visualisation
grafana:
image: grafana/grafana:latest
container_name: grafana
environment:
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:-ChangeMe!}
GF_INSTALL_PLUGINS: grafana-piechart-panel
volumes:
- grafana_data:/var/lib/grafana
- ./monitoring/grafana/dashboards:/etc/grafana/provisioning/dashboards:ro
- ./monitoring/grafana/datasources:/etc/grafana/provisioning/datasources:ro
networks:
- monitoring
ports:
- "3000:3000"
restart: unless-stopped
depends_on:
- prometheus
# Node Exporter (métriques système hôte)
node-exporter:
image: prom/node-exporter:latest
container_name: node-exporter
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- '--path.procfs=/host/proc'
- '--path.sysfs=/host/sys'
- '--path.rootfs=/rootfs'
networks:
- monitoring
ports:
- "9100:9100"
restart: unless-stopped
pid: "host" # Accès au PID namespace hôte
# cAdvisor (métriques conteneurs)
cadvisor:
image: gcr.io/cadvisor/cadvisor:latest
container_name: cadvisor
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
- /dev/disk/:/dev/disk:ro
devices:
- /dev/kmsg
networks:
- monitoring
ports:
- "8080:8080"
restart: unless-stopped
privileged: true # Nécessaire pour accéder aux métriques Docker
# Alert Manager
alertmanager:
image: prom/alertmanager:latest
container_name: alertmanager
volumes:
- ./monitoring/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
- alertmanager_data:/alertmanager
networks:
- monitoring
ports:
- "9093:9093"
restart: unless-stopped
# Loki (logs)
loki:
image: grafana/loki:latest
container_name: loki
command: -config.file=/etc/loki/local-config.yaml
volumes:
- loki_data:/loki
networks:
- monitoring
ports:
- "3100:3100"
restart: unless-stopped
# Promtail (collecte logs)
promtail:
image: grafana/promtail:latest
container_name: promtail
volumes:
- /var/log:/var/log:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- ./monitoring/promtail.yml:/etc/promtail/config.yml:ro
networks:
- monitoring
restart: unless-stopped
command: -config.file=/etc/promtail/config.yml
networks:
monitoring:
driver: bridge
volumes:
prometheus_data:
driver: local
grafana_data:
driver: local
alertmanager_data:
driver: local
loki_data:
driver: local
Configuration Prometheus :
# monitoring/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093
rule_files:
- "alerts.yml"
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'node'
static_configs:
- targets: ['node-exporter:9100']
- job_name: 'cadvisor'
static_configs:
- targets: ['cadvisor:8080']
metrics_path: /metrics
- job_name: 'docker'
static_configs:
- targets: ['cadvisor:8080']
metrics_path: /metrics/docker
- job_name: 'app'
static_configs:
- targets: ['api:3000']
metrics_path: /metrics
Alertes :
# monitoring/alerts.yml
groups:
- name: infrastructure
rules:
- alert: HighMemoryUsage
expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100 > 85
for: 5m
labels:
severity: warning
annotations:
summary: "High memory usage on {{ $labels.instance }}"
description: "Memory usage is at {{ $value }}%"
- alert: ContainerRestarted
expr: increases(container_restarts_total[5m]) > 0
labels:
severity: critical
annotations:
summary: "Container {{ $labels.name }} restarted"
- alert: HighCPUUsage
expr: rate(container_cpu_usage_seconds_total[5m]) * 100 > 80
for: 5m
labels:
severity: warning
Centralisation des Logs
Configuration Docker pour les logs :
// /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"labels": ["environment=production"]
}
Ou mieux, directement vers un agrégateur :
{
"log-driver": "loki",
"log-opts": {
"loki-url": "http://loki:3100/api/prom/push",
"loki-external-labels": "job=docker"
}
}
Dans docker-compose :
services:
api:
logging:
driver: "loki"
options:
loki-url: "http://loki:3100/api/prom/push"
loki-external-labels: "service=api"
5.3 Orchestration - Quand Docker Compose ne Suffit Plus
Les Limites de Docker Compose
Docker Compose, c'est pour :
- Une seule machine
- Développement
- Petits déploiements
Dès que vous avez besoin de :
- Haute disponibilité
- Scaling horizontal
- Rolling updates
- Multi-nœuds
- Auto-healing
... il faut un orchestrateur.
Docker Swarm vs Kubernetes
Docker Swarm : Le petit frère intégré.
# Initialiser le swarm
docker swarm init --advertise-addr <IP>
# Joindre un nœud
docker swarm join --token <TOKEN> <IP>:2377
# Déployer un stack
docker stack deploy -c docker-compose.prod.yml mon_app
# Voir les services
docker service ls
Avantages Swarm :
- Simple, intégré à Docker
- Courbe d'apprentissage douce
- Bon pour les petites/moyennes infrastructures
Inconvénients :
- Communauté plus petite
- Moins de fonctionnalités
- Pas le standard du marché
Kubernetes : Le géant.
# kubectl apply -f deployment.yml
# kubectl scale deployment api --replicas=5
# kubectl rolling-update...
Avantages Kubernetes :
- Standard de facto
- Énorme écosystème
- Très puissant
- Cloud-agnostic
Inconvénients :
- Complexe
- Nécessite une équipe dédiée (ou managed service)
- Overkill pour les petites apps
Exemple de Stack Swarm pour Production
docker-compose.swarm.yml :
version: '3.8'
services:
# Application avec réplicas
api:
image: registry.monentreprise.com/api:${TAG:-latest}
deploy:
mode: replicated
replicas: 3
update_config:
parallelism: 1
delay: 10s
order: start-first
rollback_config:
parallelism: 0
order: stop-first
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
window: 120s
placement:
constraints:
- node.role == worker
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
networks:
- backend
secrets:
- db_password
configs:
- source: app_config
target: /app/config.yaml
# Load balancer
traefik:
image: traefik:v2.10
deploy:
mode: global
placement:
constraints:
- node.role == manager
ports:
- "80:80"
- "443:443"
- "8080:8080" # Dashboard
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik:/etc/traefik
networks:
- backend
- frontend
command:
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.swarmmode=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
secrets:
db_password:
external: true
configs:
app_config:
file: ./config/prod.yaml
networks:
backend:
driver: overlay
attachable: true
frontend:
driver: overlay
attachable: true
5.4 CI/CD avec Docker - L'Usine à Conteneurs
Pipeline GitLab CI
.gitlab-ci.yml :
stages:
- test
- build
- security
- deploy
variables:
DOCKER_REGISTRY: registry.gitlab.com
IMAGE_NAME: ${CI_REGISTRY_IMAGE}
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
# Services nécessaires
services:
- docker:dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
# 1. Tests unitaires
unit-tests:
stage: test
image: node:18-alpine
script:
- cd backend
- npm ci
- npm test
artifacts:
reports:
junit: backend/junit.xml
paths:
- backend/coverage/
# 2. Build de l'image
build-image:
stage: build
image: docker:latest
script:
- |
docker build \
--build-arg VERSION=${CI_COMMIT_SHA} \
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
-t ${IMAGE_NAME}:${CI_COMMIT_SHA} \
-t ${IMAGE_NAME}:latest \
.
- docker push ${IMAGE_NAME}:${CI_COMMIT_SHA}
- docker push ${IMAGE_NAME}:latest
only:
- main
- develop
# 3. Scan de sécurité
security-scan:
stage: security
image: aquasec/trivy:latest
script:
- trivy image --exit-code 1 --severity HIGH,CRITICAL ${IMAGE_NAME}:${CI_COMMIT_SHA}
allow_failure: false
only:
- main
# 4. Tests d'intégration
integration-tests:
stage: test
image: docker:latest
services:
- docker:dind
script:
- docker-compose -f docker-compose.test.yml up -d
- sleep 10 # Attendre que les services démarrent
- docker-compose -f docker-compose.test.yml exec api npm run test:integration
- docker-compose -f docker-compose.test.yml down
only:
- main
- develop
# 5. Déploiement staging
deploy-staging:
stage: deploy
image: docker:latest
script:
- docker stack deploy -c docker-compose.staging.yml --with-registry-auth mon_app_staging
environment:
name: staging
url: https://staging.monapp.com
only:
- develop
# 6. Déploiement production
deploy-production:
stage: deploy
image: docker:latest
script:
- |
docker service update \
--image ${IMAGE_NAME}:${CI_COMMIT_SHA} \
mon_app_production_api
environment:
name: production
url: https://monapp.com
when: manual # Déclenchement manuel
only:
- main
GitHub Actions
.github/workflows/docker.yml :
name: Docker Build and Deploy
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to Container Registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Scan with Trivy
uses: aquasecurity/trivy-action@master
with:
image-ref: '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest'
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy scan results
uses: github/codeql-action/upload-sarif@v2
if: always()
with:
sarif_file: 'trivy-results.sarif'
5.5 Bonnes Pratiques Production - Le Code de Survie
1. Tagging Stratégique
# MAUVAIS
docker build -t monapp .
# BON
docker build \
-t registry.com/monapp:1.2.3 \
-t registry.com/monapp:1.2 \
-t registry.com/monapp:1 \
-t registry.com/monapp:latest \
.
# MEILLEUR (avec Git commit SHA)
docker build \
-t registry.com/monapp:$(git rev-parse --short HEAD) \
-t registry.com/monapp:latest \
.
2. Healthchecks Intelligents
# Dans le Dockerfile
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# Endpoint /health qui vérifie :
# - Connexion DB
# - Connexion Redis
# - Espace disque
# - Mémoire disponible
3. Graceful Shutdown
# Capturer SIGTERM
STOPSIGNAL SIGTERM
# Script d'arrêt
COPY docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["app"]
docker-entrypoint.sh :
#!/bin/sh
set -e
# Fonction de cleanup
cleanup() {
echo "Received SIGTERM, shutting down..."
# Fermer les connexions DB, sauvegarder l'état, etc.
kill -TERM "$PID" 2>/dev/null
wait "$PID"
exit 0
}
# Capturer les signaux
trap cleanup TERM INT
# Démarrer l'application en arrière-plan
exec "$@" &
PID=$!
# Attendre
wait $PID
4. Configuration Externalisée
# docker-compose.prod.yml
services:
api:
configs:
- source: app_config_${ENVIRONMENT}
target: /app/config.yaml
configs:
app_config_production:
external: true
app_config_staging:
external: true
5. Backup des Volumes
Script de backup :
#!/bin/bash
# backup-volumes.sh
BACKUP_DIR="/backups"
DATE=$(date +%Y%m%d_%H%M%S)
# Backup PostgreSQL
docker run --rm \
-v postgres_data:/volume \
-v $BACKUP_DIR:/backup \
alpine \
tar czf /backup/postgres_${DATE}.tar.gz -C /volume ./
# Backup Redis
docker run --rm \
-v redis_data:/volume \
-v $BACKUP_DIR:/backup \
alpine \
tar czf /backup/redis_${DATE}.tar.gz -C /volume ./
# Rotation (garder 7 jours)
find $BACKUP_DIR -name "*.tar.gz" -mtime +7 -delete
6. Mise à Jour sans Downtime
Blue-Green Deployment :
# Version A (current)
docker service update --image monapp:1.2.3 api-green
# Tester
curl https://api-green.monapp.com/health
# Basculer le routeur
docker service update \
--update-delay 10s \
--update-parallelism 1 \
--update-failure-action rollback \
--image monapp:1.2.3 api-blue
5.6 Checklist de Mise en Production
Avant le Déploiement
- Images scannées pour vulnérabilités
- Secrets gérés proprement (pas dans les images)
- Healthchecks configurés
- Logging centralisé configuré
- Monitoring en place
- Alertes configurées
- Backup des volumes testé
- Plan de rollback préparé
- Load testing effectué
- Documentation à jour
Après le Déploiement
- Vérifier les métriques (CPU, mémoire, erreurs)
- Vérifier les logs (pas d'erreurs au démarrage)
- Tester les endpoints critiques
- Vérifier les connexions aux services externes
- Confirmer que le monitoring collecte bien
- Documenter les incidents/actions
En Cas d'Incident
- Ne pas paniquer
- Consulter les logs centralisés
- Vérifier les métriques (saturation ?)
- Rollback si nécessaire
- Documenter l'incident
- Corriger et redéployer
Scène Finale : La Nuit de Garde
# 22h00 - Tout est calme
watch -n 5 'docker service ls && echo "---" && docker stats --no-stream'
# 23h30 - Pic de charge
# Les métriques montent
# AlertManager envoie une alerte Slack
# Investigation
docker service ps api --no-trunc # Voir où tournent les tâches
docker logs api.1.qwerty123 --tail 100 # Logs d'un réplica spécifique
docker exec api.1.qwerty123 top # Processus dans le conteneur
# Action
docker service scale api=8 # Scaling horizontal
# Ou
docker service update --limit-cpu 1.0 api # Donner plus de CPU
# 00h30 - Retour à la normale
# Les métriques redescendent
# Ticket d'incident créé
# Post-mortem planifié pour demain
Conclusion du Module
La production, c'est où les théories rencontrent la réalité.
Où les "ça devrait marcher" deviennent des "ça marche, ou ça casse".
Les règles de la production :
- Tout est surveillé - Si tu ne le mesures pas, tu ne peux pas le gérer.
- Tout est loggé - Les logs sont ton seul témoin quand tout plante.
- Tout est automatique - Les déploiements manuels sont des erreurs en puissance.
- Tout est reproductible - Un environnement, un script, un résultat.
- Tout est prévu - Plan de rollback, backup, alertes.
Docker en production, ce n'est pas la fin du voyage.
C'est le début de la vraie aventure.
La ville est en ligne.
Elle respire.
Elle sert des milliers d'utilisateurs.
Et toi, tu es son gardien.