Profils Docker Compose : piles Dev/Prod sans duplication YAML

Cet article vous a aidé ?

Vous connaissez la scène : un dépôt avec docker-compose.yml, docker-compose.dev.yml, docker-compose.prod.yml,
et encore un fichier de plus que quelqu’un a créé “temporairement” pendant un incident. Puis un an passe. Maintenant votre pile “dev” active par erreur
le proxy inverse de niveau production, ou la prod exécute en silence l’image de debug parce que la chaîne d’overrides est hantée.

Les profils Compose sont la réponse mature : un seul fichier Compose, plusieurs piles, comportement prévisible. Moins d’archéologie YAML, moins de surprises du type « ça marche sur ma machine »,
et beaucoup moins de retours en arrière le vendredi soir.

Ce que sont (et ne sont pas) les profils Compose

Un profil Compose est une étiquette que vous attachez à un service (et parfois à d’autres ressources) pour qu’il ne démarre que lorsque ce profil est activé.
C’est essentiellement une inclusion conditionnelle. Le fichier Compose reste un modèle cohérent ; les profils décident quelles parties sont actives lors d’un lancement.

Voici le modèle mental central :

  • Sans profils : exécuter docker compose up démarre tous les services du fichier (sous réserve des dépendances).
  • Avec profils : les services marqués par des profils sont exclus sauf si ce profil est activé via
    --profile ou COMPOSE_PROFILES.
  • Services par défaut : les services sans entrée profiles: se comportent comme « toujours activés ».

Ce que les profils ne sont pas : un système de templating complet, un gestionnaire de secrets, ou un substitut à un outil de déploiement approprié. Ils ne vous empêcheront pas
de faire quelque chose d’imprudent ; ils rendent simplement cela plus difficile à faire par accident.

Conseil d’opinion : considérez les profils comme des verrous fonctionnels pour la topologie d’exécution. Utilisez-les pour ajouter/supprimer des sidecars, des outils,
des dépendances réservées au dev, et des aides opérationnelles. Ne les utilisez pas pour masquer des architectures de production fondamentalement différentes. Si la prod tourne
sur Kubernetes et le dev sur Compose, d’accord — les profils restent utiles en dev et pour la validation locale proche de la prod. Mais ne prétendez pas que les profils Compose
rendent dev identique à prod. Ils rendent les choses disciplinées.

Une citation pour garder la tête froide lors du prochain débat « on le déploie tout de suite » :
L’espoir n’est pas une stratégie. — General Gordon R. Sullivan

Blague n°1 : Si vos fichiers Compose dev et prod divergent trop longtemps, ils finiront par faire des déclarations fiscales séparées.

Faits et histoire : pourquoi les profils existent

Les profils paraissent évidents aujourd’hui, mais ils répondent à des années de réalité désordonnée. Un peu de contexte aide à comprendre les aspérités.

8 faits concrets qui comptent en pratique

  1. Compose a commencé avec Fig (époque 2013–2014) : il a été conçu pour des applis multi-conteneurs locales, pas pour le déploiement en entreprise.
    Les profils sont une concession ultérieure à la manière dont les gens l’utilisaient réellement.
  2. Les fichiers d’override sont devenus la solution par défaut : docker-compose.override.yml était une fonctionnalité pratique,
    et elle a involontairement entraîné des équipes à forker la configuration sans fin.
  3. Les profils sont arrivés pour réduire la prolifération YAML : ils permettent à un seul fichier de représenter plusieurs formes sans une pile d’overrides.
  4. Compose V2 est passé dans l’CLI Docker : docker compose (espace) a remplacé docker-compose (tiret)
    pour la plupart des installations modernes. Les profils y sont beaucoup plus uniformément pris en charge.
  5. Les profils sont résolus côté client : le CLI Compose décide quoi créer. L’Engine n’est pas conscient de votre intention.
    Cela signifie que la « source de vérité » est la configuration Compose que vous avez réellement exécutée.
  6. Les profils interagissent avec les dépendances de façon non triviale : un service avec un profil peut être inclus parce qu’un autre service
    en dépend (selon la façon dont vous démarrez les choses). Vous devez tester vos chemins de démarrage.
  7. La dérive multi-environnements est un problème de disponibilité : les fichiers YAML dupliqués ne font pas que perdre du temps — ils créent des inconnues
    qui apparaissent pendant les incidents.
  8. Les profils s’associent bien avec des conteneurs « outils opérationnels » : jobs de sauvegarde, runners de migration, expéditeurs de logs et UIs admin peuvent être
    opt-in sans infecter votre pile par défaut.

Principes de conception : structurer une pile dev/prod en un seul fichier

Un seul fichier Compose peut être propre ou maudit. Les profils ne vous sauvent pas si vous concevez pour le chaos. Conceptez pour la prévisibilité à la place.

1) Séparez les services « toujours actifs » des services « contextuels »

