Docker Compose Local Stack
"Works on my machine" is not a defense, it is a confession. It means your app depends on things that live outside the repo: the Postgres you installed with Homebrew two years ago, the Redis that autostarts on login, the one environment variable you exported in a terminal that has since been closed. Your agent inherits none of that. Neither does your laptop after it dies, and laptops die.
Docker Compose fixes this by writing the whole environment down. One file declares your app, its database, and its dependencies, with versions pinned. docker compose up builds that world from nothing; docker compose down removes it without residue. The database version in the file is the database version, period. When your agent needs to run the app to test its own work, it reads this file instead of guessing what is installed, and its guesses about your environment are the source of half its weirdest failures.
The template below is a working compose file for the most common shape: one app, one Postgres, one Redis, wrapped in a short spec telling the agent how to adapt it and which commands do what. The annotations in the YAML are load-bearing. The healthcheck, in particular, is the difference between an app that waits for its database and an app that races it and loses at random, and intermittent startup crashes are miserable to debug precisely because they are intermittent.
Prerequisites
- Docker Desktop (Mac/Windows) or Docker Engine + the compose plugin (Linux), installed and running.
- A `Dockerfile` for your app, or willingness to let the agent write one as part of this task.
# Task: local dev stack with Docker Compose
Create `docker-compose.yml` at the repo root using the annotated file below
as the base. Adapt it to this project, then verify with the checklist.
## Project facts
- App: [language/framework, e.g. Node 20 + Next.js, Python 3.12 + FastAPI]
- App port: [e.g. 3000]
- Database: [postgres:16 unless you have a reason. Match production's major version]
- Other services: [redis / none / queue / etc.]
## The compose file
```yaml
name: [project-name]
services:
app:
build: . # uses the Dockerfile in this repo
ports:
- "3000:3000" # host:container, change the LEFT side on collision
env_file: .env # secrets live here; .env is gitignored
environment:
# Hostname is the SERVICE NAME, not localhost. Inside the compose
# network, "db" resolves to the Postgres container.
DATABASE_URL: postgres://app:app@db:5432/app_dev
REDIS_URL: redis://cache:6379
volumes:
- .:/app # live-reload: your edits appear in the container
- /app/node_modules # but the container keeps its own dependencies
depends_on:
db:
condition: service_healthy # wait for Postgres to ACCEPT CONNECTIONS,
cache: # not merely to exist. This kills the
condition: service_started # random "connection refused" on startup.
db:
image: postgres:16 # pin the major version; match production
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: app # local-only credentials. Fine here, nowhere else.
POSTGRES_DB: app_dev
ports:
- "5433:5432" # 5433 on purpose: avoids fighting any Postgres
# already installed on the host
volumes:
- db_data:/var/lib/postgresql/data # named volume = data survives restarts
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app_dev"]
interval: 2s
timeout: 3s
retries: 15
cache:
image: redis:7
# No volume: cache contents are disposable by definition. If losing this
# data would hurt, it is not a cache and does not belong in Redis like this.
volumes:
db_data:
```
## Commands (put these in the README verbatim)
- `docker compose up -d`: start everything in the background
- `docker compose logs -f app`: tail the app's logs
- `docker compose down`: stop everything; DATA SURVIVES (named volume)
- `docker compose down -v`: stop everything and DESTROY the database volume.
This is the clean-slate command. It asks nothing and forgives nothing.
- `docker compose exec db psql -U app app_dev`: SQL shell into the database
## Verification checklist
- [ ] `docker compose up -d` from a fresh clone reaches "ready" with no errors
- [ ] App responds at http://localhost:3000 and can read/write the database
- [ ] `docker compose down` then `up -d` again: data is still there
- [ ] Editing a source file is reflected without rebuilding the image
- [ ] `.env` is in `.gitignore` and a `.env.example` with dummy values is committedAdaptation notes:
- No Redis? Delete the
cacheservice and its references. Compose files should describe what you run, not what tutorials run. - Python or compiled apps: the
node_modulesvolume trick becomes your virtualenv or build directory. The principle is the same: source code is shared with the host, build artifacts belong to the container. - To seed the database, mount SQL files into
/docker-entrypoint-initdb.d/on thedbservice. They run once, on first creation of the volume only, which surprises everyone exactly once. - The mistake: running
docker compose down -vto "restart" the stack and vaporizing a week of local test data.-vdeletes volumes. Restart isdownthenup, no flag. - When production behaves differently from local, check versions first. The whole point of pinning
postgres:16is that the answer to "which Postgres are we on?" is in the repo, in one place, under version control.