18 - ENTRYPOINT, CMD et scripts d'initialisation
Ce que tu vas apprendre
- La vraie différence entre ENTRYPOINT et CMD (et quand utiliser lequel)
- Écrire un script d'entrypoint propre avec
exec "$@" - Attendre qu'une base de donnees soit prete avec wait-for-it
- Lancer des migrations au démarrage du conteneur
- Le pattern init containers
Prerequisites
Avoir suivi les articles précédents, notamment la partie sur les Dockerfiles multi-stage. Savoir écrire un Dockerfile basique.
J'ai passe des mois a confondre ENTRYPOINT et CMD. La doc officielle n'aide pas vraiment, parce qu'elle te donne un tableau avec des combinaisons sans te dire quand utiliser quoi en vrai. Alors je vais te donner ma regle simple.
ENTRYPOINT vs CMD : la regle du bar
CMD, c'est la commande par défaut. Si tu lances docker run mon-image, ca exécuté le CMD. Mais si tu ajoutes quelque chose apres (docker run mon-image sh), ca remplace le CMD.
ENTRYPOINT, c'est le programme principal. Il ne se fait pas remplacer facilement. Ce que tu passes apres docker run mon-image devient des arguments de l'ENTRYPOINT.
dockerfile# CMD seul : commande par defaut, facile a remplacer
FROM node:20-alpine
CMD ["node", "server.js"]
# docker run mon-image -> node server.js
# docker run mon-image sh -> sh
dockerfile# ENTRYPOINT + CMD : le combo classique
FROM node:20-alpine
ENTRYPOINT ["node"]
CMD ["server.js"]
# docker run mon-image -> node server.js
# docker run mon-image worker.js -> node worker.js
Ma regle : si ton conteneur fait une seule chose (un serveur, un worker), utilise CMD. Si tu veux un script d'initialisation avant de lancer la commande, utilise ENTRYPOINT avec un script.
Le script d'entrypoint
C'est là où ca devient utile. Tu veux faire des trucs avant de lancer ton app : vérifier des variables d'environnement, attendre la base, lancer des migrations. Un script d'entrypoint fait tout ca.
bash#!/bin/sh
set -e
echo "Verification des variables d'environnement..."
if [ -z "$DATABASE_URL" ]; then
echo "ERREUR: DATABASE_URL n'est pas definie"
exit 1
fi
echo "En attente de la base de donnees..."
until pg_isready -h "$DB_HOST" -p 5432 -q; do
echo "PostgreSQL pas encore pret, on attend..."
sleep 2
done
echo "Lancement des migrations..."
npx prisma migrate deploy
echo "Demarrage de l'application..."
exec "$@"
Le exec "$@" a la fin, c'est le trick. Il remplace le processus shell par la commande passee en argument (le CMD du Dockerfile). Sans ca, ton app tourne comme un fils du shell, et les signaux (SIGTERM) ne lui arrivent pas directement. C'est une source de bugs subtils ou ton conteneur met 10 secondes a s'arrêter au lieu de s'eteindre proprement.
dockerfileFROM node:20-alpine
WORKDIR /app
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
COPY . .
RUN npm ci --omit=dev
ENTRYPOINT ["/entrypoint.sh"]
CMD ["node", "server.js"]
wait-for-it et wait-for
Le pg_isready c'est bien pour PostgreSQL, mais si tu veux un outil générique qui attend qu'un port soit ouvert, il y a wait-for-it et wait-for.
bash#!/bin/sh
set -e
# wait-for : version legere en sh pur
/wait-for db:5432 --timeout=30
# Ou avec wait-for-it (plus complet, en bash)
/wait-for-it.sh db:5432 --timeout=30 --strict
exec "$@"
Sur paltemps.fr, j'utilise une boucle until maison parce que je n'ai que PostgreSQL a attendre. Mais sur des projets avec Redis, RabbitMQ et trois microservices, wait-for-it simplifie la vie.
Tu peux aussi gerer ca dans ton docker-compose.yml avec depends_on et des healthchecks, comme on l'a vu dans l'article sur le debug. Mais le script d'entrypoint reste utile pour les migrations.
Migrations au démarrage
Lancer les migrations dans l'entrypoint, c'est un choix. Certains preferent un job séparé. Moi je trouve ca pratique pour les petits projets :
bash#!/bin/sh
set -e
/wait-for db:5432 --timeout=30
# Migrations uniquement si la variable est definie
if [ "$RUN_MIGRATIONS" = "true" ]; then
echo "Lancement des migrations..."
npx prisma migrate deploy
fi
exec "$@"
Le flag RUN_MIGRATIONS te permet de contrôler ca. En prod, tu le mets sur un seul replica. Tu ne veux pas que trois instances lancent les migrations en parallèle.
Le pattern init containers
L'idee vient de Kubernetes, mais tu peux l'appliquer avec Docker Compose. Un conteneur "init" qui tourne une fois pour preparer l'environnement, puis s'arrêté :
yamlservices:
init-db:
image: mon-app:latest
command: ["npx", "prisma", "migrate", "deploy"]
environment:
DATABASE_URL: postgres://user:pass@db:5432/app
depends_on:
db:
condition: service_healthy
app:
image: mon-app:latest
depends_on:
init-db:
condition: service_completed_successfully
environment:
DATABASE_URL: postgres://user:pass@db:5432/app
Le service_completed_successfully est disponible depuis Docker Compose v2. L'app ne démarré que quand le conteneur init a fini sans erreur. C'est plus propre que de tout mettre dans un script d'entrypoint.
Les pièges classiques
Le shell form vs exec form dans le Dockerfile. Toujours utiliser la forme exec (avec les crochets) :
dockerfile# Mauvais : shell form, lance un /bin/sh -c autour
ENTRYPOINT node server.js
# Bon : exec form, lance directement node
ENTRYPOINT ["node", "server.js"]
La forme shell empeche les signaux d'arriver a ton processus. Ton conteneur ne s'arrêté pas proprement et Docker le tue apres 10 secondes.
Autre piège : oublier le set -e dans ton script. Sans ca, une migration qui echoue ne bloque pas le démarrage, et ton app tourne avec une base dans un état bancal.
Résumé
- CMD est la commande par défaut, remplacable. ENTRYPOINT est le programme principal.
- Un script d'entrypoint te permet d'initialiser l'environnement avant de lancer l'app.
exec "$@"remplace le shell par la commande : les signaux arrivent bien au processus.- wait-for-it attend qu'un service soit pret avant de continuer.
- Les migrations au démarrage marchent bien pour les petits projets, le pattern init containers est plus propre pour les gros.
Article précédent : Docker 17 - Dev vs Prod Article suivant : Docker 19 - Debug