Placez votre appli, sa base de données et ce qui est nécessaire au démarrage dans l’ensemble par défaut (sans profil).
Placez les luxes développeur (rechargement à chaud, UIs admin, SMTP factice, S3 local, shells de debug) derrière dev.
Placez les choix infra réservés à la production (bord TLS réel, règles de reverse proxy type WAF, forwarders de logs) derrière prod ou ops.

2) Gardez les ports simples, stables et intentionnels

En dev, vous publiez probablement des ports vers l’hôte. En prod, vous ne le faites souvent pas ; vous attachez à un réseau et laissez un reverse proxy gérer l’ingress.
Utilisez les profils pour éviter les incidents du type « la prod lie accidentellement 0.0.0.0:5432 ».

3) Préférez les volumes nommés ; rendez la persistance explicite

Le stockage est l’endroit où les différences dev/prod deviennent perte de données. Les volumes nommés conviennent pour le local, mais la prod doit utiliser des chemins montés ou un driver de volume managé
et des workflows de sauvegarde/restauration clairement définis.

4) Traitez les variables d’environnement comme une API, pas comme un fourre-tout

Utilisez des fichiers .env, mais ne les laissez pas devenir un second langage de configuration. Utilisez des valeurs par défaut explicites, documentez les variables requises,
et validez-les dans votre entrypoint si l’appli vous appartient.

5) Compose n’est pas un orchestrateur ; ne jouez pas à ça

Compose peut redémarrer des conteneurs, effectuer des healthchecks et définir des dépendances. Ce n’est pas de l’ordonnancement multi-nœuds, ni des déploiements progressifs, ni la gestion
des secrets à grande échelle. Utilisez-le comme un « lanceur de pile » fiable. Si vous avez besoin de plus, évoluez — ne colmatez pas tout avec une pile de scripts jusqu’à recréer un Kubernetes pire.

Blague n°2 : « Encore un fichier override » est la manière d’invoquer les poltergeists YAML.

Fichier Compose de référence utilisant des profils (dev/prod/ops)

Voici une base réaliste : une application web, une base Postgres, un cache, et des helpers optionnels. Le but n’est pas d’être sophistiqué.
Le but est d’être difficile à mal utiliser.

cr0x@server:~$ cat compose.yml
services:
  app:
    image: ghcr.io/acme/demo-app:1.8.2
    environment:
      APP_ENV: ${APP_ENV:-dev}
      DATABASE_URL: postgres://app:${POSTGRES_PASSWORD:-devpass}@db:5432/app
      REDIS_URL: redis://redis:6379/0
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    networks: [backend]
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8080/healthz"]
      interval: 10s
      timeout: 2s
      retries: 12

  db:
    image: postgres:16
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-devpass}
    volumes:
      - db_data:/var/lib/postgresql/data
    networks: [backend]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 5s
      timeout: 2s
      retries: 20

  redis:
    image: redis:7
    command: ["redis-server", "--save", "", "--appendonly", "no"]
    networks: [backend]

  # Dev-only: bind ports, live reload, friendly tools
  app-dev:
    profiles: ["dev"]
    image: ghcr.io/acme/demo-app:1.8.2
    environment:
      APP_ENV: dev
      LOG_LEVEL: debug
      DATABASE_URL: postgres://app:${POSTGRES_PASSWORD:-devpass}@db:5432/app
      REDIS_URL: redis://redis:6379/0
    command: ["./run-dev.sh"]
    volumes:
      - ./src:/app/src:delegated
    ports:
      - "8080:8080"
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    networks: [backend]

  mailhog:
    profiles: ["dev"]
    image: mailhog/mailhog:v1.0.1
    ports:
      - "8025:8025"
    networks: [backend]

  adminer:
    profiles: ["dev"]
    image: adminer:4
    ports:
      - "8081:8080"
    networks: [backend]

  # Prod-ish: reverse proxy and tighter exposure
  edge:
    profiles: ["prod"]
    image: nginx:1.27
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
    ports:
      - "80:80"
    depends_on:
      app:
        condition: service_healthy
    networks: [frontend, backend]

  # Ops-only: migrations and backups
  migrate:
    profiles: ["ops"]
    image: ghcr.io/acme/demo-app:1.8.2
    command: ["./migrate.sh"]
    environment:
      APP_ENV: ${APP_ENV:-prod}
      DATABASE_URL: postgres://app:${POSTGRES_PASSWORD}@db:5432/app
    depends_on:
      db:
        condition: service_healthy
    networks: [backend]

  pg-backup:
    profiles: ["ops"]
    image: postgres:16
    environment:
      PGPASSWORD: ${POSTGRES_PASSWORD}
    entrypoint: ["/bin/sh", "-lc"]
    command: >
      pg_dump -h db -U app -d app
      | gzip -c
      > /backup/app-$(date +%F_%H%M%S).sql.gz
    volumes:
      - ./backup:/backup
    depends_on:
      db:
        condition: service_healthy
    networks: [backend]

networks:
  frontend: {}
  backend: {}

volumes:
  db_data: {}

