Introduction
Docker Compose est parfait pour de petites à moyennes infrastructures (jusqu'à ~10-20 conteneurs sur un VPS). Mais en production, on n'utilise pas un docker-compose comme en dev. Il faut prévoir :
- Healthchecks pour la reprise auto en cas de crash
- Restart policies pour redémarrer après reboot
- Volumes nommés (pas de bind mounts à l'aveugle)
- Networks isolés
- Gestion des secrets
- Logs limités
Ce guide vous donne le template prod que vous pouvez réutiliser.
Prérequis
- VPS Linux avec Docker et Docker Compose v2 installés
- Voir tuto VeryCloud "Installer Docker sur Linux" pour l'install : https://verycloud.fr/docs/article/install-docker-linux
Étape 1 : Vérifier Docker
docker --version
docker compose version
Docker Compose v2 (docker compose, pas docker-compose) est requis.
Étape 2 : Structure de projet recommandée
Pour chaque app, organisez :
/opt/myapp/
├── docker-compose.yml
├── .env # variables d'env (gitignore)
├── .env.example # template public
├── secrets/ # secrets sensibles (gitignore)
│ ├── db_password.txt
│ └── api_key.txt
├── data/ # bind mounts si nécessaire
│ └── ...
└── config/ # configs personnalisées
└── nginx.conf
Permissions :
sudo mkdir -p /opt/myapp/{secrets,data,config}
sudo chmod 700 /opt/myapp/secrets
Étape 3 : Template docker-compose.yml de production
# /opt/myapp/docker-compose.yml
services:
app:
image: monapp:1.2.3 # ⚠️ pin la version exacte, pas "latest"
container_name: myapp
restart: unless-stopped
ports:
- "127.0.0.1:3000:3000" # Bind sur localhost uniquement, exposé via Nginx
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://app:${DB_PASSWORD}@db:5432/myapp
secrets:
- db_password
volumes:
- app_data:/data
- ./config/app.conf:/etc/app/app.conf:ro
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
networks:
- frontend
- backend
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
deploy:
resources:
limits:
memory: 1G
cpus: '1.0'
db:
image: postgres:16-alpine
container_name: myapp-db
restart: unless-stopped
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=app
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
secrets:
- db_password
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 10s
timeout: 5s
retries: 5
networks:
- backend
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
redis:
image: redis:7-alpine
container_name: myapp-redis
restart: unless-stopped
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 3s
retries: 5
networks:
- backend
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
volumes:
app_data:
driver: local
db_data:
driver: local
redis_data:
driver: local
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # Pas d'accès Internet pour ce network
secrets:
db_password:
file: ./secrets/db_password.txt
Points clés :
restart: unless-stopped: redémarre auto sauf si arrêté manuellement- Images pinnées : pas de
latest, toujours1.2.3 - Healthchecks sur tous les services critiques
- Volumes nommés : portable, ne pollue pas le système
- Networks séparés : frontend (web visible) vs backend (DB isolée)
- Logs limités : 10 MB × 3 fichiers max par container
- Resources : caps CPU/RAM pour éviter qu'un container saturé bouffe tout
- Secrets : mots de passe dans des fichiers, pas dans
environment
Étape 4 : Fichier .env
sudo nano /opt/myapp/.env
# Database
DB_PASSWORD=will_be_overridden_by_secret_file
# Redis
REDIS_PASSWORD=UnMotDePasseLongRedis_42
# App
APP_PORT=3000
Variables réutilisables avec ${VAR} dans docker-compose.yml.
sudo chmod 600 /opt/myapp/.env
Étape 5 : Secrets via fichiers
echo "UnMotDePasseLongBDD_2024" | sudo tee /opt/myapp/secrets/db_password.txt
sudo chmod 600 /opt/myapp/secrets/db_password.txt
Le container y accède via /run/secrets/db_password et l'environnement avec POSTGRES_PASSWORD_FILE=/run/secrets/db_password.
Avantage vs environment: POSTGRES_PASSWORD=... :
- Pas visible dans
docker inspect - Pas exporté dans les logs/env du process
Étape 6 : Démarrer la stack
cd /opt/myapp
sudo docker compose up -d
Vérifiez :
sudo docker compose ps
sudo docker compose logs -f
État attendu :
NAME STATUS PORTS
myapp Up (healthy) 127.0.0.1:3000->3000/tcp
myapp-db Up (healthy)
myapp-redis Up (healthy)
Étape 7 : Healthchecks expliqués
Le healthcheck permet à Docker de savoir si un container va bien. Format :
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s # Fréquence des checks
timeout: 5s # Temps max pour répondre
retries: 3 # Tentatives avant de marquer "unhealthy"
start_period: 30s # Période de grâce au démarrage
depends_on: condition: service_healthy attend que le check passe avant de démarrer l'app dépendante (ex: app attend que la DB soit ready).
Étape 8 : Backup automatique des volumes
Les volumes Docker sont stockés dans /var/lib/docker/volumes/. Pour les sauvegarder :
sudo nano /usr/local/bin/backup-docker-volumes.sh
#!/bin/bash
set -e
BACKUP_DIR=/backup/docker
DATE=$(date +%F_%H%M)
mkdir -p $BACKUP_DIR
cd /opt/myapp
# Arrêter brièvement la DB pour un snapshot cohérent (10s max)
docker compose stop db redis
# Backup volumes
for vol in $(docker volume ls -q --filter name=myapp_); do
docker run --rm \
-v ${vol}:/source:ro \
-v ${BACKUP_DIR}:/backup \
alpine tar -czf /backup/${vol}_${DATE}.tar.gz -C /source .
done
# Redémarrer
docker compose start db redis
# Rotation : garder 7 jours
find $BACKUP_DIR -type f -mtime +7 -delete
echo "Backup OK: $DATE"
sudo chmod +x /usr/local/bin/backup-docker-volumes.sh
sudo crontab -e
0 3 * * * /usr/local/bin/backup-docker-volumes.sh
Pour quelque chose de plus propre, voir le tuto Restic + Backblaze B2.
Étape 9 : Mise à jour propre
cd /opt/myapp
# Pull les nouvelles versions des images
sudo docker compose pull
# Recréer avec nouvelles images
sudo docker compose up -d --remove-orphans
# Nettoyer les vieilles images
sudo docker image prune -f
Pour automatiser, utilisez Watchtower :
watchtower:
image: containrrr/watchtower
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_SCHEDULE=0 0 4 * * * # 4h du matin
- WATCHTOWER_NOTIFICATIONS=email
⚠️ Watchtower met à jour vers la dernière tag. Si vous pinnez à 1.2.3, il n'updatera pas. Si vous voulez du 1.x automatique, utilisez la tag 1 ou 1.2.
Étape 10 : Logs centralisés
Par défaut les logs Docker sont stockés en JSON dans /var/lib/docker/containers/. Pour centraliser :
sudo docker compose logs --tail=100 -f
Pour du multi-services en agrégé, voir le tuto Loki + Promtail.
Étape 11 : Resource limits (très important)
Sans limites, un container qui fuit peut bouffer toute la RAM/CPU :
services:
app:
deploy:
resources:
limits:
memory: 1G
cpus: '1.0'
reservations:
memory: 512M
cpus: '0.5'
⚠️ En mode "compose" (pas swarm), deploy: est partiellement supporté. Préférez :
services:
app:
mem_limit: 1g
cpus: 1.0
Étape 12 : Reverse proxy frontal
Le port 3000 est bindé sur 127.0.0.1 uniquement. Pour exposer en HTTPS, ajoutez Nginx ou Traefik en frontal :
# /etc/nginx/sites-available/myapp
server {
listen 443 ssl http2;
server_name app.votre-domaine.com;
ssl_certificate /etc/letsencrypt/live/app.votre-domaine.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.votre-domaine.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Dépannage
Container redémarre en boucle
sudo docker compose logs app
Causes fréquentes :
- Variable d'env manquante
- Volume avec mauvaises permissions
- Healthcheck qui fail au démarrage (augmentez
start_period)
"no space left on device"
Nettoyez :
sudo docker system prune -af --volumes
⚠️ Supprime tout ce qui n'est pas utilisé. Soyez sûr avant.
Logs énormes qui remplissent le disque
Vérifiez que logging: est bien configuré dans chaque service. Sinon, les logs Docker peuvent grossir indéfiniment.
Container ne voit pas un autre
Ils doivent être sur le même network. Vérifiez :
sudo docker compose config
sudo docker network inspect myapp_backend
Commandes utiles
# État de la stack
sudo docker compose ps
# Logs
sudo docker compose logs -f service_name
# Restart un service
sudo docker compose restart app
# Re-build et redémarrer
sudo docker compose up -d --force-recreate
# Stop sans supprimer
sudo docker compose stop
# Stop + remove containers (mais garde volumes)
sudo docker compose down
# Stop + remove TOUT (incluant volumes ⚠️)
sudo docker compose down -v
# Exec dans un container
sudo docker compose exec app sh
# Inspect un service
sudo docker compose config
# Voir les ressources
sudo docker stats
# Networks
sudo docker network ls
# Volumes
sudo docker volume ls
sudo docker volume inspect myapp_db_data
Conclusion
Avec ce template, votre app est :
- Résiliente : redémarre automatiquement en cas de crash
- Sécurisée : secrets isolés, backend non exposé
- Maintenable : volumes nommés, networks séparés
- Observée : healthchecks, logs limités
Pour aller plus loin :
- Migrez vers Docker Swarm ou Kubernetes quand vous avez 10+ services à orchestrer
- Utilisez Portainer ou Komodo pour gérer plusieurs stacks via UI
- Mettez en place Watchtower pour les updates auto contrôlés
Ressources
- Documentation Docker Compose : https://docs.docker.com/compose/
- Tuto VeryCloud — Installer Docker : https://verycloud.fr/docs/article/install-docker-linux
- Tuto VeryCloud — Backup Restic :
/docs/article/restic-b2 - Tuto VeryCloud — Logs centralisés Loki :
/docs/article/loki-promtail



















