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 infinitysur le serviceapp: 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 infinityle 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émarredbavantapp(démarrage, pas "prêt à accepter des connexions" — voir plus bas).db-dataen volume nommé : les données PostgreSQL survivent à undocker compose downsans-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.
- Ouvrez le Dev Container (le service
apps'ouvre automatiquement,dbdémarre en arrière-plan grâce àrunServices). - Dans le terminal du conteneur
app, testez la résolution DNS et la connexion réseau versdb:
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!
- Si
postgresql-clientest installé dans l'imageapp(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.