Ce que cette structure vous apporte

  • Le défaut est sûr : app, db, redis s’exécutent sans exposition de ports hôte par défaut.
  • Le dev est ergonomique : activez dev pour obtenir rechargement à chaud, test d’e-mails et Adminer.
  • La prod est contrôlée : activez prod pour ajouter un proxy d’entrée ; toujours pas de ports dev aléatoires.
  • Les ops sont explicites : migrations et sauvegardes ne sont pas « toujours en cours » ; elles sont invoquées intentionnellement.

Remarquez la duplication délibérée : app et app-dev sont des services séparés. Ce n’est pas de la paresse.
C’est une frontière de sécurité. Le service dev lie des ports et monte le code source ; le service de type prod ne le fait pas.
Vous pouvez partager un tag d’image tout en séparant le comportement d’exécution.

Tâches pratiques : 12+ commandes réelles, sorties et décisions

Ci-dessous des mouvements opérationnels concrets que vous utiliserez réellement. Chacun a : une commande, ce que signifie une sortie typique, et la décision suivante à prendre.
Exécutez-les depuis la racine du dépôt où se trouve compose.yml.

Tâche 1 : Vérifier que votre Compose supporte les profils (et quelle version vous exécutez)

cr0x@server:~$ docker compose version
Docker Compose version v2.27.0

Signification : Compose V2 est installé. Les profils sont pris en charge.
Si vous voyez « command not found » ou un binaire v1 ancien, attendez-vous à un comportement incohérent.

Décision : Standardisez sur docker compose au sein de votre équipe/CI. Mélanger v1/v2 est la voie vers des tickets « mais ça marchait hier ».

Tâche 2 : Rendre la configuration effective pour un profil (attraper les surprises avant de démarrer les conteneurs)

cr0x@server:~$ docker compose -f compose.yml --profile dev config
services:
  adminer:
    image: adminer:4
    networks:
      backend: null
    ports:
    - mode: ingress
      target: 8080
      published: "8081"
      protocol: tcp
  app:
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    environment:
      APP_ENV: dev
      DATABASE_URL: postgres://app:devpass@db:5432/app
      REDIS_URL: redis://redis:6379/0
    healthcheck:
      test:
      - CMD
      - curl
      - -fsS
      - http://localhost:8080/healthz
    image: ghcr.io/acme/demo-app:1.8.2
    networks:
      backend: null
  app-dev:
    command:
    - ./run-dev.sh
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    environment:
      APP_ENV: dev
      DATABASE_URL: postgres://app:devpass@db:5432/app
      LOG_LEVEL: debug
      REDIS_URL: redis://redis:6379/0
    image: ghcr.io/acme/demo-app:1.8.2
    networks:
      backend: null
    ports:
    - mode: ingress
      target: 8080
      published: "8080"
      protocol: tcp
    volumes:
    - type: bind
      source: /home/cr0x/src
      target: /app/src
      bind:
        create_host_path: true
  db:
    environment:
      POSTGRES_DB: app
      POSTGRES_PASSWORD: devpass
      POSTGRES_USER: app
    healthcheck:
      test:
      - CMD-SHELL
      - pg_isready -U app -d app
    image: postgres:16
    networks:
      backend: null
    volumes:
    - type: volume
      source: db_data
      target: /var/lib/postgresql/data
  mailhog:
    image: mailhog/mailhog:v1.0.1
    networks:
      backend: null
    ports:
    - mode: ingress
      target: 8025
      published: "8025"
      protocol: tcp
  redis:
    command:
    - redis-server
    - --save
    - ""
    - --appendonly
    - "no"
    image: redis:7
    networks:
      backend: null
networks:
  backend: {}
volumes:
  db_data: {}

Signification : Vous voyez exactement quels services apparaissent sous dev. C’est votre contrôle de sanity pour une exposition de port accidentelle,
des variables d’environnement manquantes ou des images incorrectes.

Décision : Si la configuration rendue inclut quelque chose que vous n’aviez pas l’intention d’avoir, corrigez le fichier Compose avant de l’exécuter. Ne déboguez pas une faute de configuration au runtime.

Tâche 3 : Démarrer la pile par défaut sûre (aucun profil activé)

cr0x@server:~$ docker compose -f compose.yml up -d
[+] Running 4/4
 ✔ Network server_backend  Created
 ✔ Volume "server_db_data" Created
 ✔ Container server-db-1   Started
 ✔ Container server-redis-1 Started
 ✔ Container server-app-1  Started

Signification : Seuls les services par défaut ont démarré. Pas d’outils dev, pas de proxy d’entrée.

Décision : Utilisez ceci comme base pour les tests de fumée CI et les exécutions locales « proche de la prod ». Plus c’est ennuyeux, mieux ça se comporte lors des incidents.

Tâche 4 : Démarrer explicitement l’expérience dev

cr0x@server:~$ docker compose -f compose.yml --profile dev up -d
[+] Running 3/3
 ✔ Container server-mailhog-1 Started
 ✔ Container server-adminer-1 Started
 ✔ Container server-app-dev-1 Started

Signification : Compose a ajouté uniquement les services du profil dev ; les services par défaut étaient déjà en cours d’exécution.

