feat: add Docker Compose local dev stack #5

Merged
forgejo_admin merged 2 commits from 4-add-docker-compose-local-dev-stack-fully into main 2026-03-17 02:41:53 +00:00
Contributor

Summary

Fully self-contained 4-service Docker Compose stack (postgres, keycloak, api, app) for local development. Zero external dependencies — works offline. Production URLs are preserved as fallback defaults when env vars are unset.

Changes

  • docker-compose.yml — 4-service stack with health checks, proper depends_on ordering, and named volumes for persistence
  • dev/realm-export.json — Keycloak realm with PKCE client config and 3 test users (testuser, testadmin, emptyuser)
  • dev/seed-data.py — Python script that authenticates via Keycloak direct grant and seeds 7 Denver locations + 10 codes via the API
  • .env.example — Documents all VITE_* env vars for reference
  • src/lib/api.jsAPI_BASE now reads VITE_API_URL with production URL as fallback
  • src/lib/keycloak.js — Keycloak config now reads VITE_KEYCLOAK_URL, VITE_KEYCLOAK_REALM, VITE_KEYCLOAK_CLIENT_ID with production fallbacks

Test Plan

  1. docker compose up -d — all 4 services should start and become healthy
  2. Visit http://localhost:5173 — app loads, Keycloak login redirects to localhost:8080
  3. Log in as testuser / testpass — should authenticate and reach home screen
  4. Run python dev/seed-data.py — should create locations and codes for testuser/testadmin
  5. Verify production deploy is unaffected (no env vars set = production URLs used)

Review Checklist

  • docker compose config validates successfully
  • Production fallback defaults preserved — no env vars = connects to production
  • Dockerfile and .woodpecker.yaml not modified
  • Keycloak realm export includes PKCE config matching production client
  • Seed script uses correct API endpoints (POST /locations, POST /locations/{id}/codes, PATCH /codes/{id}/redeem)
  • Forgejo issue: #4

Closes #4

## Summary Fully self-contained 4-service Docker Compose stack (postgres, keycloak, api, app) for local development. Zero external dependencies — works offline. Production URLs are preserved as fallback defaults when env vars are unset. ## Changes - **`docker-compose.yml`** — 4-service stack with health checks, proper `depends_on` ordering, and named volumes for persistence - **`dev/realm-export.json`** — Keycloak realm with PKCE client config and 3 test users (testuser, testadmin, emptyuser) - **`dev/seed-data.py`** — Python script that authenticates via Keycloak direct grant and seeds 7 Denver locations + 10 codes via the API - **`.env.example`** — Documents all `VITE_*` env vars for reference - **`src/lib/api.js`** — `API_BASE` now reads `VITE_API_URL` with production URL as fallback - **`src/lib/keycloak.js`** — Keycloak config now reads `VITE_KEYCLOAK_URL`, `VITE_KEYCLOAK_REALM`, `VITE_KEYCLOAK_CLIENT_ID` with production fallbacks ## Test Plan 1. `docker compose up -d` — all 4 services should start and become healthy 2. Visit `http://localhost:5173` — app loads, Keycloak login redirects to `localhost:8080` 3. Log in as `testuser` / `testpass` — should authenticate and reach home screen 4. Run `python dev/seed-data.py` — should create locations and codes for testuser/testadmin 5. Verify production deploy is unaffected (no env vars set = production URLs used) ## Review Checklist - [x] `docker compose config` validates successfully - [x] Production fallback defaults preserved — no env vars = connects to production - [x] Dockerfile and .woodpecker.yaml not modified - [x] Keycloak realm export includes PKCE config matching production client - [x] Seed script uses correct API endpoints (POST /locations, POST /locations/{id}/codes, PATCH /codes/{id}/redeem) ## Related - Forgejo issue: #4 Closes #4
feat: add Docker Compose local dev stack
All checks were successful
ci/woodpecker/pr/woodpecker Pipeline was successful
70467116ed
Fully self-contained 4-service stack: postgres, keycloak, api, app.
Zero external dependencies. Works offline.

