---
phase: 01-project-infrastructure-module-wiring
plan: 06
subsystem: dev-ergonomics
tags: [docker-compose, postgres, readme, local-dev, infra]
dependency_graph:
  requires: []
  provides:
    - "Local Postgres 16 dev instance matching application.conf HOCON defaults (recipe/recipe/recipe)"
    - "Named volume recipe-pgdata for persistence across container restarts"
    - "pg_isready healthcheck enabling docker compose up --wait usage"
    - "README 'Local development' section documenting the two-command dev loop"
  affects:
    - "server/src/main/resources/application.conf (Plan 05 — credentials match contract)"
    - "Phase 3 (Households + DB migrations) — depends on a working local Postgres"
    - "Phase 11 (homelab deployment) — separate compose config will diverge from this dev-local one"
tech_stack:
  added:
    - "postgres:16 (Docker image, pinned major version)"
  patterns:
    - "Dev-local compose file committed to repo (non-secret literal creds)"
    - "Healthcheck via pg_isready gating sequencing"
    - "Named Docker volume for data persistence"
key_files:
  created:
    - "docker-compose.yml"
  modified:
    - "README.md"
decisions:
  - "Kept it single-service: postgres only. Authentik stays on homelab (CONTEXT.md D-17); Ktor server runs via Gradle on the dev host for fast iteration."
  - "Pinned postgres:16 (not :latest, not :15) matching D-17 scope statement."
  - "No version: key in compose file — modern docker compose v2 treats it as legacy and emits warnings."
  - "No .env file in this plan — inline POSTGRES_* is fine for single-dev + matching application.conf defaults (D-17 / PATTERNS.md recommendation)."
  - "Port binding 5432:5432 is dev-local; README calls it out. Phase 11 homelab compose will use a different approach."
metrics:
  duration_seconds: 92
  duration_human: "1m32s"
  tasks_completed: 2
  files_created: 1
  files_modified: 1
  completed_at: "2026-04-24T16:22:48Z"
---

# Phase 01 Plan 06: Dev ergonomics — docker-compose + README Local development summary