Décision : Faites de « dev est opt-in » une règle d’équipe. Si quelqu’un veut des ports de debug en prod, il doit l’annoncer à voix haute avec un flag de profil.

Tâche 5 : Prouver quels profils sont activés (utile dans les logs CI)

cr0x@server:~$ COMPOSE_PROFILES=prod docker compose -f compose.yml config --profiles
prod

Signification : Le CLI reconnaît quel(s) profil(s) seront pris en compte. C’est une petite astuce qui prévient de grandes incompréhensions.

Décision : En CI, affichez les profils effectifs en haut du job. Vous vous éviterez un incident plus tard.

Tâche 6 : Lister les conteneurs du projet et repérer les services de profil

cr0x@server:~$ docker compose -f compose.yml ps
NAME              IMAGE                         COMMAND                  SERVICE    STATUS          PORTS
server-adminer-1   adminer:4                     "entrypoint.sh php …"   adminer    running         0.0.0.0:8081->8080/tcp
server-app-1       ghcr.io/acme/demo-app:1.8.2   "./start.sh"            app        running (healthy)
server-app-dev-1   ghcr.io/acme/demo-app:1.8.2   "./run-dev.sh"          app-dev    running         0.0.0.0:8080->8080/tcp
server-db-1        postgres:16                   "docker-entrypoint…"    db         running (healthy) 5432/tcp
server-mailhog-1   mailhog/mailhog:v1.0.1        "MailHog"               mailhog    running         0.0.0.0:8025->8025/tcp
server-redis-1     redis:7                       "docker-entrypoint…"    redis      running         6379/tcp

Signification : Vous voyez quels services tournent et quels ports sont publiés. La colonne PORTS est votre audit « qu’avons-nous exposé ? ».

Décision : Si vous voyez des ports publiés dans des environnements où ils ne devraient pas l’être, arrêtez-vous et corrigez le fichier. Ne normalisez pas une exposition accidentelle.

Tâche 7 : Confirmer pourquoi un service ne démarre pas (vérification dépendance et healthcheck)

cr0x@server:~$ docker compose -f compose.yml logs --no-log-prefix --tail=30 app
curl: (7) Failed to connect to localhost port 8080: Connection refused

Signification : Le healthcheck échoue. Soit l’application n’écoute pas, soit elle écoute sur un autre port, soit elle plante avant le bind.

Décision : Vérifiez docker compose logs app pour les erreurs de démarrage, puis docker exec dans le conteneur pour valider le port d’écoute.
Ne touchez pas encore à la base de données ; la plupart des échecs de healthcheck applicatif sont des problèmes de configuration applicative, pas de stockage.

Tâche 8 : Inspecter les variables d’environnement effectives (trouver vite le problème du « .env incorrect »)

cr0x@server:~$ docker compose -f compose.yml exec -T app env | egrep 'APP_ENV|DATABASE_URL|REDIS_URL'
APP_ENV=dev
DATABASE_URL=postgres://app:devpass@db:5432/app
REDIS_URL=redis://redis:6379/0

Signification : Le conteneur voit les valeurs que vous pensez qu’il voit. Si le mot de passe manque ou est vide, votre .env n’est pas chargé ou le nom de variable est erroné.

Décision : Si les variables d’environnement sont incorrectes, corrigez le côté appelant (votre export shell, l’injection de secrets CI, ou le fichier Compose). Ne « hotfixez » pas en modifiant les conteneurs.

Tâche 9 : Identifier la dérive d’images entre services dev et prod

cr0x@server:~$ docker compose -f compose.yml images
CONTAINER         REPOSITORY                   TAG     IMAGE ID       SIZE
server-app-1      ghcr.io/acme/demo-app        1.8.2   7a1d0f2c9a33   212MB
server-app-dev-1  ghcr.io/acme/demo-app        1.8.2   7a1d0f2c9a33   212MB
server-db-1       postgres                     16      5e2c6e1e12b8   435MB
server-redis-1    redis                        7       1c90a3f8e3a4   118MB

Signification : Les deux services app utilisent le même ID d’image. C’est bon : votre comportement dev diffère par commande/volumes/ports, pas par code non tracké.

Décision : Si les ID d’image diffèrent de manière inattendue, décidez si c’est intentionnel. Si ce n’est pas le cas, unifiez les tags ou cessez de prétendre que les environnements sont comparables.

Tâche 10 : Prouver quels services font réellement partie d’un profil (utile lors de refactors)

cr0x@server:~$ docker compose -f compose.yml config --services
adminer
app
app-dev
db
edge
mailhog
migrate
pg-backup
redis

Signification : Ceci liste tous les services du fichier, y compris ceux conditionnés par des profils. Vous pouvez maintenant vérifier la propriété et supprimer le poids mort.

Décision : Si personne ne peut expliquer pourquoi un service existe, supprimez-le ou placez-le derrière un profil ops et exigez une invocation explicite.

Tâche 11 : Démarrer le profil prod localement sans exposition dev

