Aller au contenu principal
Nicolas Cousin Tech SolutionsNicolas Cousin Tech Solutions
Module 1 sur 6

Devcontainer multi-services avec docker-compose

Le problème que ce module résout

Jusqu'ici, vos devcontainer.json pointaient vers une seule image ou un seul build. Un conteneur, un usage : écrire du code. Mais dès qu'un projet a besoin d'une base de données, d'un cache, ou d'un service tiers pendant le développement, un seul conteneur ne suffit plus — vous ne voulez pas installer PostgreSQL dans le conteneur applicatif, vous voulez un conteneur PostgreSQL séparé, jetable, reproductible.

C'est exactement ce que docker-compose fait pour la production ou le staging. La spécification Dev Containers réutilise ce même mécanisme : au lieu de décrire un conteneur unique, devcontainer.json référence un fichier docker-compose.yml et indique auquel des services décrits il faut se connecter.

Les trois propriétés à connaître

Trois champs de devcontainer.json, documentés dans la référence officielle du format sur containers.dev, pilotent ce mode :

dockerComposeFile

Chemin (ou liste de chemins, dans l'ordre) vers le ou les fichiers Docker Compose à utiliser, relatifs à devcontainer.json.

"dockerComposeFile": "docker-compose.yml"

Vous pouvez aussi passer un tableau — utile pour superposer un fichier de base et un override spécifique au développement :

"dockerComposeFile": ["docker-compose.yml", "docker-compose.override.yml"]

service

Le nom du service (au sens docker-compose.yml, la clé sous services:) auquel les outils compatibles Dev Containers se connectent pour ouvrir un terminal, exécuter le débogueur et installer les extensions.

"service": "app"

workspaceFolder

Le chemin, à l'intérieur du conteneur service, où le code source est monté et où s'ouvre l'espace de travail. Par défaut c'est /, mais en pratique vous le fixez toujours explicitement au chemin de montage de votre volume.

"workspaceFolder": "/workspace"

runServices (optionnel)

Une liste des services à démarrer avec docker compose up. Par défaut, tous les services du fichier compose démarrent. Utilisez runServices si votre fichier compose contient des services que vous ne voulez pas lancer systématiquement (par exemple un outil d'administration graphique de la base).

"runServices": ["app", "db"]

L'exemple complet : app + base de données

Structure de fichiers :

.devcontainer/
  devcontainer.json
  docker-compose.yml

docker-compose.yml :

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - ..:/workspace:cached
    command: sleep infinity
    depends_on:
      - db

  db:
    image: postgres:16
    restart: unless-stopped
    environment:
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: dev
      POSTGRES_DB: appdb
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:

Points à noter :

  • command: sleep infinity sur le service app : Docker Compose, laissé à lui-même, arrête un service dès que sa commande principale se termine. Les outils Dev Containers attendent qu'un shell reste ouvrable dans le conteneur ; sleep infinity le garde vivant sans rien faire d'autre. C'est le service auquel VS Code se connecte, pas un serveur qui tourne en tâche de fond.
  • depends_on: [db] : garantit que Docker Compose démarre db avant app (démarrage, pas "prêt à accepter des connexions" — voir plus bas).
  • db-data en volume nommé : les données PostgreSQL survivent à un docker compose down sans -v, contrairement à un volume anonyme.

devcontainer.json :

{
  "name": "App + PostgreSQL",
  "dockerComposeFile": "docker-compose.yml",
  "service": "app",
  "workspaceFolder": "/workspace",
  "runServices": ["app", "db"],
  "postCreateCommand": "npm install"
}

Documenter le graphe de services

Sur un vrai projet, dès qu'on dépasse deux services, une ligne de texte ne suffit plus à faire comprendre l'architecture à quelqu'un qui découvre le repo. Documentez le graphe explicitement — un simple fichier .devcontainer/README.md ou une section dans le README principal suffit :

app (service Dev Container, hostname: app)
 └─ dépend de → db (postgres:16, hostname: db, port 5432)

Pour chaque service, notez : son nom (= son hostname sur le réseau compose), l'image ou le Dockerfile utilisé, le port qu'il écoute, et ce qui en dépend. Ce document devient la référence quand un service supplémentaire (Redis, une queue, un second microservice) s'ajoute plus tard — quelqu'un qui lit le graphe doit pouvoir prédire quel hostname utiliser sans relire tout le docker-compose.yml.

Preuve de réussite : la base est-elle vraiment joignable ?

Une affirmation ("ça devrait marcher") ne suffit pas — vous devez le démontrer depuis l'intérieur du conteneur app, exactement comme le ferait votre code applicatif.

  1. Ouvrez le Dev Container (le service app s'ouvre automatiquement, db démarre en arrière-plan grâce à runServices).
  2. Dans le terminal du conteneur app, testez la résolution DNS et la connexion réseau vers db :
getent hosts db
# doit résoudre vers l'IP interne du conteneur db sur le réseau compose

nc -zv db 5432
# Connection to db 5432 port [tcp/postgresql] succeeded!
  1. Si postgresql-client est installé dans l'image app (ajoutez-le à votre Dockerfile ou via une Feature si besoin), validez la connexion applicative réelle, pas seulement le port ouvert :
PGPASSWORD=dev psql -h db -U dev -d appdb -c "SELECT 1;"
#  ?column?
# ----------
#         1
# (1 row)

Le critère de réussite mécanique de ce module : la commande psql (ou nc) ci-dessus réussit depuis le conteneur app, en utilisant db comme hostname — pas une adresse IP codée en dur, pas localhost. Si nc échoue en timeout, vérifiez d'abord que db figure bien dans runServices (ou qu'il n'y a pas de runServices du tout, auquel cas tous les services démarrent), puis que depends_on est présent — sinon app peut démarrer avant que le processus PostgreSQL n'accepte encore de connexions, même si le conteneur db est lui-même démarré.

Un depends_on simple garantit l'ordre de démarrage des conteneurs, pas que le service à l'intérieur soit prêt. Pour un besoin de production plus strict, docker-compose supporte des healthcheck et depends_on.condition: service_healthy — hors périmètre ici, mais gardez cette limite en tête : dans un vrai projet, un postCreateCommand qui échoue une fois sur dix parce que Postgres n'a pas fini de démarrer est un piège classique.

Ce qui vous attend au module suivant

Ce setup fonctionne, mais chaque contributeur qui clone le repo doit reconstruire l'image app localement — potentiellement plusieurs minutes, à chaque fois qu'un git pull change le Dockerfile. Le module 02 vous demande de trancher, avec des chiffres, si votre équipe doit publier des images pré-construites ou continuer à construire à la volée.

Vérifiez votre compréhension

Dans un devcontainer.json qui utilise docker-compose, quel champ indique à VS Code (ou à la CLI devcontainers) à QUEL service se connecter pour ouvrir un terminal et exécuter les extensions ?

Depuis le conteneur du service 'app', comment atteindre la base de données du service 'db' définie dans le même docker-compose.yml ?

À quoi sert runServices dans devcontainer.json, par opposition à service ?

Envie d'être prévenu des prochains modules ?

L'Académie reste gratuite et en accès libre, sans inscription. Si vous voulez juste être averti par email à la sortie d'un nouveau module, c'est ici — aucune obligation, désinscription en un clic.