Ce que vous saurez faire à la fin
- Installer Docker et lancer votre premier conteneur
- Écrire un Dockerfile multi-stage optimisé
- Orchestrer plusieurs services avec Docker Compose
- Gérer volumes, réseaux, secrets et health checks
- Déployer en production avec registry et stratégie de mise à jour
Durée : 3 heures. Pré-requis : Linux/Mac/Windows avec WSL2, 10 Go d’espace disque, compte Docker Hub.
Étape 1 — Installer Docker
# Linux (un-liner officiel)
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER
# Déconnectez-vous puis reconnectez-vous
# macOS
brew install --cask docker
# Ou téléchargez Docker Desktop depuis docker.com
# Windows: Docker Desktop avec WSL2 activé
# Vérification
docker --version
docker compose version
docker run hello-world
Étape 2 — Premier conteneur
# Lancer un conteneur Nginx
docker run -d -p 8080:80 --name web nginx:alpine
# Vérifier
curl http://localhost:8080
docker ps
docker logs web
# Entrer dans le conteneur
docker exec -it web sh
# Arrêter et supprimer
docker stop web
docker rm web
Étape 3 — Premier Dockerfile
- Créez un dossier
mon-app/avec un simpleserver.js:
// server.js
const http = require("http");
http.createServer((req, res) => {
res.writeHead(200);
res.end("Hello from Docker!");
}).listen(3000);
console.log("Server running on 3000");
- Créez
Dockerfile:
FROM node:20-alpine
WORKDIR /app
COPY server.js .
EXPOSE 3000
USER node
CMD ["node", "server.js"]
- Build et run :
docker build -t mon-app:1.0 .
docker run -d -p 3000:3000 --name app mon-app:1.0
curl http://localhost:3000
docker logs -f app
Étape 4 — Dockerfile multi-stage optimisé
# syntax=docker/dockerfile:1.7
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm npm ci --only=production
FROM node:20-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:20-alpine AS runtime
ENV NODE_ENV=production
WORKDIR /app
# Utilisateur non-root
RUN addgroup -S app && adduser -S app -G app
USER app
COPY --from=build --chown=app:app /app/dist ./dist
COPY --from=deps --chown=app:app /app/node_modules ./node_modules
COPY --chown=app:app package.json ./
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget --spider -q http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]
- Avantages : image finale < 200 MB, sans outils de build, sans racines, avec healthcheck.
Étape 5 — .dockerignore
Évite d’envoyer des fichiers inutiles au daemon Docker.
node_modules
dist
.git
.env
.env.*
*.log
coverage
.vscode
.idea
README.md
Dockerfile*
docker-compose*
.github
Étape 6 — Volumes pour la persistance
# Volume nommé (recommandé en prod)
docker volume create pgdata
docker run -d \
--name pg \
-v pgdata:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=secret \
-p 5432:5432 \
postgres:16
# Bind mount (pour dev)
docker run -v "$(pwd):/app" -w /app node:20 npm test
# Lister, inspecter, supprimer
docker volume ls
docker volume inspect pgdata
docker volume rm pgdata # attention: perd les données
Étape 7 — Docker Compose : stack complet
- Créez
compose.yaml:
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: itsc
POSTGRES_USER: itsc
POSTGRES_PASSWORD: ${DB_PASS:?DB_PASS manquant}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U itsc"]
interval: 5s
retries: 10
redis:
image: redis:7-alpine
command: ["redis-server", "--save", "60", "1"]
volumes:
- redisdata:/data
api:
build: .
environment:
DATABASE_URL: postgres://itsc:${DB_PASS}@db:5432/itsc
REDIS_URL: redis://redis:6379
ports:
- "3000:3000"
depends_on:
db: { condition: service_healthy }
redis: { condition: service_started }
restart: unless-stopped
deploy:
resources:
limits: { cpus: "1.0", memory: "512M" }
volumes:
pgdata:
redisdata:
- Créez
.env:
DB_PASS=un_mot_de_passe_fort
- Lancez :
docker compose up -d
docker compose ps
docker compose logs -f api
docker compose exec db psql -U itsc
docker compose down # stop et retire
docker compose down -v # + retire volumes
Étape 8 — Réseau et DNS interne
- Docker Compose crée automatiquement un réseau
itsc_default. - Dans le conteneur api, les noms db et redis sont résolus par DNS interne.
- Pas besoin d’exposer les ports DB à l’extérieur. Seul api est exposé via le port 3000.
docker network ls
docker network inspect itsc_default
Étape 9 — Secrets et variables d’environnement
services:
api:
secrets:
- db_password
- api_key
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
db_password:
file: ./secrets/db_password.txt
api_key:
external: true # géré par Docker secret
Dans le code, lire depuis /run/secrets/db_password au lieu de variables d’env. Les secrets ne sont pas dans les logs ni dans l’image.
Étape 10 — Image Go ultra-légère
FROM golang:1.22-alpine AS build
WORKDIR /src
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app -ldflags="-s -w" ./cmd/server
FROM scratch
COPY --from=build /app /app
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
USER 65532:65532
ENTRYPOINT ["/app"]
Image finale : 15 MB au lieu de 900 MB. Aucun OS, aucun shell : surface d’attaque minimale.
Étape 11 — Scan de sécurité
# Trivy (gratuit, très complet)
brew install trivy
trivy image mon-app:1.0
# Docker Scout (intégré Docker Desktop)
docker scout cves mon-app:1.0
docker scout recommendations mon-app:1.0
# Snyk
npm install -g snyk
snyk container test mon-app:1.0
Étape 12 — Push vers un registry
# Docker Hub
docker login
docker tag mon-app:1.0 monuser/mon-app:1.0
docker push monuser/mon-app:1.0
# GitHub Container Registry (recommandé)
echo $GH_TOKEN | docker login ghcr.io -u monuser --password-stdin
docker tag mon-app:1.0 ghcr.io/monorg/mon-app:1.0
docker push ghcr.io/monorg/mon-app:1.0
# Registry privé interne
docker login registry.itsc.sn
docker tag mon-app:1.0 registry.itsc.sn/mon-app:1.0
docker push registry.itsc.sn/mon-app:1.0
Étape 13 — Déploiement en production
# Sur le serveur
docker pull ghcr.io/monorg/mon-app:1.2.3
docker compose pull api
docker compose up -d --no-deps api
# Rolling update avec un script simple
#!/usr/bin/env bash
set -e
docker pull ghcr.io/monorg/mon-app:$1
docker compose up -d --no-deps --scale api=2 api
sleep 15 # laisse temps au health check
docker compose up -d --no-deps --scale api=1 api
Étape 14 — Monitoring et logs
docker stats # CPU/RAM/IO temps réel
docker logs --tail=100 -f api
docker inspect --format='{{json .State}}' api | jq
# Logs centralisés (driver gelf/loki)
docker run -d --log-driver=loki \
--log-opt loki-url="https://loki.itsc.sn/loki/api/v1/push" \
mon-app:1.0
Étape 15 — Nettoyage
# Voir ce qui prend de la place
docker system df
# Nettoyer proprement
docker container prune # conteneurs arrêtés
docker image prune -a # images non utilisées
docker volume prune # volumes orphelins
docker network prune # réseaux orphelins
# Tout d'un coup (attention)
docker system prune -a --volumes
Checklist production
✓ Dockerfile multi-stage, image finale < 300 MB
✓ USER non-root dans le conteneur
✓ HEALTHCHECK défini
✓ .dockerignore présent
✓ Secrets via Docker secrets, jamais en env brut
✓ Compose avec depends_on + healthchecks
✓ Limits CPU/RAM définies
✓ Scan Trivy: 0 vuln High/Critical
✓ Tags avec version sémantique (pas latest en prod)
✓ Registry privé pour images internes
À lire ensuite : scanner les vulnerabilites containers et IaC avec Trivy.
Vous cherchez un hébergement web sérieux et abordable ?
Hostinger offre des serveurs avec installation WordPress en un clic et un panel simple. Notre équipe l’utilise au quotidien.
Lien d affiliation. Si vous achetez via ce lien, le blog reçoit une petite commission sans surcoût pour vous.
Maîtriser BuildKit et le cache layers pour des builds rapides
BuildKit est le moteur de build moderne de Docker (par défaut depuis 23.0). Sa différence majeure avec le builder legacy : exécution parallèle des étapes indépendantes et cache partagé entre projets via cache mounts. Sur un Dockerfile Node.js typique, BuildKit divise le build par 3-5 quand le cache est chaud.
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --prefer-offline
COPY . .
RUN npm run build
Le cache mount sur /root/.npm économise les téléchargements npm répétitifs entre builds. Sur un VPS Hetzner CX22 à Cotonou, un build qui prenait 3 minutes passe à 35 secondes en mode incrémental. Multipliez par 50 builds par semaine = 2 heures économisées par développeur.
Sécuriser ses images avec Trivy et Snyk dans la CI
Une image Docker non auditée peut contenir des CVE critiques connues. Trivy (open-source, gratuit) scanne une image et liste les vulnérabilités classées par sévérité. Intégrez-le dans GitHub Actions pour bloquer le merge si des CVE high ou critical apparaissent.
# .github/workflows/docker-scan.yml
- uses: aquasecurity/trivy-action@master
with:
image-ref: 'monapp:${{ github.sha }}'
severity: 'CRITICAL,HIGH'
exit-code: '1'
Trivy détecte aussi les secrets accidentellement copiés dans l’image (clés AWS, tokens GitHub). Pour une fintech à Almadies qui livre 5 microservices, cette protection automatique évite la fuite de credentials qui coûte des dizaines de milliers de FCFA en remediation.
Optimiser la taille des images avec distroless ou Alpine
Une image Node.js classique pèse 1-1,5 Go (debian:slim + npm + node_modules). Une image distroless équivalente pèse 150-250 Mo. Ce gain accélère les déploiements, réduit le coût de stockage registry, et diminue la surface d’attaque (pas de shell, pas de package manager dans le runtime).
FROM node:22-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
FROM gcr.io/distroless/nodejs22-debian12 AS runtime
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
CMD ["server.js"]
L’image distroless n’a pas de shell : impossible d’exécuter docker exec -it container sh pour debugger. Pour debugger, utilisez la variante :debug qui inclut un BusyBox léger uniquement pour les sessions de troubleshooting.
Utiliser docker compose watch pour le développement local
docker compose watch (stable depuis Docker Compose 2.22) automatise le rebuild ou la synchronisation de fichiers à chaque changement. Pour une équipe à Plateau qui développe une API Node.js avec hot reload, cette commande remplace nodemon en synchronisant les fichiers du host vers le container sans redémarrer le service.
# compose.yaml
services:
api:
build: .
develop:
watch:
- action: sync
path: ./src
target: /app/src
- action: rebuild
path: package.json
Lancez avec docker compose watch. Les changements dans src/ se synchronisent en moins d’une seconde, les changements package.json déclenchent un rebuild complet. Cette ergonomie élimine la friction qui poussait certaines équipes à abandonner Docker en dev.
Adopter rootless Docker pour la sécurité production
Le démon Docker tourne par défaut en root, ce qui pose un risque de privilege escalation si un container compromis exploite une faille du runtime. Docker rootless permet de tourner le démon sous un utilisateur non-privilégié. Pour les environnements de production sensibles (banque, santé), c’est un must-have en 2026.
L’installation rootless prend 15-20 minutes via le script officiel dockerd-rootless-setuptool.sh install. Quelques limites à connaître : pas de mode –privileged, certaines options réseau différentes (bridge limité). Pour 90 % des charges applicatives, ces limites sont sans impact. Sur le même thème, voir notre tutoriel Docker rootless et Docker vs Podman.
Choisir entre registry public et registry privé
Docker Hub reste le registry public par défaut, gratuit pour les images publiques mais limité en pull rate (200 pulls/6h pour les comptes anonymes en 2026). Pour une équipe qui pull en CI plusieurs fois par jour, cette limite devient bloquante. Trois alternatives selon le besoin. GitHub Container Registry (gratuit, illimité pour les repos publics, rate-limité par token GitHub Actions). GitLab Container Registry (intégré à votre instance GitLab self-hosted ou cloud). Harbor self-hosted (complexe mais total contrôle, recommandé pour 50+ services).
# Pousser vers GHCR
echo $TOKEN | docker login ghcr.io -u USERNAME --password-stdin
docker tag monapp:latest ghcr.io/org/monapp:latest
docker push ghcr.io/org/monapp:latest
Pour une PME basée à Lomé qui livre 3 microservices, GHCR couvre largement. Pour une plateforme de 30+ services, Harbor self-hosted sur un VPS Hetzner CX42 à 18 000 FCFA/mois donne le contrôle complet et économise les frais cloud.
Implémenter un health check propre pour orchestrateurs
Sans health check, un orchestrateur (Docker Swarm, Kubernetes, Coolify) ne sait pas si votre container répond vraiment ou s’il est zombie. La directive HEALTHCHECK dans le Dockerfile décrit une commande qui valide la santé toutes les N secondes.
FROM node:22-alpine
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "server.js"]
Côté code, exposez un endpoint /health qui vérifie : connexion base de données OK, accès à Redis OK, pas de file d’attente saturée. Si l’un échoue, renvoyez 503. L’orchestrateur redémarre alors le container automatiquement. Cette discipline évite les pannes silencieuses qui ne sont détectées que par des plaintes utilisateur.
Documentez le contrat du health check (codes HTTP, conditions exactes) dans le README du service pour qu’un opérateur de garde sache interpréter une alerte sans avoir à fouiller le code.
Cette traçabilité écrite est ce qui distingue une équipe junior réactive d’une équipe senior qui anticipe les incidents.
Investir 30 minutes dans cette documentation paie pendant des années face aux audits clients ou aux relèves d’équipe.
C’est le pari long-terme qui rend une équipe DevOps vraiment fiable.