cr0x@server:~$ COMPOSE_PROFILES=prod docker compose -f compose.yml up -d
[+] Running 1/1
 ✔ Container server-edge-1  Started

Signification : Seul le service edge a été ajouté ; les services par défaut étaient déjà présents.

Décision : Utilisez ceci pour valider les modifications de configuration nginx avec la même app/db que vous utilisez ailleurs, sans ramener les outils dev.

Tâche 12 : Exécuter des jobs ops ponctuels sans laisser de conteneurs zombies

cr0x@server:~$ COMPOSE_PROFILES=ops docker compose -f compose.yml run --rm migrate
Running migrations...
Migrations complete.

Signification : Le conteneur de migration a exécuté et a été supprimé. Pas de service de longue durée, pas de redémarrages surprises.

Décision : Gardez les « actions ops » comme jobs run --rm. Si vos migrations tournent en service permanent, vous vous créez un pager auto-infligé.

Tâche 13 : Faire une sauvegarde avec le profil ops et valider que le fichier existe

cr0x@server:~$ COMPOSE_PROFILES=ops docker compose -f compose.yml run --rm pg-backup
cr0x@server:~$ ls -lh backup | tail -n 2
-rw-r--r-- 1 cr0x cr0x  38M Jan  3 01:12 app-2026-01-03_011230.sql.gz

Signification : La sauvegarde est arrivée sur le système de fichiers hôte. C’est la différence entre « on a des sauvegardes » et « on a une histoire rassurante ».

Décision : Si le fichier n’est pas là, ne poursuivez pas de changements risqués. Corrigez d’abord les montages/permissions. Les sauvegardes qui ne restaurent pas ne sont que de l’art performance.

Tâche 14 : Détecter les collisions de ports avant d’accuser Docker

cr0x@server:~$ ss -ltnp | egrep ':8080|:8081|:8025' || true
LISTEN 0      4096         0.0.0.0:8080      0.0.0.0:*    users:(("docker-proxy",pid=22419,fd=4))
LISTEN 0      4096         0.0.0.0:8081      0.0.0.0:*    users:(("docker-proxy",pid=22455,fd=4))
LISTEN 0      4096         0.0.0.0:8025      0.0.0.0:*    users:(("docker-proxy",pid=22501,fd=4))

Signification : Les ports hôtes sont déjà liés par des processus docker-proxy. Si votre prochain up échoue avec « port is already allocated », c’est la raison.

Décision : Arrêtez la pile concurrente ou changez les ports publiés. Ne « résolvez » pas le problème en lançant tout en mode privilégié en espérant que ça ira.

Trois mini-histoires d’entreprise issues du terrain

Mini-histoire 1 : L’incident causé par une mauvaise hypothèse

Une équipe SaaS de taille moyenne gardait deux fichiers Compose : un pour le dev, un pour le « prod-like ». L’hypothèse était polie et mortelle :
« Ils sont essentiellement les mêmes ; prod-like ajoute juste nginx. » Personne n’a revérifié cette affirmation après la dixième petite modification.

Un nouvel ingénieur a ajouté un conteneur Redis au fichier dev seulement, parce que l’appli avait un feature flag et « la prod ne l’utilise pas encore ».
Quelques semaines plus tard, la prod a commencé à activer le flag en canary. La pile prod-like utilisée en CI n’avait pas Redis.
La CI passait parce que les tests concernés étaient ignorés quand Redis n’était pas détecté.

Puis est venue une mise en production où le flag a été étendu plus largement que prévu. Le comportement de secours de l’appli était de relancer agressivement les connexions Redis.
Le CPU a monté, la latence des requêtes a suivi, et quelques nœuds ont commencé à être tués par l’OOM killer du kernel. Pas tous,
juste suffisamment pour créer un brownout progressif ressemblant à une « instabilité réseau ».

La correction n’a pas été héroïque. Ils sont revenus à un seul fichier Compose et ont utilisé des profils : Redis est devenu par défaut dans la pile utilisée pour la CI, et une nouvelle
profile a servi à isoler les dépendances « expérimentales ». Cela a forcé une décision consciente : si l’appli peut utiliser Redis en prod, Redis doit exister dans le modèle prod-like.

Leçon : les hypothèses sur la parité des environnements sont comme le lait. Elles expirent silencieusement, puis gâchent votre journée bruyamment.

Mini-histoire 2 : L’optimisation qui s’est retournée contre eux

Une grande équipe plateforme a tenté « d’optimiser l’expérience développeur » en utilisant des profils pour swapper des images complètes :
une petite image de debug pour le dev et une image durcie pour la prod. Sur le papier, cela réduisait le temps de build local et rendait l’image prod plus stricte.
En pratique, ils ont créé un univers forké.

L’image dev contenait des paquets supplémentaires : curl, netcat, Python, et quelques bundles CA qui « faisaient simplement fonctionner les choses ».
L’image prod était mince : moins de libs, moins d’outils, moins de surface d’attaque. Objectifs respectables.
Mais l’appli avait une dépendance cachée sur les certificats CA système via un SDK tiers réalisant des appels TLS.