- docker-compose.yml with health checks and proper depends_on
- dev/realm-export.json for local Keycloak with test users
- dev/seed-data.py for populating test data
- .env.example documenting all VITE_* env vars
- api.js and keycloak.js now use env vars with production fallbacks

Closes #4

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Contributor

Self-Review

Reviewed full diff (6 files, +404/-4). No issues found.

Verified:

  • api.js and keycloak.js changes are minimal -- env var reads with production fallback defaults only
  • docker-compose.yml validates (docker compose config passed), health checks use correct patterns (pg_isready, exec tcp for keycloak)
  • dev/realm-export.json is valid Keycloak realm export with PKCE S256, public client, 3 test users with correct role mappings
  • dev/seed-data.py uses correct API endpoints (POST /locations, POST /locations/{id}/codes, PATCH /codes/{id}/redeem), includes service readiness wait
  • .env.example covered by existing .gitignore exception (!.env.example)
  • Dockerfile and .woodpecker.yaml untouched
  • No secrets committed
## Self-Review Reviewed full diff (6 files, +404/-4). No issues found. **Verified:** - `api.js` and `keycloak.js` changes are minimal -- env var reads with production fallback defaults only - `docker-compose.yml` validates (`docker compose config` passed), health checks use correct patterns (pg_isready, exec tcp for keycloak) - `dev/realm-export.json` is valid Keycloak realm export with PKCE S256, public client, 3 test users with correct role mappings - `dev/seed-data.py` uses correct API endpoints (POST /locations, POST /locations/{id}/codes, PATCH /codes/{id}/redeem), includes service readiness wait - `.env.example` covered by existing `.gitignore` exception (`!.env.example`) - Dockerfile and .woodpecker.yaml untouched - No secrets committed
Author
Contributor

PR #5 Review

DOMAIN REVIEW

Tech stack identified: Docker Compose (dev infra), SvelteKit/JS (frontend config), Python (seed script), Keycloak realm JSON (OIDC config).

Docker Compose (docker-compose.yml)

  • postgres: Correct image (16-alpine), proper healthcheck using pg_isready, named volume for persistence. Good.
  • keycloak: Good image pinning (26.0.7), start-dev --import-realm is correct for dev. Healthcheck uses exec 3<>/dev/tcp/localhost/8080 instead of curl -- correct, since curl is not available in the Keycloak image. start_period: 30s gives Keycloak time to boot. Good.
  • api: Build context ../mcd-tracker-api assumes the API repo is checked out as a sibling directory. This is correct per repo layout conventions. depends_on uses service_healthy conditions for both postgres and keycloak. The API runs Alembic migrations on startup (confirmed in mcd_tracker_api/main.py:35), so no separate migration step needed. Environment variables match the MCD_TRACKER_ prefix in the API's pydantic settings config. Good.
  • api missing healthcheck: The api service has no healthcheck defined. The app service depends on api with a bare - api (no health condition), so the app container starts as soon as the API container is running, not necessarily ready. For a dev stack this is acceptable -- the Vite dev server will just show connection errors until the API is up. Not a blocker.
  • app: Uses node:22-alpine, bind-mounts the source code, and uses a named volume for node_modules to avoid platform-specific binary conflicts. npm install && npm run dev -- --host is the standard pattern. Good.
  • Credentials: mcd_tracker_dev (postgres) and admin/admin (keycloak admin) are dev-only credentials, plaintext in a dev compose file. This is acceptable -- these are local dev fixtures, not production secrets.

SvelteKit config changes

  • src/lib/api.js line 11: import.meta.env.VITE_API_URL || 'https://mcd-tracker.tail5b443a.ts.net' -- production fallback preserved. Correct.
  • src/lib/keycloak.js lines 12-14: All three VITE_KEYCLOAK_* vars with production fallbacks. Correct.
  • When no env vars are set (production deploy via static adapter), the hardcoded Tailscale URLs are used. When Docker Compose sets the vars, localhost URLs are used. This pattern is sound.

