Docker
Conteneurs
Images, conteneurs, Dockerfile, volumes, réseaux, Docker Compose et intégration CI/CD — du concept à la production.
Pourquoi Docker ?
# 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 | |
|---|---|---|
| Isolation | OS complet virtualisé | Processus isolé (namespaces Linux) |
| Taille | Plusieurs Go | Quelques Mo à centaines de Mo |
| Démarrage | Minutes | Secondes (parfois ms) |
| Densité | Quelques VMs / hôte | Dizaines à centaines / hôte |
| Portabilité | Image lourde (OVA, VMDK) | Image légère, pousse sur registry |
| Noyau | Noyau dédié par VM | Partagé avec l'hôte |
| Sécurité | Isolation forte | Bonne — 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
| Composant | Rôle |
|---|---|
| Docker Engine | Le moteur : daemon (dockerd) + CLI (docker) + API REST |
| Image | Template immuable en couches. Blueprint d'un conteneur. |
| Conteneur | Instance en cours d'exécution d'une image. Processus isolé. |
| Registry | Dépôt d'images. Docker Hub, GHCR, ECR, registry privé. |
| Volume | Stockage persistant géré par Docker, survit au conteneur. |
| Network | Réseau virtuel isolé. Les conteneurs s'y connectent. |
| Dockerfile | Recette textuelle pour construire une image. |
| Compose | Orchestration multi-conteneurs avec un fichier YAML. |
# É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.
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.
# ✗ 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
| Instruction | Usage | Exemple |
|---|---|---|
FROM | Image de base — obligatoire en 1er | FROM python:3.11-slim |
WORKDIR | Répertoire de travail (créé si absent) | WORKDIR /app |
COPY | Copier fichiers hôte → image | COPY src/ /app/src/ |
ADD | Comme COPY + décompresse archives, URLs | ADD archive.tar.gz /app/ |
RUN | Exécuter commande pendant le build | RUN apt-get update && apt-get install -y curl |
ENV | Variable d'env persistante dans l'image | ENV APP_ENV=production |
ARG | Variable de build (disparaît après build) | ARG VERSION=1.0 |
EXPOSE | Documenter le port (ne l'ouvre PAS) | EXPOSE 8000 |
VOLUME | Déclarer un point de montage | VOLUME ["/data"] |
USER | Utilisateur pour les instructions suivantes | USER appuser |
CMD | Commande par défaut (overridable) | CMD ["uvicorn", "app.main:app"] |
ENTRYPOINT | Point d'entrée fixe (non overridable) | ENTRYPOINT ["python"] |
LABEL | Métadonnées de l'image | LABEL version="1.0" |
HEALTHCHECK | Vérifier la santé du conteneur | HEALTHCHECK CMD curl -f http://localhost/ |
# 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
# ✅ 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
# ✅ 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.
# ── 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 !
# 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
# 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.
# 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
# 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 !)
# 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
# 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
# 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
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
# 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
# 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
http://db:5432 ✓ | api → http://redis:6379 ✓ | DNS interne automatique
localhost:8080 → api:8000
# 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
monté
| Type | Commande | Utilisation |
|---|---|---|
| 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. |
# 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.
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 — 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}
# 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
# 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é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é
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
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
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.
# 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
# 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é
# 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
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' }}
- 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 images | Lister |
| docker pull nginx:alpine | Récupérer |
| docker push registry/app | Publier |
| docker rmi app:tag | Supprimer |
| docker image prune -a | Nettoyer |
Conteneurs
| docker run -d -p 8080:80 | Démarrer (détaché) |
| docker ps / docker ps -a | Lister actifs / tous |
| docker stop / kill | Arrêter graceful / brutal |
| docker logs -f app | Suivre les logs |
| docker exec -it app bash | Entrer dans le conteneur |
| docker stats | CPU / mémoire en direct |
Docker Compose
| docker compose up -d | Démarrer en fond |
| docker compose up --build | Rebuild + démarrer |
| docker compose down -v | Arrêter + volumes |
| docker compose logs -f api | Logs d'un service |
| docker compose exec db bash | Shell dans service |
| docker compose run --rm api sh | One-shot |
Bonnes pratiques
| Image de base | slim ou alpine, tag précis |
| Ordre couches | Stable → volatile |
| Multi-stage | Builder séparé → image légère |
| USER | Non-root obligatoire en prod |
| Secrets | Jamais dans l'image — env runtime |
| Scan | trivy image avant déploiement |