Le dev n’a jamais vu le bug parce que l’image debug avait la bonne chaîne CA. La prod, elle, l’a vu : les handshakes TLS échouaient de façon intermittente selon l’endpoint atteint,
et les erreurs étaient enveloppées dans des exceptions opaques. L’incident a traîné parce que les ingénieurs reproduisaient constamment en dev, où tout fonctionnait.

Ils ont gardé les profils, mais changé la règle : les profils peuvent modifier les commandes, montages et ports, mais pas la composition OS de base de l’image runtime sans un test formel
qui exécute l’image prod dans les workflows dev. Ils ont aussi ajouté un profil « prod-image » qui force l’image prod localement.

Leçon : optimiser pour la vitesse en changeant le substrat runtime est la manière la plus rapide d’acheter des incidents lents.

Mini-histoire 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise

Une équipe interne paiements utilisait Compose pour le dev local et pour un petit environnement on-prem « lab » utilisé pour les intégrations partenaires.
Leur pratique était peu sexy : chaque changement au Compose devait inclure un artefact de sortie docker compose config mis à jour dans les logs CI pour chaque profil.
Pas stocké pour toujours, juste attaché au résumé du job.

Un matin, un changement est arrivé qui a déplacé un mapping de port d’un service réservé au dev vers un service par défaut. Ce n’était pas malveillant ;
c’était une erreur de copier/coller lors d’un refactor. Le service était une UI admin de base de données. Vous voyez où ça mène.

L’environnement lab avait un firewall strict, donc ce n’était pas exposé sur Internet. Mais il était accessible depuis un grand réseau d’entreprise,
ce qui est son propre genre de territoire sauvage. L’équipe a attrapé l’erreur avant le déploiement parce que l’artefact CI pour le profil par défaut
montrait soudainement un port publié qui n’existait pas la veille.

Ils ont revert, puis réintroduit le changement correctement derrière le profil dev. Pas d’incident, pas de spiral de honte, pas de « on le réparera plus tard ».
Juste une petite garde-barrière ennuyeuse qui fait son travail.

Leçon : imprimer la config effective est l’équivalent ops de se laver les mains. Ce n’est pas glamour, et ça évite les infections.

Cahier de diagnostic rapide : quoi vérifier en premier/deuxième/troisième

Quand une pile Compose « ne marche pas », le chemin le plus rapide est d’arrêter de deviner ce que Compose a fait et d’inspecter ce qu’il a effectivement fait.
Les profils ajoutent une dimension de confusion, donc votre triage doit être net.

Premier : confirmez l’ensemble de profils prévu et la config rendue

  • Exécutez docker compose --profile X config et scannez pour :

    • ports publiés inattendus
    • services manquants que vous pensiez présents (cache, broker de messages, reverse proxy)
    • valeurs par défaut d’env vars que vous aviez oubliées
  • Si la sortie config vous surprend, arrêtez-vous. Corrigez la configuration avant de poursuivre le diagnostic runtime.

Deuxième : vérifiez l’état et la santé des conteneurs, pas seulement « running »

  • Exécutez docker compose ps. Cherchez (healthy) et les boucles de redémarrage.
  • Un service peut être « Up » et néanmoins être mort à l’intérieur. Les healthchecks sont votre détecteur de mensonge peu coûteux.

Troisième : déterminez si vous avez une défaillance de dépendance ou une défaillance applicative

  • Si la BD est unhealthy : vérifiez le stockage, les permissions et les montages de volume.
  • Si la BD est healthy mais l’appli est unhealthy : vérifiez les logs applicatifs et les vars d’environnement.
  • Si tout est healthy mais les requêtes échouent : vérifiez le réseau, les ports publiés, et la config du reverse proxy (surtout si le profil prod ajoute un edge).

Bonus : isolez en retirant les profils

Si le profil dev introduit une régression, exécutez uniquement la pile par défaut. Si la pile par défaut fonctionne, la régression se trouve dans les services spécifiques au dev,
les montages ou les conflits de ports. Les profils rendent cette isolation triviale — si vous gardez vos defaults propres.

Erreurs courantes : symptôme → cause racine → correction

Erreur 1 : « Pourquoi mon outil dev tourne en prod ? »

Symptôme : UI Admin, MailHog, ou endpoints de debug apparaissent dans des environnements où ils ne devraient pas être.

Cause racine : Le service n’a pas profiles: ["dev"], ou l’environnement définit globalement COMPOSE_PROFILES=dev.

Correction : Ajoutez des profils au service, et auditez les CI/hôtes pour des COMPOSE_PROFILES fuite. Dans les scripts de prod, définissez explicitement COMPOSE_PROFILES=prod.

Erreur 2 : « Activer un profil n’a rien démarré »

Symptôme : docker compose --profile ops up n’affiche aucun nouveau conteneur, ou seuls les defaults démarrent.

Cause racine : Les services sont définis avec un nom de profil différent de celui que vous avez passé (faute de frappe), ou vous attendiez que des jobs de type run apparaissent sous up.

