Docker pour les devs - 17 - Dev vs Prod

Le pattern compose.yml + override, multi-stage dev/prod, hot reload, builds optimises et un Makefile pour simplifier le tout.

  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

17 - Dev vs Prod

Ce que tu vas apprendre

  • Le pattern compose.yaml + compose.override.yaml
  • Les targets multi-stage pour dev et prod
  • Le hot reload en dev avec bind mounts et watch
  • Le build optimise pour la production
  • Les fichiers d'environnement par contexte
  • Un Makefile pour ne plus taper de commandes a rallonge

Prerequisites


En dev, tu veux du hot reload, des logs verbeux, un debugger attache, et des outils comme Adminer pour voir ta base. En prod, tu veux une image minimale, des logs structures, zero outil superflu, et un build optimise. Le defi, c'est de gerer les deux sans dupliquer toute la configuration.

Le pattern override

On en a parle dans l'article Compose avance, mais c'est ici qu'il prend tout son sens.

Le fichier compose.yaml contient la base commune :

yamlservices:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      DATABASE_URL: postgres://postgres:secret@db:5432/myapp
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: myapp
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  pgdata:

Le fichier compose.override.yaml ajoute les spécificités dev (charge automatiquement) :

yamlservices:
  app:
    build:
      target: development
    ports:
      - "3000:3000"
      - "9229:9229"     # debugger Node
    volumes:
      - ./src:/app/src
    command: node --inspect=0.0.0.0:9229 --watch src/index.js
    environment:
      NODE_ENV: development
      DEBUG: "true"

  adminer:
    image: adminer
    ports:
      - "8080:8080"

Pour la production, un fichier compose.prod.yaml :

yamlservices:
  app:
    build:
      target: production
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: production
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "1.0"
    restart: unless-stopped
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

  db:
    restart: unless-stopped

En dev, un simple docker compose up charge compose.yaml + compose.override.yaml. En prod :

bashdocker compose -f compose.yaml -f compose.prod.yaml up -d

Multi-stage : dev vs prod dans le meme Dockerfile

Un Dockerfile avec des stages nommes pour chaque environnement :

dockerfileFROM node:20-alpine AS base
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable

# --- Development ---
FROM base AS development
RUN pnpm install
COPY . .
# Pas de CMD ici : on le met dans compose.override.yaml
CMD ["node", "--watch", "src/index.js"]

# --- Build ---
FROM base AS build
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

# --- Production ---
FROM node:20-alpine AS production
WORKDIR /app
RUN addgroup -g 1001 appgroup && \
    adduser -u 1001 -G appgroup -D appuser

COPY --from=build --chown=appuser:appgroup /app/dist ./dist
COPY --from=build --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=build --chown=appuser:appgroup /app/package.json ./

USER appuser
EXPOSE 3000
CMD ["node", "dist/index.js"]

Le stage development garde toutes les devDependencies et le code source. Le stage production ne copie que le dist et les node_modules de production. L'image finale est plus petite et plus sécurisée (voir l'article sur les permissions).

Hot reload en dev

Deux approches pour le rechargement automatique :

Bind mounts + --watch

La méthode classique. Tu montes ton code source dans le conteneur et tu lances Node avec --watch :

yaml# compose.override.yaml
services:
  app:
    volumes:
      - ./src:/app/src
    command: node --watch src/index.js

Simple et efficace. Mais les bind mounts peuvent etre lents sur Docker Desktop (Mac surtout).

Docker Compose Watch

L'alternative moderne, vue dans Compose avance :

yaml# compose.override.yaml
services:
  app:
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
        - action: rebuild
          path: package.json
bashdocker compose watch

Watch copie les fichiers modifies dans le conteneur sans passer par un bind mount. C'est souvent plus rapide sur Mac et Windows.

Environnement par contexte

Sur paltemps.fr, j'ai trois fichiers d'environnement :

.env              # valeurs par defaut pour Compose (ports, versions)
.env.development  # variables passees aux conteneurs en dev
.env.production   # variables passees aux conteneurs en prod

Dans les fichiers Compose :

yaml# compose.override.yaml (dev)
services:
  app:
    env_file:
      - .env.development

# compose.prod.yaml
services:
  app:
    env_file:
      - .env.production

Les secrets de production ne sont jamais dans le repo. Ils viennent de Docker secrets ou d'un gestionnaire comme Vault (voir l'article sur les secrets).

Le Makefile pour simplifier

Taper docker compose -f compose.yaml -f compose.prod.yaml up -d --build a chaque fois, c'est penible. Un Makefile resout ca :

makefile.PHONY: dev prod build logs clean

dev:
	docker compose up -d --build

dev-logs:
	docker compose logs -f

prod:
	docker compose -f compose.yaml -f compose.prod.yaml up -d --build

build:
	docker compose -f compose.yaml -f compose.prod.yaml build

logs:
	docker compose logs -f app

ps:
	docker compose ps

clean:
	docker compose down -v

restart:
	docker compose restart app

shell:
	docker compose exec app sh

db-shell:
	docker compose exec db psql -U postgres myapp

Maintenant :

bashmake dev        # lance l'environnement de dev
make prod       # lance la prod
make logs       # affiche les logs
make shell      # ouvre un shell dans l'app
make clean      # supprime tout

C'est quatre caractères au lieu de quarante. Et ca documente implicitement les commandes du projet. Un nouveau développeur ouvre le Makefile et comprend comment lancer le projet.

Exemple complet : la structure finale

project/
  docker/
    Dockerfile          # ou a la racine
  src/
    index.js
  compose.yaml          # config commune
  compose.override.yaml # dev (charge automatiquement)
  compose.prod.yaml     # production
  .env                  # variables Compose
  .env.development      # variables dev
  .env.production       # variables prod (pas dans git)
  .dockerignore
  Makefile
  package.json

Le .env.production est dans le .gitignore. Les secrets ne sont jamais commites.

La checklist dev vs prod

Aspect Dev Prod
Stage Dockerfile development production
Source code bind mount copie dans l'image
node_modules toutes (devDeps incluses) production uniquement
Utilisateur root (pratique) non-root (sécurité)
Logs stdout verbeux json-file avec rotation
Restart policy aucune unless-stopped
Resources illimitees limitees (memory, CPU)
Outils adminer, mailhog rien de superflu

Résumé

  • compose.override.yaml pour le dev, compose.prod.yaml pour la prod
  • Le multi-stage Dockerfile avec des targets nommes évité la duplication
  • Bind mounts ou watch pour le hot reload en dev
  • L'image de prod ne contient que le strict nécessaire : code compile, deps de prod, utilisateur non-root
  • Un Makefile transforme des commandes de 40 caractères en 4

Navigation : Precedent : 16 - Docker et monorepo | Suivant : 18 - Init et entrypoints


Sources

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