Keycloak realm export (dev/realm-export.json)

  • Valid structure. Realm name matches (mcd-tracker).
  • Client mcd-tracker-app: publicClient: true, standardFlowEnabled: true, directAccessGrantsEnabled: true (needed for seed script's resource owner password grant). PKCE configured with S256. Good.
  • Redirect URIs include http://localhost:5173/* and capacitor://localhost/*. Web origins match. Good.
  • sslRequired: "none" is correct for local dev (no TLS on localhost).
  • Three test users with temporary: false passwords. Role assignments look correct (testadmin gets both user and admin).

Seed script (dev/seed-data.py)

  • Uses direct grant (grant_type: password) with the Keycloak token endpoint. Correct auth flow for scripted seeding.
  • Calls POST /locations, POST /locations/{id}/codes, PATCH /codes/{id}/redeem -- these match the API router structure (confirmed: locations_router and codes_router are included in main.py).
  • Type hints, docstrings, clean error handling. Good Python quality.
  • Graceful requests import check with helpful error message.

.env.example

  • Documents all four VITE_* vars. .gitignore excludes .env and .env.* but explicitly includes !.env.example. Good.

BLOCKERS

None.

This PR adds dev infrastructure (Docker Compose, realm config, seed script) and minimal config changes to existing source files (env var with fallback). No new user-facing functionality requiring test coverage. The source changes (2 files, 6 lines changed) are purely additive env-var reads with fallback defaults -- no behavioral change in production.

NITS

  1. Seed script health check URL mismatch: wait_for_services() in dev/seed-data.py line 149 checks {API_URL}/health, but the actual API health endpoint is /healthz (see /home/ldraney/mcd-tracker-api/src/mcd_tracker_api/routes/health.py:15). The wait loop still passes because 404 < 500, so the script won't fail. But it does not actually validate the API is healthy (the DB connectivity check in /healthz is never exercised). Should be changed to /healthz.

  2. API service has no healthcheck: Adding a healthcheck to the api service and using condition: service_healthy in the app service's depends_on would prevent the app from starting before the API is ready. Low priority for a dev stack.

  3. api.js doc comment stale: Line 4-5 in src/lib/api.js still says the hardcoded URL as if it is the only target. Consider updating the comment to mention the env var override.

  4. Seed script testadmin section is not DRY: Lines 206-211 duplicate the auth + location creation pattern that seed_user() already handles. Could call seed_user with a subset of locations, or parameterize seed_user to accept a location list. Minor.

  5. f-string without interpolation: Line 206 of dev/seed-data.py uses f"\nSeeding data for 'testadmin'..." but there is nothing to interpolate. Plain string would be cleaner.

SOP COMPLIANCE

  • Branch named after issue (4-add-docker-compose-local-dev-stack-fully references issue #4)
  • PR body has Summary, Changes, Test Plan, Related sections
  • Related section references issue #4 with "Closes #4"
  • Related section does not reference a plan slug (no plan slug visible -- may not apply to mcd-tracker-app if no active plan exists)
  • No secrets committed (dev-only credentials in fixtures, .env is gitignored, .env.example committed correctly)
  • No unnecessary file changes -- all 6 files are directly related to the Docker Compose local dev stack
  • Commit messages are descriptive (PR title: "feat: add Docker Compose local dev stack")

PROCESS OBSERVATIONS

  • Deployment frequency: This PR enables local development without depending on the production Tailscale cluster. This directly improves developer velocity and deployment frequency -- changes can be tested locally before pushing.
  • Change failure risk: Low. The production code path is unchanged when env vars are unset. The fallback-default pattern is clean.
  • Sibling repo dependency: The api service's build: ../mcd-tracker-api requires the API repo to be checked out alongside this repo. This is documented implicitly but could benefit from a note in the README or a comment in docker-compose.yml for onboarding.
  • Database migrations: The API runs Alembic migrations on startup, so the local Postgres starts fresh and gets migrated automatically. Good pattern.

VERDICT: APPROVED

Clean, well-structured local dev stack. Production fallbacks are preserved. No behavioral changes to production code paths. The health endpoint URL mismatch in the seed script (nit #1) is the most actionable item but is non-blocking since the script still functions correctly.

## PR #5 Review ### DOMAIN REVIEW **Tech stack identified:** Docker Compose (dev infra), SvelteKit/JS (frontend config), Python (seed script), Keycloak realm JSON (OIDC config). **Docker Compose (`docker-compose.yml`)** - **postgres**: Correct image (16-alpine), proper healthcheck using `pg_isready`, named volume for persistence. Good. - **keycloak**: Good image pinning (26.0.7), `start-dev --import-realm` is correct for dev. Healthcheck uses `exec 3<>/dev/tcp/localhost/8080` instead of curl -- correct, since curl is not available in the Keycloak image. `start_period: 30s` gives Keycloak time to boot. Good. - **api**: Build context `../mcd-tracker-api` assumes the API repo is checked out as a sibling directory. This is correct per repo layout conventions. `depends_on` uses `service_healthy` conditions for both postgres and keycloak. The API runs Alembic migrations on startup (confirmed in `mcd_tracker_api/main.py:35`), so no separate migration step needed. Environment variables match the `MCD_TRACKER_` prefix in the API's pydantic settings config. Good. - **api missing healthcheck**: The `api` service has no `healthcheck` defined. The `app` service depends on `api` with a bare `- api` (no health condition), so the app container starts as soon as the API container is _running_, not necessarily _ready_. For a dev stack this is acceptable -- the Vite dev server will just show connection errors until the API is up. Not a blocker. - **app**: Uses `node:22-alpine`, bind-mounts the source code, and uses a named volume for `node_modules` to avoid platform-specific binary conflicts. `npm install && npm run dev -- --host` is the standard pattern. Good. - **Credentials**: `mcd_tracker_dev` (postgres) and `admin/admin` (keycloak admin) are dev-only credentials, plaintext in a dev compose file. This is acceptable -- these are local dev fixtures, not production secrets. **SvelteKit config changes** - `src/lib/api.js` line 11: `import.meta.env.VITE_API_URL || 'https://mcd-tracker.tail5b443a.ts.net'` -- production fallback preserved. Correct. - `src/lib/keycloak.js` lines 12-14: All three `VITE_KEYCLOAK_*` vars with production fallbacks. Correct. - When no env vars are set (production deploy via static adapter), the hardcoded Tailscale URLs are used. When Docker Compose sets the vars, localhost URLs are used. This pattern is sound. **Keycloak realm export (`dev/realm-export.json`)** - Valid structure. Realm name matches (`mcd-tracker`). - Client `mcd-tracker-app`: `publicClient: true`, `standardFlowEnabled: true`, `directAccessGrantsEnabled: true` (needed for seed script's resource owner password grant). PKCE configured with `S256`. Good. - Redirect URIs include `http://localhost:5173/*` and `capacitor://localhost/*`. Web origins match. Good. - `sslRequired: "none"` is correct for local dev (no TLS on localhost). - Three test users with `temporary: false` passwords. Role assignments look correct (testadmin gets both `user` and `admin`). **Seed script (`dev/seed-data.py`)** - Uses direct grant (`grant_type: password`) with the Keycloak token endpoint. Correct auth flow for scripted seeding. - Calls `POST /locations`, `POST /locations/{id}/codes`, `PATCH /codes/{id}/redeem` -- these match the API router structure (confirmed: `locations_router` and `codes_router` are included in `main.py`). - Type hints, docstrings, clean error handling. Good Python quality. - Graceful `requests` import check with helpful error message. **`.env.example`** - Documents all four `VITE_*` vars. `.gitignore` excludes `.env` and `.env.*` but explicitly includes `!.env.example`. Good. ### BLOCKERS None. This PR adds dev infrastructure (Docker Compose, realm config, seed script) and minimal config changes to existing source files (env var with fallback). No new user-facing functionality requiring test coverage. The source changes (2 files, 6 lines changed) are purely additive env-var reads with fallback defaults -- no behavioral change in production. ### NITS 1. **Seed script health check URL mismatch**: `wait_for_services()` in `dev/seed-data.py` line 149 checks `{API_URL}/health`, but the actual API health endpoint is `/healthz` (see `/home/ldraney/mcd-tracker-api/src/mcd_tracker_api/routes/health.py:15`). The wait loop still passes because `404 < 500`, so the script won't fail. But it does not actually validate the API is healthy (the DB connectivity check in `/healthz` is never exercised). Should be changed to `/healthz`. 2. **API service has no healthcheck**: Adding a healthcheck to the `api` service and using `condition: service_healthy` in the `app` service's `depends_on` would prevent the app from starting before the API is ready. Low priority for a dev stack. 3. **`api.js` doc comment stale**: Line 4-5 in `src/lib/api.js` still says the hardcoded URL as if it is the only target. Consider updating the comment to mention the env var override. 4. **Seed script `testadmin` section is not DRY**: Lines 206-211 duplicate the auth + location creation pattern that `seed_user()` already handles. Could call `seed_user` with a subset of locations, or parameterize `seed_user` to accept a location list. Minor. 5. **f-string without interpolation**: Line 206 of `dev/seed-data.py` uses `f"\nSeeding data for 'testadmin'..."` but there is nothing to interpolate. Plain string would be cleaner. ### SOP COMPLIANCE - [x] Branch named after issue (`4-add-docker-compose-local-dev-stack-fully` references issue #4) - [x] PR body has Summary, Changes, Test Plan, Related sections - [x] Related section references issue #4 with "Closes #4" - [ ] Related section does not reference a plan slug (no plan slug visible -- may not apply to mcd-tracker-app if no active plan exists) - [x] No secrets committed (dev-only credentials in fixtures, `.env` is gitignored, `.env.example` committed correctly) - [x] No unnecessary file changes -- all 6 files are directly related to the Docker Compose local dev stack - [x] Commit messages are descriptive (PR title: "feat: add Docker Compose local dev stack") ### PROCESS OBSERVATIONS - **Deployment frequency**: This PR enables local development without depending on the production Tailscale cluster. This directly improves developer velocity and deployment frequency -- changes can be tested locally before pushing. - **Change failure risk**: Low. The production code path is unchanged when env vars are unset. The fallback-default pattern is clean. - **Sibling repo dependency**: The `api` service's `build: ../mcd-tracker-api` requires the API repo to be checked out alongside this repo. This is documented implicitly but could benefit from a note in the README or a comment in docker-compose.yml for onboarding. - **Database migrations**: The API runs Alembic migrations on startup, so the local Postgres starts fresh and gets migrated automatically. Good pattern. ### VERDICT: APPROVED Clean, well-structured local dev stack. Production fallbacks are preserved. No behavioral changes to production code paths. The health endpoint URL mismatch in the seed script (nit #1) is the most actionable item but is non-blocking since the script still functions correctly.
fix: use correct /healthz endpoint in seed script
All checks were successful
ci/woodpecker/pr/woodpecker Pipeline was successful
91e14f4737
QA caught that wait_for_services() was checking /health (404) instead
of /healthz (the actual health endpoint). The check happened to pass
because 404 < 500, but it wasn't validating API+DB readiness.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
forgejo_admin deleted branch 4-add-docker-compose-local-dev-stack-fully 2026-03-17 02:41:53 +00:00
Commenting is not possible because the repository is archived.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ldraney/mcd-tracker-app!5
No description provided.