Correction : Utilisez docker compose config --services et inspectez les sections profiles. Pour les jobs ponctuels, utilisez docker compose run --rm SERVICE.

Erreur 3 : « L’appli ne peut pas se connecter à la base en dev, mais la prod fonctionne »

Symptôme : Connection refusée/timeouts seulement sous le profil dev.

Cause racine : Le service dev utilise un DATABASE_URL différent, ou vous l’avez accidentellement pointé vers localhost au lieu du nom de service db.

Correction : Dans les conteneurs, utilisez les noms DNS de service sur le réseau Compose : db:5432. Confirmez avec docker compose exec app env.

Erreur 4 : « Port is already allocated » apparaît aléatoirement

Symptôme : Le démarrage du profil dev échoue avec une erreur de binding de port.

Cause racine : Une autre pile lie déjà le port, ou vous avez démarré deux profils qui publient le même port hôte (commun avec app et app-dev si les deux publient 8080).

Correction : Ne publiez des ports que dans un seul des services (généralement le dev). Vérifiez les collisions avec ss -ltnp.

Erreur 5 : « depends_on n’a pas attendu ; l’appli a démarré trop tôt »

Symptôme : L’appli démarre avant que la BD soit prête, provoquant des boucles de crash.

Cause racine : Vous avez utilisé depends_on sans conditions de santé, ou le healthcheck de la BD est manquant/incorrect.

Correction : Ajoutez des healthchecks et utilisez condition: service_healthy. Rendez aussi l’appli résiliente avec des retries ; Compose n’est pas votre couche de fiabilité.

Erreur 6 : « Nous pensions que les services de profil n’étaient pas créés, mais ils l’étaient »

Symptôme : Un service conditionné par un profil existe comme artefact conteneur/réseau, même si le profil n’était pas activé.

Cause racine : Vous avez précédemment exécuté avec ce profil activé ; les ressources restent jusqu’à suppression. Ou votre automatisation utilise docker compose up avec des variables d’environnement définies.

Correction : Utilisez docker compose down (et éventuellement -v en dev seulement). Considérez « ce qui tourne actuellement » comme état, pas comme intention.

Erreur 7 : « Nos sauvegardes ont réussi mais les restaurations ont échoué »

Symptôme : Le job de backup s’exécute sans erreur ; la restauration échoue plus tard ou produit des données vides.

Cause racine : Le conteneur de backup a écrit dans un chemin à l’intérieur du conteneur qui n’était pas monté, ou les permissions ont empêché l’écriture sur l’hôte.

Correction : Stockez les sauvegardes sur un chemin monté sur l’hôte. Après la sauvegarde, vérifiez la présence et la taille du fichier avec ls -lh. Testez périodiquement la restauration.

Listes de contrôle / plan étape par étape

Étape par étape : migrer de plusieurs fichiers Compose vers un seul fichier avec profils

  1. Faire l’inventaire des services à travers les fichiers. Listez les services et notez les différences (ports, volumes, tags d’image, commandes).
  2. Définir des profils qui reflètent des décisions, pas des personnes.
    Utilisez des noms comme dev, prod, ops, debug. Évitez alice ou newthing.
  3. Choisir la pile « défaut sûre ». Pas d’outils dev, pas de ports DB publiés sauf ce qui est strictement nécessaire (souvent aucun).
  4. Placer les services réservés au dev derrière dev. MailHog, Adminer, S3 factice, UIs de tracing local, etc.
  5. Scinder les services lorsque le comportement d’exécution diffère matériellement.
    Si le dev a besoin de bind mounts et d’une commande différente : créez app-dev plutôt que d’essayer tout modifier via des vars d’environnement.
  6. Garder l’identité des images stable autant que possible. Préférez la même image pour app et app-dev ; changez commande/montages/ports.
  7. Rendre les configs dans la CI pour chaque profil. Sauvegardez les sorties docker compose config dans les logs de build.
  8. Documenter les commandes « comment exécuter ». Rendre copiable/collable ; les gens vont de toute façon les copier/coller.
  9. Tester trois chemins : default seulement, --profile dev, --profile prod (ou prod-like).
  10. Tuer les anciens fichiers. Ne les gardez pas « au cas où ». C’est comme ça que la dérive revient.

Checklist opérationnelle : avant de déclarer une stratégie de profils « terminée »

  • Le profil par défaut démarre et est fonctionnel sans ports DB publiés.
  • La sortie docker compose config est stable et revue pour chaque profil.
  • Le profil dev ne change pas les images de base sans un plan de test explicite.
  • Les tâches ops utilisent run --rm et écrivent sur des chemins montés sur l’hôte.
  • Les mappings de ports sont uniques entre les services qui peuvent coexister.
  • Des healthchecks existent pour les dépendances stateful (BD) et l’application.
  • Les secrets ne sont pas commités, et les invocations de prod définissent les profils explicitement.

Plan CI : minimal mais efficace

  1. Rendre la config pour default + dev + prod et stocker dans les logs.
  2. Démarrer la pile par défaut, exécuter les tests de fumée, démonter.
  3. Démarrer la pile dev (ou un sous-ensemble), exécuter tests unitaires/intégration, démonter.
  4. Exécuter les migrations ops en job ponctuel dans un environnement jetable.

