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

Multi-service devcontainer with docker-compose

The problem this module solves

So far, your devcontainer.json files pointed at a single image or a single build. One container, one purpose: writing code. But as soon as a project needs a database, a cache, or a third-party service during development, one container stops being enough — you don't want PostgreSQL installed inside the application container, you want a separate, disposable, reproducible PostgreSQL container.

That's exactly what docker-compose does for production or staging. The Dev Containers spec reuses the same mechanism: instead of describing a single container, devcontainer.json references a docker-compose.yml file and states which of the services it describes to connect to.

The three properties to know

Three fields in devcontainer.json, documented in the official format reference on containers.dev, drive this mode:

dockerComposeFile

Path (or ordered list of paths) to the Docker Compose file(s) to use, relative to devcontainer.json.

"dockerComposeFile": "docker-compose.yml"

You can also pass an array — useful for layering a base file with a dev-specific override:

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

service

The name of the service (the key under services: in docker-compose.yml) that Dev Containers-compatible tools connect to for opening a terminal, running the debugger, and installing extensions.

"service": "app"

workspaceFolder

The path, inside the service container, where the source code is mounted and where the workspace opens. It defaults to /, but in practice you always set it explicitly to your volume's mount path.

"workspaceFolder": "/workspace"

runServices (optional)

A list of services to start with docker compose up. By default, every service in the compose file starts. Use runServices if your compose file defines services you don't want to run by default (for example, a graphical database admin tool).

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

The full example: app + database

File layout:

.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:

Things to note:

  • command: sleep infinity on the app service: left alone, Docker Compose stops a service once its main command exits. Dev Containers tooling expects to be able to open a shell in the container at any time; sleep infinity keeps it alive without doing anything else. This is the service VS Code connects to, not a background server process.
  • depends_on: [db] guarantees Docker Compose starts db before app (container start order, not "ready to accept connections" — see below).
  • A named volume, db-data: PostgreSQL data survives a docker compose down without -v, unlike an anonymous volume.

devcontainer.json:

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

Documenting the service graph

On a real project, once you're past two services, a sentence of prose stops being enough for someone new to the repo to understand the architecture. Document the graph explicitly — a plain .devcontainer/README.md file, or a section in the main README, is enough:

app (Dev Container service, hostname: app)
 └─ depends on → db (postgres:16, hostname: db, port 5432)

For each service, record: its name (= its hostname on the compose network), the image or Dockerfile it uses, the port it listens on, and what depends on it. This document becomes the reference once another service (Redis, a queue, a second microservice) gets added later — someone reading the graph should be able to predict which hostname to use without re-reading the whole docker-compose.yml.

Proof of success: is the database actually reachable?

A claim ("it should work") isn't enough — you need to demonstrate it from inside the app container, exactly the way your application code would.

  1. Open the Dev Container (the app service opens automatically; db starts in the background thanks to runServices).
  2. In the app container's terminal, test DNS resolution and network connectivity to db:
getent hosts db
# should resolve to db's internal IP on the compose network

nc -zv db 5432
# Connection to db 5432 port [tcp/postgresql] succeeded!
  1. If postgresql-client is installed in the app image (add it to your Dockerfile, or via a Feature if needed), verify the actual application-level connection, not just an open port:
PGPASSWORD=dev psql -h db -U dev -d appdb -c "SELECT 1;"
#  ?column?
# ----------
#         1
# (1 row)

The mechanical success criterion for this module: the psql (or nc) command above succeeds from the app container, using db as the hostname — not a hardcoded IP address, not localhost. If nc times out, first check that db is listed in runServices (or that there's no runServices at all, in which case every service starts), then that depends_on is present — without it, app can start before the PostgreSQL process inside db is actually accepting connections, even though the db container itself is up.

A plain depends_on only guarantees container start order, not that the service inside is ready. For a stricter production-grade requirement, docker-compose supports healthcheck and depends_on.condition: service_healthy — out of scope here, but keep this limitation in mind: on a real project, a postCreateCommand that fails one time in ten because Postgres hasn't finished starting is a classic trap.

What's next

This setup works, but every contributor who clones the repo has to rebuild the app image locally — potentially several minutes, every time a git pull changes the Dockerfile. Module 02 asks you to decide, with numbers, whether your team should publish prebuilt images or keep building on demand.

Check your understanding

In a devcontainer.json that uses docker-compose, which field tells VS Code (or the devcontainers CLI) WHICH service to connect to for opening a terminal and running extensions?

From the 'app' service container, how do you reach the database running as the 'db' service in the same docker-compose.yml?

What does runServices do in devcontainer.json, as opposed to service?

Want to hear about the next modules?

The Academy stays free and open-access, no sign-up required. If you'd just like to be notified by email when a new module ships, here you go — no obligation, unsubscribe anytime with one click.