Configuring a Postgres service in a DevContainer Compose stack

A database in a DevContainer needs three things the default setup omits: persistent data across rebuilds, a health check so the app waits for readiness, and a pinned image for reproducibility. Skip them and you get a database that loses data on rebuild and an app that races startup. This page adds a correct Postgres service to a Compose-backed DevContainer. It extends the Docker Compose Integration for Multi-Service Apps cluster.

Prerequisites

  • A devcontainer.json using dockerComposeFile and service
  • Docker Engine 24+ with Compose v2
  • A remoteUser for the app service

How-To Steps

  1. Reference the Compose file from devcontainer.json:

    {
      "name": "app-with-postgres",
      "dockerComposeFile": "docker-compose.yml",
      "service": "app",
      "workspaceFolder": "/workspace",
      "remoteUser": "vscode"
    }
    
  2. Define the Postgres service with a pinned image, persistent volume, and health check:

    services:
      app:
        build: .
        volumes:
          - ..:/workspace:cached
        command: sleep infinity
        depends_on:
          db:
            condition: service_healthy
        networks: [devnet]
      db:
        image: postgres:16-alpine@sha256:1f3d...
        environment:
          POSTGRES_USER: dev
          POSTGRES_PASSWORD: ${DB_PASSWORD:-devpass}
          POSTGRES_DB: appdb
        volumes:
          - pgdata:/var/lib/postgresql/data
        healthcheck:
          test: ["CMD-SHELL", "pg_isready -U dev -d appdb"]
          interval: 5s
          timeout: 3s
          retries: 10
        networks: [devnet]
    volumes:
      pgdata:
    networks:
      devnet:
        driver: bridge
    
  3. Let the app reach Postgres by service name — no hardcoded IPs:

    { "remoteEnv": { "DATABASE_URL": "postgres://dev:devpass@db:5432/appdb" } }
    
  4. Verify readiness and persistence:

    devcontainer exec --workspace-folder . pg_isready -h db -U dev   # accepting connections
    devcontainer exec --workspace-folder . psql "$DATABASE_URL" -c "CREATE TABLE t(id int);"
    devcontainer up --workspace-folder . --remove-existing-container
    devcontainer exec --workspace-folder . psql "$DATABASE_URL" -c "\dt"   # table t survives
    

Common Pitfalls

SymptomRoot CauseRemediation
Data lost on rebuildNo named volume for /var/lib/postgresql/dataMount the pgdata volume
App can’t connect on startupNo health gate; app starts before DB readyUse depends_on: condition: service_healthy
db host not foundServices on different networksPut both on one user-defined network
Non-reproducible DB versionFloating postgres:16 tagPin the image by SHA digest

Conclusion

The invariant: a database service is only reproducible when its data is on a named volume, its image is digest-pinned, and dependents gate on its health check. Wire those three and the app reaches a ready Postgres by service name on every create — with its data intact across rebuilds.

FAQ

Why gate on a health check instead of depends_on alone? Plain depends_on only waits for the container to start, not for Postgres to accept connections. The service_healthy condition plus pg_isready waits for actual readiness, eliminating the startup race.

Should the database password be in the Compose file? Use a default for local dev (${DB_PASSWORD:-devpass}) but source real values from the host via remoteEnv and ${localEnv:...} so secrets never enter the image or repo.