FAQ

1) Dois-je utiliser des profils ou des fichiers d’override ?

Utilisez les profils pour les changements de topologie (quels services existent) et pour « les outils dev sont optionnels ».
Utilisez les fichiers d’override avec parcimonie pour des ajustements machine-locaux (comme le port personnalisé d’un développeur), et seulement si vous tolérez la dérive.
Si vous devez choisir : les profils sont plus faciles à raisonner et à auditer.

2) Un service peut-il appartenir à plusieurs profils ?

Oui. Vous pouvez définir profiles: ["dev", "ops"] pour un service utile dans les deux contextes.
Faites attention : l’appartenance multiple peut devenir un puzzle logique pendant les incidents.
Gardez-le rare et justifié.

3) Que se passe-t-il si j’exécute docker compose up sans profil spécifié ?

Les services sans clé profiles sont démarrés. Les services avec une clé profiles sont ignorés.
C’est pourquoi vos services par défaut doivent être sûrs et minimaux.

4) Activer un profil peut-il démarrer accidentellement des services supplémentaires via des dépendances ?

Cela peut arriver, selon la façon dont vous démarrez les choses et comment vos dépendances sont déclarées. Votre travail est de tester les chemins de démarrage :
démarrer « juste l’app », démarrer la pile complète, et démarrer les services de profil.
Supposez que les humains exécuteront des commandes étranges pendant les incidents.

5) Les profils affectent-ils les réseaux et volumes ?

Les profils conditionnent les services. Les réseaux et volumes sont généralement créés au besoin par les services qui y font référence.
Si un volume est uniquement référencé par un service profilé, il ne sera pas créé sauf si ce profil est actif.

6) Comment empêcher que des ports dev soient exposés quand quelqu’un exécute le mauvais profil ?

Rendez le profil par défaut sûr, et rendez les invocations prod explicites. Dans les scripts, définissez COMPOSE_PROFILES=prod
plutôt que de compter sur les variables d’environnement présentes. Évitez aussi de publier des ports dans les services par défaut à moins que ce soit vraiment nécessaire.

7) Comment gérer les migrations avec les profils ?

Placez les migrations dans un profil ops comme job ponctuel et exécutez-les avec docker compose run --rm migrate.
Ne faites pas des migrations un service de longue durée. S’il redémarre, vous finirez par migrer deux fois. Ce n’est pas un plan de mise à niveau.

8) Les profils conviennent-ils pour des déploiements « prod sur une VM unique » ?

Oui, avec discipline. Les profils vous aident à garder les outils ops hors de la baseline et à empêcher l’exposition accidentelle.
Mais ne confondez pas « ça marche sur une VM » et « c’est une plateforme de production orchestrée ».
Ajoutez monitoring, sauvegardes et procédures explicites de rollback. Compose ne les inventera pas pour vous.

9) Quelle est la manière la plus propre de basculer entre comportement dev et prod pour la même appli ?

Préférez des services séparés (comme app et app-dev) lorsque les différences sont significatives (bind mounts, commandes, ports).
Gardez-les sur le même tag d’image quand c’est possible. Comportement séparé, artefact partagé.

10) Dois-je garder un profil debug ?

Oui, si vous l’utilisez responsablement. Un profil debug pour des outils éphémères (conteneur tcpdump, shell, agent de profiling)
peut réduire le temps moyen pour comprendre. Ne le laissez juste pas devenir « prod avec des petites roues » toujours activé.

Conclusion : étapes pratiques suivantes

Les profils Compose sont le moyen le plus simple d’arrêter de dupliquer le YAML tout en exécutant différentes piles pour différents contextes.
Ils n’éliminent pas la complexité ; ils la rendent visible et contrôlable. C’est bien le but.

Faites ceci ensuite, dans l’ordre

  1. Choisissez une pile par défaut sûre sans outils dev et avec une exposition minimale de ports hôte.
  2. Ajoutez les profils dev, prod et ops pour contrôler ce qui est optionnel, risqué, ou ponctuel.
  3. Faites de la sortie docker compose config une partie des logs CI pour chaque profil. Traitez-la comme une piste d’audit.
  4. Convertissez les utilitaires de migration/sauvegarde en jobs run --rm derrière ops.
  5. Supprimez vos fichiers Compose supplémentaires une fois que l’approche en un seul fichier est validée. La dérive adore l’attachement sentimental.

Quand vous êtes de garde, vous voulez moins de pièces mobiles et moins de branches de comportement non documentées. Les profils vous donnent cela — si vous gardez vos defaults propres
et vos profils intentionnels. Exécutez moins de magie. Livrez plus de prévisibilité.

← Précédent
Suppression des snapshots ZFS : pourquoi ils refusent de disparaître (et comment y remédier)
Suivant →
Exercice DR ZFS send/receive : s’entraîner à la restauration, pas seulement à la sauvegarde

Laisser un commentaire