Shipped `docker-compose.yml` (single postgres:16 service, named volume, healthcheck — credentials matching Plan 05's `application.conf` HOCON defaults exactly) and a "Local development" README section documenting the `docker compose up -d postgres && ./gradlew :server:run && curl /health` dev loop, while dropping the legacy `js` target docs per D-01.

## What was built

### docker-compose.yml (20 lines)

- `services.postgres`:
  - `image: postgres:16` (pinned major version)
  - `container_name: recipe-postgres`
  - `environment`: `POSTGRES_DB / POSTGRES_USER / POSTGRES_PASSWORD` all literal `recipe`
  - `ports: "5432:5432"` (dev-local loopback via host Docker)
  - `volumes: recipe-pgdata:/var/lib/postgresql/data` (persistence)
  - `healthcheck`: `pg_isready -U recipe -d recipe` every 5s, timeout 5s, 5 retries
- Top-level `volumes.recipe-pgdata:` (named volume declaration)
- No `version:` key (modern compose v2)
- No additional services (no Authentik — lives on user's homelab per D-17)

### README.md edits

**Edit A — dropped js target block** (lines 77-85 of previous README): the "- for the JS target (slower, supports older browsers)" paragraph and its two command blocks were deleted. The `wasmJs` paragraph is preserved intact.

**Edit B — inserted new "Local development" section** (after the iOS subsection, before the trailing `---` horizontal rule):

- Two-command boot: `docker compose up -d postgres` + `./gradlew :server:run`
- Smoke test: `curl http://localhost:8080/health` with expected `{"status":"ok"}` response
- Documented env-var overrides: `DATABASE_URL`, `DATABASE_USER`, `DATABASE_PASSWORD`, `PORT`
- Pre-commit formatter hint: `./gradlew spotlessApply` (D-10)
- Full-suite: `./gradlew check`
- DB reset: `docker compose down -v` (destroys `recipe-pgdata`)

All other existing headings (Android, Desktop/JVM, Server, iOS, web `wasmJs`) and the top introduction (lines 1-20) are unchanged. The trailing `---` + learn-more links paragraph is unchanged.

## Credential-match contract with Plan 05

The three compose env-vars are byte-identical to the literals in `server/src/main/resources/application.conf`:

| compose env | application.conf |
|-------------|------------------|
| `POSTGRES_DB: recipe` | JDBC URL path `/recipe` |
| `POSTGRES_USER: recipe` | `user = "recipe"` |
| `POSTGRES_PASSWORD: recipe` | `password = "recipe"` |

Verified via `grep -c '^\s*POSTGRES_\(DB\|USER\|PASSWORD\): recipe$' docker-compose.yml` → `3`.

## Requirements addressed

- **INFRA-02** — local development environment via `docker-compose.yml` and README dev loop documentation.

## Tasks executed

| Task | Name | Commit | Files |
|------|------|--------|-------|
| 1 | Create docker-compose.yml at repo root | `af4428f` | docker-compose.yml (new) |
| 2 | Add "Local development" section to README.md and drop js target docs | `f691400` | README.md (modified) |

## Deviations from Plan

None — plan executed exactly as written. No Rule 1-3 auto-fixes, no checkpoints, no auth gates. Both `<automated>` verify blocks and every acceptance criterion passed on first attempt.

## Threat surface scan

No new network endpoints, auth paths, file access patterns, or schema changes at trust boundaries were introduced beyond what the plan's `<threat_model>` already covers (T-01-06-01..04). The `5432:5432` host binding and literal `recipe/recipe/recipe` credentials are the exact surface the plan's STRIDE register dispositions (`mitigate`/`accept`) already cover. No new flags.

## Known stubs

None. Both deliverables are complete — no placeholders, no TODOs, no empty data paths.

## Verification

**Task 1 automated check:**
```
test -f docker-compose.yml && grep -q 'image: postgres:16' ... && grep -q 'pg_isready -U recipe -d recipe' ... && grep -q '^volumes:$' ...
→ VERIFY PASS
grep -c '^\s*POSTGRES_\(DB\|USER\|PASSWORD\): recipe$' docker-compose.yml → 3
```

**Task 2 automated check:**
```
grep -q 'Local development' && grep -q 'docker compose up -d postgres' && grep -q 'curl http://localhost:8080/health' && grep -q 'DATABASE_URL' && grep -q 'gradlew spotlessApply' && grep -q 'docker compose down -v' && ! grep -q 'jsBrowserDevelopmentRun' && grep -q 'wasmJsBrowserDevelopmentRun'
→ VERIFY PASS
```

**Acceptance criteria — Task 2 individually confirmed:**
- `Local development` appears exactly once (section heading)
- All 4 env-vars listed: `DATABASE_URL`, `DATABASE_USER`, `DATABASE_PASSWORD`, `PORT`
- `gradlew check` present
- Existing section headings (Android / Desktop (JVM) / Server / iOS) all preserved (grep `-c` → `1` each)
- `jsBrowserDevelopmentRun` absent; `wasmJsBrowserDevelopmentRun` present
- Top introduction (lines 1-20) unchanged

## Manual sanity checks (optional, not blocking)

Skipped per plan `<verification>`:
- `docker compose config` YAML parse — not blocking per plan; docker may not be running in this worktree sandbox.
- `docker compose up -d postgres && pg_isready` live test — not required; will be validated in Phase 3 when migrations land.

## Notes for downstream plans

- **Plan 05** (this wave) — credential contract lives in both files; any future change to the `recipe/recipe/recipe` triple MUST update both `application.conf` AND `docker-compose.yml` in the same commit.
- **Phase 3** (Households + DB migrations) — can add `depends_on: { postgres: { condition: service_healthy } }` to a future `server` service in compose if we ever run the Ktor server in Docker; the healthcheck is already wired for it.
- **Phase 11** (homelab deployment) — will ship a separate compose file (not editing this one) because homelab creds are secret and this file's creds are deliberately non-secret literals.

## Self-Check: PASSED

- `docker-compose.yml` exists at repo root: FOUND
- `README.md` contains "Local development" section: FOUND
- Commit `af4428f` (Task 1): FOUND in `git log`
- Commit `f691400` (Task 2): FOUND in `git log`
- All acceptance criteria from both tasks verified via grep
- No file deletions in either commit
