🔝 Retour au Sommaire
Dans une application composée de plusieurs services, certains services dépendent d'autres pour fonctionner. Par exemple, votre application backend ne peut pas démarrer sans que la base de données soit prête à accepter des connexions.
Gérer correctement ces dépendances est crucial pour éviter des erreurs au démarrage et garantir que votre application fonctionne de manière fiable. Dans cette section, nous allons explorer les différentes techniques pour gérer ces dépendances efficacement.
Nous avons vu depends_on dans les sections précédentes. Rappel :
services:
app:
image: mon-app
depends_on:
- database
database:
image: postgres:15Avec cette configuration, Docker Compose garantit que :
- Le conteneur
databasedémarre avant le conteneurapp - Le conteneur
apps'arrête avant le conteneurdatabaselors d'undocker compose down
depends_on attend seulement que le conteneur démarre, pas qu'il soit prêt !
Voici ce qui se passe en réalité :
Temps 0s : database démarre (conteneur créé)
Temps 1s : app démarre (depends_on satisfait)
Temps 2s : app essaie de se connecter à database
Temps 3s : ERREUR ! database n'est pas encore prête
Temps 5s : database est enfin prête à accepter des connexions
PostgreSQL, MySQL et autres bases de données ont besoin de temps pour :
- Initialiser leurs fichiers
- Charger leurs configurations
- Créer les bases de données
- Être prêtes à accepter des connexions
Pendant ce temps, votre application peut échouer en essayant de se connecter trop tôt ! 😱
Les health checks permettent de vérifier qu'un service est réellement prêt à fonctionner, pas seulement qu'il a démarré.
services:
database:
image: postgres:15
environment:
POSTGRES_PASSWORD: secret
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s
app:
image: mon-app
depends_on:
database:
condition: service_healthyExplication des paramètres :
-
test : La commande à exécuter pour vérifier la santé
pg_isready: Outil PostgreSQL qui vérifie si la base accepte des connexionsCMD-SHELL: Exécute la commande dans un shell
-
interval : Temps entre chaque vérification (ici, toutes les 5 secondes)
-
timeout : Temps maximum pour que la commande se termine (5 secondes)
-
retries : Nombre d'échecs consécutifs avant de marquer le service comme "unhealthy" (5 fois)
-
start_period : Période de grâce au démarrage où les échecs ne comptent pas (10 secondes)
depends_on:
database:
condition: service_healthy # Attend que le healthcheck réussisseLes conditions disponibles :
service_started: Le conteneur a démarré (comportement par défaut)service_healthy: Le healthcheck réussitservice_completed_successfully: Le conteneur s'est terminé avec succès (code 0)
database:
image: postgres:15
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5database:
image: mysql:8
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 5s
retries: 5cache:
image: redis:7
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5database:
image: mongo:6
healthcheck:
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
interval: 10s
timeout: 5s
retries: 5api:
image: mon-api
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 10s
timeout: 5s
retries: 3Si curl n'est pas disponible dans l'image :
api:
image: mon-api
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/health"]
interval: 10s
timeout: 5s
retries: 3Une autre approche consiste à faire attendre votre application que le service soit prêt.
wait-for-it est un script bash populaire qui attend qu'un port TCP soit accessible.
services:
database:
image: postgres:15
environment:
POSTGRES_PASSWORD: secret
app:
image: mon-app
depends_on:
- database
command: >
sh -c "
./wait-for-it.sh database:5432 --timeout=60 --strict --
node server.js
"Ce qui se passe :
- Le script
wait-for-it.shattend que le port 5432 dedatabasesoit accessible - Timeout après 60 secondes si la connexion échoue
- Une fois prêt, lance
node server.js
Dans votre Dockerfile :
FROM node:18
# Installer wait-for-it
ADD https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh /usr/local/bin/wait-for-it.sh
RUN chmod +x /usr/local/bin/wait-for-it.sh
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["node", "server.js"]dockerize est un autre outil similaire mais plus puissant :
services:
database:
image: postgres:15
app:
image: mon-app
depends_on:
- database
command: >
dockerize
-wait tcp://database:5432
-timeout 60s
node server.jsLa meilleure solution est souvent d'implémenter une logique de retry directement dans votre application.
// config/database.js
const { Client } = require('pg');
async function connectWithRetry(maxRetries = 10, delay = 5000) {
for (let i = 0; i < maxRetries; i++) {
try {
const client = new Client({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME
});
await client.connect();
console.log('✅ Connecté à la base de données');
return client;
} catch (error) {
console.log(`❌ Tentative ${i + 1}/${maxRetries} échouée`);
if (i < maxRetries - 1) {
console.log(`⏳ Nouvelle tentative dans ${delay/1000}s...`);
await new Promise(resolve => setTimeout(resolve, delay));
} else {
throw new Error('Impossible de se connecter à la base de données');
}
}
}
}
module.exports = { connectWithRetry };# database.py
import psycopg2
import time
import os
def connect_with_retry(max_retries=10, delay=5):
for attempt in range(max_retries):
try:
connection = psycopg2.connect(
host=os.getenv('DB_HOST'),
port=os.getenv('DB_PORT'),
user=os.getenv('DB_USER'),
password=os.getenv('DB_PASSWORD'),
database=os.getenv('DB_NAME')
)
print('✅ Connecté à la base de données')
return connection
except psycopg2.OperationalError as e:
print(f'❌ Tentative {attempt + 1}/{max_retries} échouée')
if attempt < max_retries - 1:
print(f'⏳ Nouvelle tentative dans {delay}s...')
time.sleep(delay)
else:
raise Exception('Impossible de se connecter à la base de données')services:
app:
build: .
depends_on:
- database
environment:
DB_HOST: database
DB_PORT: 5432
DB_USER: postgres
DB_PASSWORD: secret
# L'application gère elle-même l'attente
database:
image: postgres:15
environment:
POSTGRES_PASSWORD: secretAvantages de cette approche :
- ✅ Fonctionne même si la base de données redémarre pendant l'exécution
- ✅ Pas de dépendance externe (wait-for-it, dockerize)
- ✅ Plus robuste en production
- ✅ Logs clairs pour le debugging
Une approche simple : laisser le conteneur redémarrer automatiquement s'il échoue :
services:
app:
image: mon-app
depends_on:
- database
restart: on-failure
environment:
DB_HOST: database
database:
image: postgres:15
restart: alwaysComment ça fonctionne :
appdémarre et essaie de se connecter àdatabase- Si
databasen'est pas prête,appcrash - Docker redémarre automatiquement
appgrâce àrestart: on-failure - Après quelques tentatives,
databaseest prête etappse connecte
Vous pouvez créer un service temporaire qui attend que tout soit prêt :
services:
# Service d'initialisation
wait-for-db:
image: postgres:15
command: >
sh -c "
until pg_isready -h database -U postgres; do
echo 'En attente de la base de données...';
sleep 2;
done;
echo 'Base de données prête !';
"
depends_on:
- database
# Base de données
database:
image: postgres:15
environment:
POSTGRES_PASSWORD: secret
# Application
app:
image: mon-app
depends_on:
wait-for-db:
condition: service_completed_successfullyservices:
# Niveau 1 : Base de données
database:
image: postgres:15
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 5s
# Niveau 2 : Migrations
migrations:
image: flyway/flyway
depends_on:
database:
condition: service_healthy
command: migrate
# Niveau 3 : API
api:
image: mon-api
depends_on:
migrations:
condition: service_completed_successfully
database:
condition: service_healthy
# Niveau 4 : Worker
worker:
image: mon-worker
depends_on:
api:
condition: service_startedOrdre de démarrage : database → migrations → api → worker
services:
# L'application dépend de plusieurs services
app:
image: mon-app
depends_on:
database:
condition: service_healthy
cache:
condition: service_healthy
queue:
condition: service_healthy
database:
image: postgres:15
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 5s
cache:
image: redis:7
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
queue:
image: rabbitmq:3
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "ping"]
interval: 10sCe qui se passe : database, cache et queue démarrent en parallèle, puis app démarre quand tous sont healthy.
Voici un exemple qui combine plusieurs techniques :
services:
# Base de données principale
database:
image: postgres:15
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: production
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s
restart: always
# Cache Redis
cache:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
restart: always
# Migration de base de données
migrate:
image: migrate/migrate
depends_on:
database:
condition: service_healthy
volumes:
- ./migrations:/migrations
command: >
-path=/migrations
-database postgres://postgres:${DB_PASSWORD}@database:5432/production?sslmode=disable
up
# API Backend
api:
build: ./api
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://postgres:${DB_PASSWORD}@database:5432/production
REDIS_URL: redis://cache:6379
NODE_ENV: production
depends_on:
database:
condition: service_healthy
cache:
condition: service_healthy
migrate:
condition: service_completed_successfully
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s
restart: unless-stopped
# Worker de tâches en arrière-plan
worker:
build: ./worker
environment:
DATABASE_URL: postgres://postgres:${DB_PASSWORD}@database:5432/production
REDIS_URL: redis://cache:6379
depends_on:
database:
condition: service_healthy
cache:
condition: service_healthy
api:
condition: service_healthy
restart: unless-stopped
# Frontend
frontend:
build: ./frontend
ports:
- "80:80"
depends_on:
api:
condition: service_healthy
restart: unless-stopped
volumes:
postgres-data:Points clés de cet exemple :
- Tous les services critiques ont des health checks
- Les migrations s'exécutent avant l'API
- L'API démarre après la base de données ET les migrations
- Le worker attend que l'API soit healthy
- Le frontend attend que l'API soit healthy
- Politiques de restart appropriées pour chaque service
# Voir tous les services et leur état
docker compose ps
# Voir les logs d'un service spécifique
docker compose logs database
# Suivre les logs en temps réel
docker compose logs -f api
# Voir les logs de tous les services
docker compose logs -f# Voir l'état de santé des conteneurs
docker compose ps
# Inspecter un conteneur pour voir les détails du health check
docker inspect <container-name> | grep -A 10 Health# Se connecter à un conteneur
docker compose exec api sh
# Tester la connexion à la base de données depuis l'intérieur
ping database
nc -zv database 5432 services:
app:
image: mon-app
depends_on:
- database
restart: on-failure
database:
image: postgres:15Simple et suffisant pour le développement local.
services:
app:
image: mon-app
depends_on:
database:
condition: service_healthy
restart: unless-stopped
database:
image: postgres:15
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 5sPlus robuste avec health checks.
services:
app:
image: mon-app
depends_on:
database:
condition: service_healthy
cache:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s
restart: always
database:
image: postgres:15
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 5s
timeout: 5s
retries: 5
restart: alwaysHealth checks partout + retry logic dans le code.
-
Toujours définir des health checks pour les bases de données
database: healthcheck: test: ["CMD-SHELL", "pg_isready"]
-
Implémenter la logique de retry dans votre code
- Plus robuste que depends_on seul
- Gère les redémarrages en production
-
Utiliser start_period pour les services lents
healthcheck: start_period: 30s # 30s de grâce au démarrage
-
Définir des politiques de restart appropriées
restart: unless-stopped # En production restart: on-failure # En développement
-
Logger clairement les tentatives de connexion
- Aide au debugging
- Montre la progression
-
Ne pas utiliser sleep avec un délai fixe
# ❌ Mauvais command: sh -c "sleep 10 && node app.js" # Trop court = échec, trop long = perte de temps
-
Ne pas ignorer les dépendances
# ❌ Risqué app: image: mon-app # Pas de depends_on !
-
Ne pas utiliser depends_on sans condition pour les services critiques
# ❌ Insuffisant en production depends_on: - database # Service started seulement # ✅ Mieux depends_on: database: condition: service_healthy
✅ depends_on seul n'attend que le démarrage du conteneur, pas qu'il soit prêt
✅ Les health checks sont la meilleure solution pour vérifier qu'un service est réellement prêt
✅ La condition: service_healthy dans depends_on attend que le health check réussisse
✅ Implémenter une logique de retry dans votre code est la solution la plus robuste
✅ Les scripts comme wait-for-it sont utiles mais pas indispensables
✅ Utilisez restart: on-failure ou restart: unless-stopped pour plus de résilience
✅ Définissez des health checks pour tous les services critiques (bases de données, APIs)
✅ Adaptez votre stratégie selon l'environnement (dev, staging, production)
✅ Loggez clairement les tentatives de connexion pour faciliter le debugging
✅ Combinez plusieurs techniques pour une solution robuste
Prochaine section : Nous allons explorer les commandes Docker Compose essentielles pour gérer votre application au quotidien !