DevOps

Docker
Conteneurs

Images, conteneurs, Dockerfile, volumes, réseaux, Docker Compose et intégration CI/CD — du concept à la production.

Pourquoi Docker ?

Le problème que Docker résout
# Scénario classique SANS Docker :

Dev Alice :
  Python 3.11, PostgreSQL 15, Redis 7
  "Ça marche sur ma machine !"
  → requirements.txt, virtualenv, config manuelle

Collègue Bob :
  Python 3.9, PostgreSQL 14, pas de Redis
  → "Erreur ImportError: module 'asyncpg' …"
  → 2h pour reproduire l'environnement

Serveur de prod :
  Ubuntu 20.04, Python 3.8 système
  → "Version incompatible, dépendances manquantes"
  → Nuit blanche de déploiement

# AVEC Docker :

Alice définit l'environnement dans un Dockerfile :
  FROM python:3.11-slim
  COPY requirements.txt .
  RUN pip install -r requirements.txt
  ...

Bob :
  docker compose up
  → Environnement IDENTIQUE à celui d'Alice ✓
  → Fonctionne en 30 secondes

Serveur de prod :
  docker pull monapp:v1.2.3
  docker run monapp:v1.2.3
  → Même image qu'en dev → même comportement ✓
🖥️ VM🐳 Conteneur Docker
IsolationOS complet virtualiséProcessus isolé (namespaces Linux)
TaillePlusieurs GoQuelques Mo à centaines de Mo
DémarrageMinutesSecondes (parfois ms)
DensitéQuelques VMs / hôteDizaines à centaines / hôte
PortabilitéImage lourde (OVA, VMDK)Image légère, pousse sur registry
NoyauNoyau dédié par VMPartagé avec l'hôte
SécuritéIsolation forteBonne — surface d'attaque partagée
💡

Analogie : une VM = une maison entière (fondations, murs, toit). Un conteneur = un appartement dans un immeuble partagé (les infrastructures communes sont partagées — noyau — mais chaque appartement est isolé).

Architecture & composants

🖥️ Machine hôte (Linux / macOS / Windows)
🐳 Docker Daemon (dockerd)  ←  Docker CLI / Docker Desktop / API REST
📦 Images locales  |  🌐 docker.io · ghcr.io · registry privé
🟢 api (running)
⬆ Couche R/W (conteneur)
app layer
deps layer
python:3.11-slim
🟢 db (running)
⬆ Couche R/W (conteneur)
init scripts
postgres:16-alpine
⚪ worker (stopped)
⬆ Couche R/W
worker layer
python:3.11-slim
ComposantRôle
Docker EngineLe moteur : daemon (dockerd) + CLI (docker) + API REST
ImageTemplate immuable en couches. Blueprint d'un conteneur.
ConteneurInstance en cours d'exécution d'une image. Processus isolé.
RegistryDépôt d'images. Docker Hub, GHCR, ECR, registry privé.
VolumeStockage persistant géré par Docker, survit au conteneur.
NetworkRéseau virtuel isolé. Les conteneurs s'y connectent.
DockerfileRecette textuelle pour construire une image.
ComposeOrchestration multi-conteneurs avec un fichier YAML.
Cycle de vie d'un conteneur
# États d'un conteneur :
created  → existe mais n'a pas démarré
running  → processus actif
paused   → suspendu (SIGSTOP)
stopped  → processus terminé, conteneur existe encore
removed  → supprimé définitivement

# Transitions :
docker create  → created
docker start   → running
docker pause   → paused
docker unpause → running
docker stop    → stopped  (SIGTERM puis SIGKILL)
docker kill    → stopped  (SIGKILL immédiat)
docker restart → stopped → running
docker rm      → removed

# Raccourci : docker run = create + start
docker run nginx
# Équivalent à :
docker create nginx   # → id
docker start <id>

Images & système de couches (layers)

Chaque instruction dans un Dockerfile crée une couche immuable. Docker ne reconstruit que les couches modifiées.

FROM python:3.11-slim 128 MB
WORKDIR /app 0 B
COPY requirements.txt . ✓ cached
RUN pip install -r requirements.txt ✓ cached
COPY . . modifié → rebuild
EXPOSE 8000 0 B
CMD ["uvicorn", "app.main:app"] 0 B
⬆ Couche R/W (conteneur uniquement) runtime
ℹ️

