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.jsonusingdockerComposeFileandservice - Docker Engine 24+ with Compose v2
- A
remoteUserfor the app service
How-To Steps
-
Reference the Compose file from
devcontainer.json:{ "name": "app-with-postgres", "dockerComposeFile": "docker-compose.yml", "service": "app", "workspaceFolder": "/workspace", "remoteUser": "vscode" } -
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 -
Let the app reach Postgres by service name — no hardcoded IPs:
{ "remoteEnv": { "DATABASE_URL": "postgres://dev:devpass@db:5432/appdb" } } -
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
| Symptom | Root Cause | Remediation |
|---|---|---|
| Data lost on rebuild | No named volume for /var/lib/postgresql/data | Mount the pgdata volume |
| App can’t connect on startup | No health gate; app starts before DB ready | Use depends_on: condition: service_healthy |
db host not found | Services on different networks | Put both on one user-defined network |
| Non-reproducible DB version | Floating postgres:16 tag | Pin 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.
Related
- Docker Compose Integration for Multi-Service Apps — the parent cluster.
- Debugging Network & DNS Issues in Containers — when the app can’t resolve
db. - Migrating from Docker Compose to DevContainers — adopting Compose delegation.