Docker pour les devs - 18 - ENTRYPOINT, CMD et scripts d'initialisation

ENTRYPOINT vs CMD en pratique, scripts d'init, wait-for-it, migrations au démarrage et le trick exec $@.

  1. 01 Docker pour les devs - 00 - Pourquoi Docker change tout
  2. 02 Docker pour les devs - 01 - Containers vs VMs
  3. 03 Docker pour les devs - 02 - L'architecture de Docker
  4. 04 Docker pour les devs - 03 - Docker Desktop, Engine et alternatives
  5. 05 Docker pour les devs - 04 - Écrire un Dockerfile
  6. 06 Docker pour les devs - 05 - Layers et cache
  7. 07 Docker pour les devs - 06 - Le .dockerignore
  8. 08 Docker pour les devs - 07 - Multi-stage builds
  9. 09 Docker pour les devs - 08 - Choisir son image de base
  10. 10 Docker pour les devs - 09 - Docker Compose, les bases
  11. 11 Docker pour les devs - 10 - Docker Compose avance
  12. 12 Docker pour les devs - 11 - Networking Docker, les bases
  13. 13 Docker pour les devs - 12 - Networking Docker avance
  14. 14 Docker pour les devs - 13 - Volumes et persistance
  15. 15 Docker pour les devs - 14 - Variables d'environnement et secrets
  16. 16 Docker pour les devs - 15 - Permissions et utilisateurs
  17. 17 Docker pour les devs - 16 - Docker et monorepo
  18. 18 Docker pour les devs - 17 - Dev vs Prod
  19. 19 Docker pour les devs - 18 - ENTRYPOINT, CMD et scripts d'initialisation
  20. 20 Docker pour les devs - 19 - Debugger ses conteneurs
  21. 21 Docker pour les devs - 20 - Bases de donnees dans Docker
  22. 22 Docker pour les devs - 21 - Sauvegardes et restauration
  23. 23 Docker pour les devs - 22 - Conteneuriser un frontend
  24. 24 Docker pour les devs - 23 - Sécurité des conteneurs
  25. 25 Docker pour les devs - 24 - Optimisation des images
  26. 26 Docker pour les devs - 25 - Builds multi-platform
  27. 27 Docker pour les devs - 26 - Limiter les ressources de tes conteneurs
  28. 28 Docker pour les devs - 27 - Gerer les logs comme un adulte
  29. 29 Docker pour les devs - 28 - Healthchecks et restart policies
  30. 30 Docker pour les devs - 29 - Nettoyer Docker avant qu'il mange ton disque
  31. 31 Docker pour les devs - 30 - Registries et stratégie de tags
  32. 32 Docker pour les devs - 31 - Docker en CI/CD
  33. 33 Docker pour les devs - 32 - Au-dela de Compose
  34. 34 Docker pour les devs - 33 - Glossaire Docker de A a Z

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

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.