Partage de couches : si 10 conteneurs utilisent la même image de base python:3.11-slim, la couche de 128 MB n'est stockée qu'une seule fois sur le disque. Chaque conteneur n'a que sa propre couche R/W.

Pourquoi l'ordre des instructions compte
# ✗ Mauvais ordre — cache invalidé à chaque modif du code
FROM python:3.11-slim
WORKDIR /app
COPY . .                    # ← code source (change souvent)
RUN pip install flask       # ← reconstruit à CHAQUE fois !
CMD ["python", "app.py"]

# ✅ Bon ordre — dépendances cachées séparément
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .    # ← change rarement
RUN pip install -r requirements.txt  # ← CACHED si inchangé
COPY . .                   # ← code source (ici, après pip)
CMD ["python", "app.py"]

# Règle : du plus stable au plus volatile
# BASE → CONFIG → DÉPENDANCES → CODE → CMD

# Voir les couches d'une image
docker history mon-app:latest
docker inspect mon-app:latest | jq '.[0].RootFS.Layers'

# Analyser la taille par couche
docker history --no-trunc --format \
  "{{.CreatedBy}}\t{{.Size}}" mon-app:latest

Instructions Dockerfile

InstructionUsageExemple
FROMImage de base — obligatoire en 1erFROM python:3.11-slim
WORKDIRRépertoire de travail (créé si absent)WORKDIR /app
COPYCopier fichiers hôte → imageCOPY src/ /app/src/
ADDComme COPY + décompresse archives, URLsADD archive.tar.gz /app/
RUNExécuter commande pendant le buildRUN apt-get update && apt-get install -y curl
ENVVariable d'env persistante dans l'imageENV APP_ENV=production
ARGVariable de build (disparaît après build)ARG VERSION=1.0
EXPOSEDocumenter le port (ne l'ouvre PAS)EXPOSE 8000
VOLUMEDéclarer un point de montageVOLUME ["/data"]
USERUtilisateur pour les instructions suivantesUSER appuser
CMDCommande par défaut (overridable)CMD ["uvicorn", "app.main:app"]
ENTRYPOINTPoint d'entrée fixe (non overridable)ENTRYPOINT ["python"]
LABELMétadonnées de l'imageLABEL version="1.0"
HEALTHCHECKVérifier la santé du conteneurHEALTHCHECK CMD curl -f http://localhost/
CMD vs ENTRYPOINT
# CMD — commande par défaut, overridable
FROM python:3.11-slim
CMD ["python", "app.py"]

docker run mon-image           # → python app.py
docker run mon-image bash      # → bash (override CMD)

# ENTRYPOINT — exécutable fixe, CMD = arguments
FROM python:3.11-slim
ENTRYPOINT ["python"]
CMD ["app.py"]

docker run mon-image            # → python app.py
docker run mon-image script.py  # → python script.py
# python reste, seul l'argument change

# Combinaison courante pour les outils CLI :
FROM alpine
ENTRYPOINT ["curl"]
CMD ["--help"]

docker run mycurl https://api.exemple.com
# → curl https://api.exemple.com

# ARG vs ENV
ARG VERSION=1.0        # seulement au build
ENV APP_PORT=8000      # dans l'image et le conteneur

# Utiliser ARG dans RUN :
ARG NODE_ENV=production
RUN echo "Building for $NODE_ENV"

# ⚠ ARG ne doit pas contenir de secrets
# (visible dans docker history)

Bonnes pratiques Dockerfile

Images légères & propres
# ✅ Choisir la bonne image de base
FROM python:3.11         # Debian complet — 1 GB
FROM python:3.11-slim    # Debian minimal  — 130 MB ✅
FROM python:3.11-alpine  # Alpine Linux    — 50 MB
# Alpine : très léger mais peut causer des incompatibilités
# (musl libc vs glibc) — tester avant d'adopter

# ✅ Combiner les RUN pour réduire les couches
# ✗ Mauvais (3 couches = 3× la taille du cache apt)
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

# ✅ Bon (1 seule couche, nettoyage inclus)
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
       curl \
       git \
    && rm -rf /var/lib/apt/lists/*

# ✅ pip — pas de cache dans l'image
RUN pip install --no-cache-dir -r requirements.txt

# ✅ npm — pas de devDependencies en prod
RUN npm ci --only=production
RUN npm cache clean --force
Sécurité & utilisateur non-root
# ✅ Ne jamais tourner en root
FROM python:3.11-slim
WORKDIR /app

# Créer un utilisateur sans shell et sans mot de passe
RUN addgroup --system appgroup \
    && adduser --system --ingroup appgroup \
       --no-create-home appuser

# Installer les dépendances (encore root)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copier le code et changer le propriétaire
COPY --chown=appuser:appgroup . .

# Passer à l'utilisateur non-root
USER appuser

EXPOSE 8000
CMD ["uvicorn", "app.main:app", \
     "--host", "0.0.0.0", "--port", "8000"]

# ✅ Utiliser un tag précis (jamais :latest en prod)
FROM python:3.11.9-slim-bookworm  # exact ✓
# FROM python:latest ← version inconnue demain ✗

# ✅ HEALTHCHECK
HEALTHCHECK --interval=30s --timeout=5s \
            --start-period=10s --retries=3 \
  CMD curl -f http://localhost:8000/health || exit 1

Multi-stage build

Le multi-stage build permet d'utiliser plusieurs étapes dans un seul Dockerfile. L'image finale ne contient que ce qui est nécessaire à l'exécution — sans les outils de build.

Python — build vs production
# ── Stage 1 : builder ─────────────────────
# Contient les outils de compilation
FROM python:3.11 AS builder

WORKDIR /app
COPY requirements.txt .

# Installer dans un répertoire dédié
RUN pip install --user --no-cache-dir \
    -r requirements.txt

# ── Stage 2 : production ──────────────────
# Image finale légère sans les outils de build
FROM python:3.11-slim AS production

WORKDIR /app

# Copier UNIQUEMENT les packages installés
COPY --from=builder /root/.local /root/.local

# Copier le code source
COPY . .

# S'assurer que les scripts pip sont dans le PATH
ENV PATH=/root/.local/bin:$PATH

RUN adduser --disabled-password appuser
USER appuser

EXPOSE 8000
CMD ["uvicorn", "app.main:app", \
     "--host", "0.0.0.0", "--port", "8000"]

# Résultat : image finale ~130MB au lieu de ~1GB !
Node.js — build frontend + serveur
# Stage 1 : installer les dépendances
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json .
RUN npm ci

# Stage 2 : build de l'application
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build        # vite build / next build

# Stage 3 : image de production minimale
FROM nginx:alpine AS production
# Copier le build statique dans nginx
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

# Stage 4 optionnel : tests (ne va pas en prod)
FROM deps AS test
COPY . .
RUN npm run test
RUN npm run lint

# Cibler un stage spécifique :
docker build --target production -t mon-app .
docker build --target test -t mon-app:test .

.dockerignore

.dockerignore — fichier type
# Contrôle de version
.git
.gitignore
.github/

# Cache Python
__pycache__/
*.py[cod]
*$py.class
*.pyc
.pytest_cache/
.mypy_cache/
.coverage
htmlcov/
.tox/

# Environnements virtuels
.venv/
venv/
env/
ENV/

# Node.js
node_modules/
npm-debug.log
.npm/
dist/
build/

# Variables d'environnement — NE JAMAIS inclure !
.env
.env.*
*.env
secrets/
*.key
*.pem
*.p12

# Documentation et config IDE
docs/
*.md
README*
.vscode/
.idea/
*.DS_Store

# Docker lui-même
Dockerfile*
docker-compose*.yml
.dockerignore

# Tests et CI
tests/
.github/
*.test.py
*.spec.js
💡

Impact sur les performances : Docker envoie le "build context" entier au daemon avant de construire l'image. Sans .dockerignore, node_modules/ ou .git/ (souvent plusieurs centaines de Mo) sont envoyés inutilement à chaque build — ce qui peut prendre des dizaines de secondes.

⚠️

Sécurité absolue : ne jamais copier .env, des clés API, des certificats ou des tokens dans une image Docker. Ces fichiers resteraient visibles dans les couches de l'image même après suppression — docker history peut les révéler. Injecter les secrets à l'exécution via -e ou Docker secrets.

Vérifier le build context
# Voir la taille du contexte envoyé au daemon
docker build . 2>&1 | head -3
# Sending build context to Docker daemon  2.048kB ✅
# Sending build context to Docker daemon  456.9MB ✗

# Lister ce qui serait inclus (sans construire)
docker build --dry-run .  # Docker Desktop

Gestion des images

Build & tag
# Construire une image depuis le répertoire courant
docker build -t mon-app:latest .
docker build -t mon-app:1.3.2 .
docker build -t mon-app:latest -f Dockerfile.prod .

# Options build utiles
docker build \
  --no-cache \                    # forcer la reconstruction
  --build-arg ENV=production \    # passer des ARG
  --target production \           # multi-stage : étape cible
  --platform linux/amd64 \        # architecture cible
  -t mon-app:prod \
  .

# Tagger une image existante
docker tag mon-app:latest monlogin/mon-app:latest
docker tag mon-app:latest ghcr.io/owner/mon-app:v1.2.3

# Lister les images
docker images
docker image ls
docker images --filter "dangling=true"  # images orphelines (<none>)
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"

# Supprimer
docker rmi mon-app:latest
docker image prune          # supprimer les <none>
docker image prune -a       # supprimer toutes les non utilisées
docker system prune         # conteneurs + images + réseaux non utilisés
docker system prune -a --volumes # tout nettoyer (attention !)
Registry — pull & push
# Se connecter à un registry
docker login                       # Docker Hub
docker login ghcr.io               # GitHub Container Registry
docker login registry.monentreprise.com

# Récupérer une image
docker pull python:3.11-slim       # depuis Docker Hub
docker pull ghcr.io/owner/app:v1   # depuis GHCR
docker pull nginx:1.25-alpine

# Pousser une image
docker push monlogin/mon-app:latest
docker push ghcr.io/owner/mon-app:v1.2.3

# Sauvegarder / charger une image (sans registry)
docker save mon-app:latest | gzip > mon-app.tar.gz
docker load < mon-app.tar.gz

# Inspecter une image
docker inspect mon-app:latest
docker history mon-app:latest
docker manifest inspect python:3.11-slim  # multi-arch

# Format de nommage complet :
# [registry/][owner/]nom[:tag][@digest]
docker.io/library/python:3.11-slim
ghcr.io/owner/mon-app:sha-a3f2c1b
registry.k8s.io/pause:3.9

Gestion des conteneurs

docker run — toutes les options utiles
# Forme de base
docker run IMAGE [COMMANDE] [ARGS]

# Options essentielles
docker run \
  -d \                          # détaché (background)
  --name mon-api \              # nom du conteneur
  -p 8080:8000 \                # port hôte:conteneur
  -e DATABASE_URL=postgres://.. # variable d'environnement
  --env-file .env \             # fichier .env
  -v ./data:/app/data \         # volume bind mount
  -v pgdata:/var/lib/postgresql # volume nommé
  --network mon-reseau \        # réseau Docker
  --restart unless-stopped \    # politique de redémarrage
  --rm \                        # supprimer à l'arrêt
  --read-only \                 # filesystem en lecture seule
  --memory 512m \               # limite mémoire
  --cpus 1.5 \                  # limite CPU
  python:3.11-slim

# Politiques de restart :
# no            → jamais (défaut)
# always        → toujours (même après reboot hôte)
# unless-stopped→ sauf si arrêt manuel
# on-failure:3  → seulement si erreur, max 3 fois
Cycle de vie des conteneurs
# Lister
docker ps                 # conteneurs actifs
docker ps -a              # tous
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"

# Arrêter / démarrer
docker stop mon-api       # SIGTERM (graceful, 10s)
docker stop -t 30 mon-api # SIGTERM, attend 30s
docker kill mon-api        # SIGKILL (immédiat)
docker start mon-api
docker restart mon-api

# Supprimer
docker rm mon-api          # doit être arrêté
docker rm -f mon-api       # force l'arrêt + supprime
docker rm $(docker ps -aq -f status=exited)  # tous les arrêtés

# Copier des fichiers
docker cp fichier.txt mon-api:/app/
docker cp mon-api:/app/log.txt ./logs/

# Renommer
docker rename ancien-nom nouveau-nom

Debug & inspection

Logs & accès shell
# Logs
docker logs mon-api             # logs complets
docker logs -f mon-api          # suivre en temps réel
docker logs --tail 50 mon-api   # 50 dernières lignes
docker logs --since 1h mon-api  # depuis 1 heure
docker logs --timestamps mon-api

# Entrer dans un conteneur en cours
docker exec -it mon-api bash       # shell bash
docker exec -it mon-api sh         # shell sh (Alpine)
docker exec -it mon-api python     # REPL Python
docker exec mon-api python -c "import sys; print(sys.version)"

# Lancer un conteneur interactif (debug)
docker run -it --rm python:3.11-slim bash
docker run -it --rm --entrypoint bash mon-app:latest

# Statistiques en temps réel
docker stats                   # tous les conteneurs
docker stats mon-api           # un seul
docker stats --no-stream       # snapshot (pas live)

# Processus dans un conteneur
docker top mon-api
Inspection & diff
# Inspecter un conteneur (JSON complet)
docker inspect mon-api
docker inspect --format '{{.NetworkSettings.IPAddress}}' mon-api
docker inspect --format '{{json .Config.Env}}' mon-api

# Diff — voir les changements depuis l'image de base
docker diff mon-api
# A = ajouté, C = modifié, D = supprimé

# Committer un conteneur modifié (rarement utile)
docker commit mon-api mon-app:debug

# Diagnostiquer un conteneur qui crashe

# 1. Le conteneur s'arrête immédiatement ?
docker run -it --entrypoint sh mon-app  # override CMD

# 2. Voir pourquoi le conteneur s'est arrêté
docker inspect mon-api --format '{{.State.ExitCode}}'
docker inspect mon-api --format '{{.State.Error}}'

# 3. Créer une image depuis un conteneur crashé
docker commit <container-id> debug-image
docker run -it debug-image sh

Ports & exposition

Mapping de ports — exemples

8080 8000 -p 8080:8000 — app web
5433 5432 -p 5433:5432 — postgres (évite conflit local)
127.0.0.1:6379 6379 -p 127.0.0.1:6379:6379 — localhost seulement
0 (aléatoire) 80 -p 80 — port hôte assigné dynamiquement
Ports — commandes
# Exposer un port : -p hôte:conteneur
docker run -p 8080:8000 mon-app     # TCP par défaut
docker run -p 53:53/udp dns-server  # UDP
docker run -p 8080:8000 -p 8443:8443 mon-app  # multiple

# -P (majuscule) — exposer tous les EXPOSE du Dockerfile
docker run -P mon-app
# Assigne des ports aléatoires sur l'hôte

# Voir les ports exposés
docker port mon-api
docker port mon-api 8000

# EXPOSE vs -p
# EXPOSE dans Dockerfile : documentation seulement
# Ne lie pas de port sur l'hôte
# -p à docker run : bind effectif hôte→conteneur

# Accès entre conteneurs (même réseau) :
# Pas besoin de -p ! On utilise le nom du service
# api → http://db:5432 (réseau interne)
# -p seulement pour exposer vers l'extérieur

Réseaux Docker

📡 bridge: mon-app-network (172.20.0.0/16)
api172.20.0.2
worker172.20.0.3
db172.20.0.4
redis172.20.0.5
api → http://db:5432 ✓  |  api → http://redis:6379 ✓  |  DNS interne automatique
🌐 Hôte expose : localhost:8080 → api:8000
Types de réseaux & commandes
# Types de drivers :
bridge   ← défaut : réseau isolé avec DNS
host     ← partage le réseau de l'hôte (Linux)
none     ← pas de réseau du tout
overlay  ← multi-hôte (Docker Swarm)
macvlan  ← IP directe sur le réseau physique

# Créer un réseau
docker network create mon-reseau
docker network create --driver bridge \
  --subnet 172.20.0.0/16 mon-reseau

# Connecter des conteneurs
docker run --network mon-reseau --name api mon-app
docker run --network mon-reseau --name db postgres:16

# api peut joindre db via http://db:5432
# DNS automatique par nom de conteneur !

# Connecter / déconnecter un conteneur en live
docker network connect mon-reseau mon-api
docker network disconnect mon-reseau mon-api

# Lister / inspecter
docker network ls
docker network inspect mon-reseau

Volumes & persistance des données

🖥️ Hôte
/var/lib/docker/volumes/pgdata/
./src/ (bind mount)
tmpfs (mémoire)

monté
🐳 Conteneur
/var/lib/postgresql/data
/app/src (hot reload)
/tmp (secrets en mémoire)
TypeCommandeUtilisation
Volume nommé -v pgdata:/var/lib/postgresql/data Données persistantes gérées par Docker. Recommandé pour les bases de données.
Bind mount -v ./src:/app/src Monter un dossier de l'hôte. Idéal pour le développement (hot reload).
tmpfs --tmpfs /tmp Monté en mémoire RAM. Pour les secrets ou données temporaires sensibles.
Anonyme -v /data Volume sans nom — créé et géré par Docker. Difficile à réutiliser.
Commandes volumes
# Créer un volume nommé
docker volume create pgdata
docker volume create --driver local \
  --opt type=none \
  --opt device=/mnt/data \
  --opt o=bind mon-volume

# Utiliser un volume
docker run -v pgdata:/var/lib/postgresql/data postgres:16
docker run -v ./code:/app:ro mon-app  # :ro = lecture seule

# Lister / inspecter
docker volume ls
docker volume inspect pgdata

# Supprimer
docker volume rm pgdata
docker volume prune              # volumes non utilisés

# Sauvegarder les données d'un volume
docker run --rm \
  -v pgdata:/data \
  -v $(pwd):/backup \
  alpine tar czf /backup/pgdata-backup.tar.gz /data

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

Syntaxe & services

Docker Compose permet de définir et gérer une stack multi-conteneurs dans un fichier YAML. Il gère automatiquement les réseaux, volumes et dépendances entre services.

🚀 api
build: . (Dockerfile local)
ports: 8000:8000
depends_on: db, redis
env: DATABASE_URL, REDIS_URL
volumes: .:/app (dev)
restart: unless-stopped
🗄️ db
image: postgres:16-alpine
ports: 5432 (interne)
volumes: pgdata:/var/lib/postgresql
env: POSTGRES_* vars
healthcheck: pg_isready
⚡ redis
image: redis:7-alpine
ports: 6379 (interne)
command: redis-server --appendonly yes
volumes: redisdata:/data
docker-compose.yml — stack complète annotée
services:

  api:
    build:
      context: .
      dockerfile: Dockerfile
      target: development      # multi-stage : stage dev
    ports: ["8000:8000"]
    environment:
      DATABASE_URL: postgresql://user:pass@db:5432/mydb
      REDIS_URL:    redis://redis:6379/0
    volumes:
      - .:/app                  # bind mount pour hot reload
      - /app/.venv              # volume anonyme — protéger le venv
    depends_on:
      db:
        condition: service_healthy   # attend le healthcheck de db
      redis:
        condition: service_started
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER:     user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB:       mydb
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
      interval: 10s
      timeout:  5s
      retries:  5
      start_period: 30s

  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
    volumes: ["redisdata:/data"]
    ports: ["6379:6379"]      # exposer seulement en dev

volumes:
  pgdata:           # géré par Docker, persiste entre redémarrages
  redisdata:

networks:
  default:
    name: mon-app-network

Variables d'environnement & configs

.env & substitution de variables
# .env — chargé automatiquement par Compose
APP_VERSION=1.3.0
POSTGRES_PASSWORD=MonMotDePasse123
REDIS_PASSWORD=AutreMotDePasse
DEBUG=false

# docker-compose.yml — utiliser les variables
services:
  api:
    image: mon-app:${APP_VERSION}
    environment:
      # Passer la variable telle quelle
      DEBUG: ${DEBUG}
      # Valeur par défaut si variable non définie
      LOG_LEVEL: ${LOG_LEVEL:-info}
      # Erreur si variable non définie
      API_KEY: ${API_KEY:?La variable API_KEY est requise}

# Fichiers .env multiples
# docker compose --env-file .env.production up

# Priorité des variables (du plus fort au plus faible) :
# 1. Variable shell (export VAR=val)
# 2. Fichier --env-file
# 3. Fichier .env
# 4. Valeur par défaut dans le YAML ${VAR:-default}
Override par environnement
# docker-compose.yml — configuration de base
services:
  api:
    image: mon-app:latest
    ports: ["8000:8000"]
    restart: unless-stopped

# docker-compose.override.yml — dev (auto-chargé)
services:
  api:
    build: .                   # rebuild local en dev
    volumes: [".:/app"]        # hot reload
    environment:
      DEBUG: "true"

# docker-compose.prod.yml — production
services:
  api:
    image: ghcr.io/owner/mon-app:v1.2.3
    deploy:
      replicas: 3
      resources:
        limits: { memory: 512m, cpus: '0.5' }

# Utilisation :
docker compose up                          # dev (base + override)
docker compose -f docker-compose.yml \
               -f docker-compose.prod.yml up  # prod

Commandes Docker Compose

Commandes essentielles
# Démarrer la stack
docker compose up           # foreground (logs visibles)
docker compose up -d        # détaché (background)
docker compose up --build   # rebuild avant démarrage
docker compose up --build --force-recreate  # tout recréer
docker compose up api db    # seulement certains services

# Arrêter
docker compose stop         # arrête sans supprimer
docker compose down         # arrête et supprime les conteneurs
docker compose down -v      # + supprime les volumes (données !)
docker compose down --rmi all  # + supprime les images

# Redémarrer un service
docker compose restart api

# Logs
docker compose logs         # tous les services
docker compose logs -f      # suivre en temps réel
docker compose logs -f api  # un seul service
docker compose logs --tail 100 api

# État des services
docker compose ps
docker compose top
Exécution & opérations
# Exécuter une commande dans un service
docker compose exec api bash
docker compose exec db psql -U user mydb
docker compose exec api python manage.py migrate
docker compose exec api pytest tests/

# Lancer une commande one-shot (new conteneur)
docker compose run --rm api pytest
docker compose run --rm api python seed.py
docker compose run --rm --no-deps api bash
# --no-deps : ne lance pas les dépendances

# Scaler un service
docker compose up -d --scale worker=3

# Valider la config (sans démarrer)
docker compose config
docker compose config --services

# Pull toutes les images
docker compose pull

# Build sans démarrer
docker compose build
docker compose build --no-cache api

Profiles & healthcheck avancé

Profiles — services optionnels
services:
  api:
    image: mon-app
    # Pas de profile = toujours démarré

  db:
    image: postgres:16

  worker:
    image: mon-app
    command: celery worker
    profiles: ["worker"]
    # Démarré seulement si profile 'worker' activé

  pgadmin:
    image: dpage/pgadmin4
    profiles: ["debug", "tools"]

  mailhog:
    image: mailhog/mailhog
    profiles: ["debug"]

# Utilisation :
docker compose up                     # api + db
docker compose --profile worker up    # + worker
docker compose --profile debug up     # + pgadmin + mailhog
COMPOSE_PROFILES=worker,debug \
  docker compose up
Dépendances & healthchecks
services:
  api:
    depends_on:
      db:
        condition: service_healthy   # attend healthcheck OK
        restart: true               # restart api si db redémarre
      migration:
        condition: service_completed_successfully

  # Job one-shot : applique les migrations
  migration:
    image: mon-app
    command: python manage.py migrate
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"]
      interval: 5s
      timeout:  3s
      retries:  10
      start_period: 15s

# États healthcheck :
# starting  → dans start_period
# healthy   → test passe
# unhealthy → test échoue N fois
# none      → pas de healthcheck défini

Registry & stratégie de tags

ghcr.io/owner/mon-app
:latest prod sha256:a3f2c1b… 145 MB
:v1.3.2 prod sha256:a3f2c1b… 145 MB
:v1.3.1 prod sha256:9d7e3a0… 143 MB
:sha-f7c3a91 staging sha256:f7c3a91… 147 MB
:main dev sha256:f7c3a91… 147 MB
ℹ️

Règle : en production, toujours utiliser un tag immuable (:v1.3.2 ou :sha-f7c3a91) — jamais :latest qui peut changer à tout moment. Le tag :latest est pratique pour le développement local.

Workflow de publication
# Construire avec plusieurs tags en une fois
docker build \
  -t monapp:latest \
  -t monapp:v1.3.2 \
  -t monapp:sha-$(git rev-parse --short HEAD) \
  .

# Build multi-architecture (AMD64 + ARM64)
docker buildx create --use
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t ghcr.io/owner/mon-app:v1.3.2 \
  --push \
  .

# Registries populaires :
# Docker Hub       → docker.io (public par défaut)
# GitHub GHCR      → ghcr.io/owner/image
# AWS ECR          → 1234.dkr.ecr.eu-west-1.amazonaws.com
# GCP Artifact Reg → europe-docker.pkg.dev/project/repo

# Rollback instantané en production
# Un bug détecté en v1.3.2 ?
docker pull ghcr.io/owner/mon-app:v1.3.1
docker run ghcr.io/owner/mon-app:v1.3.1
# Pas de rebuild — l'ancienne image est là !

Sécurité Docker

Scan de vulnérabilités
# Trivy — scanner open source (recommandé)
trivy image python:3.11-slim
trivy image mon-app:latest
trivy image --severity HIGH,CRITICAL mon-app:latest
trivy fs .                        # scanner le code source
trivy config ./Dockerfile         # erreurs de config

# Grype — autre scanner
grype mon-app:latest
grype sbom:mon-app.sbom.json      # scanner un SBOM

# Générer un SBOM (Software Bill of Materials)
syft mon-app:latest -o spdx-json > sbom.json
# Liste toutes les dépendances de l'image

# Docker Scout (intégré à Docker Desktop)
docker scout cves mon-app:latest
docker scout recommendations mon-app:latest

# Dans GitHub Actions :
- name: Scan image
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: 'mon-app:latest'
    severity: 'CRITICAL,HIGH'
    exit-code: '1'        # fail le pipeline si vulnérabilité
Bonnes pratiques de sécurité
# 1. Utilisateur non-root (voir section Dockerfile)
USER appuser

# 2. Filesystem en lecture seule + tmpfs pour /tmp
docker run \
  --read-only \
  --tmpfs /tmp \
  --tmpfs /var/run \
  mon-app

# 3. Capabilities Linux minimales
docker run \
  --cap-drop ALL \
  --cap-add NET_BIND_SERVICE \
  mon-app

# 4. Pas de privileged (sauf cas très spécifiques)
docker run --privileged …  # ← presque root sur l'hôte !

# 5. Limiter les ressources
docker run \
  --memory 512m \
  --memory-swap 512m \   # = pas de swap
  --cpus 1.0 \
  --pids-limit 100 \     # limiter les processus
  mon-app

# 6. Secrets — jamais dans les variables d'env
# (visibles avec docker inspect)
# → Utiliser Docker secrets (Swarm) ou
#   monter un fichier via --mount type=secret
docker run \
  --mount type=secret,id=db_password \
  mon-app

Docker dans un pipeline CI/CD

GitHub Actions — pipeline complet
name: Docker Build & Deploy

on:
  push:
    branches: [main]
    tags: ['v*.*.*']

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      security-events: write

    steps:
      - uses: actions/checkout@v4

      - name: Set up QEMU (multi-arch)
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Docker metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=semver,pattern={{version}}
            type=sha,prefix=sha-
            type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
suite — build, scan, deploy
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          # Cache des couches entre builds CI
          cache-from: type=gha
          cache-to:   type=gha,mode=max

      - name: Scan pour vulnérabilités
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
          severity: CRITICAL,HIGH
          exit-code: '1'

  deploy:
    needs: build-push
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://mon-app.com
    if: github.ref == 'refs/heads/main'

    steps:
      - name: Deploy via SSH
        run: |
          ssh user@serveur \
            "docker pull $IMAGE && \
             docker compose up -d --no-deps api"
        env:
          IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest

Cheat sheet Docker

Images

docker build -t app:tag .Construire
docker imagesLister
docker pull nginx:alpineRécupérer
docker push registry/appPublier
docker rmi app:tagSupprimer
docker image prune -aNettoyer

Conteneurs

docker run -d -p 8080:80Démarrer (détaché)
docker ps / docker ps -aLister actifs / tous
docker stop / killArrêter graceful / brutal
docker logs -f appSuivre les logs
docker exec -it app bashEntrer dans le conteneur
docker statsCPU / mémoire en direct

Docker Compose

docker compose up -dDémarrer en fond
docker compose up --buildRebuild + démarrer
docker compose down -vArrêter + volumes
docker compose logs -f apiLogs d'un service
docker compose exec db bashShell dans service
docker compose run --rm api shOne-shot

Bonnes pratiques

Image de baseslim ou alpine, tag précis
Ordre couchesStable → volatile
Multi-stageBuilder séparé → image légère
USERNon-root obligatoire en prod
SecretsJamais dans l'image — env runtime
Scantrivy image avant déploiement