DEPLOYTEMPLATE

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.
markdown
# 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 committed

Adaptation notes:

  • No Redis? Delete the cache service and its references. Compose files should describe what you run, not what tutorials run.
  • Python or compiled apps: the node_modules volume 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 the db service. They run once, on first creation of the volume only, which surprises everyone exactly once.
  • The mistake: running docker compose down -v to "restart" the stack and vaporizing a week of local test data. -v deletes volumes. Restart is down then up, no flag.
  • When production behaves differently from local, check versions first. The whole point of pinning postgres:16 is that the answer to "which Postgres are we on?" is in the repo, in one place, under version control.