feat(infra): local serve-static profile — Caddy reverse proxy for the prod build
CI / commits (pull_request) Successful in 2m33s
CI / scan (pull_request) Successful in 2m33s
CI / check (pull_request) Successful in 2m41s
CI / a11y (pull_request) Successful in 2m18s
CI / perf (pull_request) Successful in 5m50s

Add a Caddy reverse proxy behind a new `--profile serve-static` so a
contributor can exercise the production build with the per-locale
routing that the on-prem reverse proxy will use (ADR-0019). Closes
the gap PR #96 surfaced: the locale switcher / route fusion / cookie
plumbing all need a prod-faithful local setup, and `nx serve-static`
falls short (no SPA fallback per locale, no smart `/` redirect).

What lands:

- `infra/local/Caddyfile` — three-block route:
    * `GET /` → smart redirect to `/{locale}/` based on
      `Accept-Language` (FR fallback, since the APF audience is
      francophone). Cookie support waits on the BFF route in
      ADR-0019.
    * `/fr/*` → serves `dist/.../browser/fr/` with SPA fallback to
      `index.html` for deep links.
    * `/en/*` → mirror.
    * Catch-all → bounces to `/fr/` so a typo can't land on the
      Caddy directory-listing footgun (same family as the perf-gate
      fix in #92).
- `infra/local/dev.compose.yml` — new `serve-static` service on the
  `serve-static` profile, bind-mounting the Caddyfile and the
  `dist/apps/portal-shell/browser/` folder (read-only). Default
  port 4200, override via `SERVE_STATIC_PORT` in `.env`.
- `infra/local/.env.example` — adds `SERVE_STATIC_PORT=4200`.
- `infra/local/dev.sh` — registers `serve-static` in
  `ALL_PROFILES` so `dev.sh down|status|logs` catches the new
  container, and `dev.sh up serve-static` works.
- `infra/README.md` — adds the new file row, the workflow in the
  "First-time setup" snippet, the cheat-sheet row, and the service
  endpoint row with the prerequisite `nx build … -c=production`.

Workflow once merged:

```
pnpm exec nx build portal-shell --configuration=production
./infra/local/dev.sh up serve-static
open http://localhost:4200/   # → /fr/ or /en/ per Accept-Language
```

Verified locally:

- GET / with Accept-Language=fr → 302 /fr/
- GET / with Accept-Language=en → 302 /en/
- GET /unknown → 302 /fr/
- GET /fr/deep/route → 200 (SPA fallback to fr/index.html)
- GET /fr/favicons/favicon.svg → 200 (per-locale asset served)
- /fr/index.html has `lang="fr"` and `<base href="/fr/">`
- /en/index.html has `lang="en"` and `<base href="/en/">`

This is local convenience only — no TLS, no auth, binds to
localhost. The on-prem reverse proxy gets its own ADR (phase 3b).
This commit is contained in:
Julien Gautier
2026-05-12 00:03:59 +02:00
parent d118d09aba
commit 1a8e2cbb21
5 changed files with 139 additions and 20 deletions
+21 -17
View File
@@ -140,15 +140,16 @@ Old, no-longer-referenced images can be reaped during the periodic `docker syste
## Local-dev stack — `local/`
A Docker Compose recipe spinning up the runtime services the BFF and ADRs assume — Postgres, Redis, OpenTelemetry Collector — plus optional viewers (pgweb, Jaeger UI) gated behind Compose profiles. Designed to start in a single command on a contributor's WSL2 / Linux / macOS host.
A Docker Compose recipe spinning up the runtime services the BFF and ADRs assume — Postgres, Redis, OpenTelemetry Collector — plus optional viewers / tooling (pgweb, Jaeger UI, Caddy serve-static) gated behind Compose profiles. Designed to start in a single command on a contributor's WSL2 / Linux / macOS host.
| File | Role |
| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| [`local/dev.sh`](local/dev.sh) | Convenience wrapper around `docker compose` — see "Convenience script" below |
| [`local/dev.compose.yml`](local/dev.compose.yml) | Service definitions: postgres, redis, otel-collector, plus pgweb and jaeger behind profiles |
| [`local/.env.example`](local/.env.example) | Credentials + ports template (copy to `.env`, which is git-ignored) |
| [`local/init/postgres/01-init.sql`](local/init/postgres/01-init.sql) | Bootstrap SQL for ADR-0013: audit roles + schema, applied on first boot only |
| [`local/otel-collector.yaml`](local/otel-collector.yaml) | Collector pipeline: OTLP receivers → batch → debug exporter (always) + forward to Jaeger when active |
| File | Role |
| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| [`local/dev.sh`](local/dev.sh) | Convenience wrapper around `docker compose` — see "Convenience script" below |
| [`local/dev.compose.yml`](local/dev.compose.yml) | Service definitions: postgres, redis, otel-collector, plus pgweb / jaeger / caddy behind profiles |
| [`local/.env.example`](local/.env.example) | Credentials + ports template (copy to `.env`, which is git-ignored) |
| [`local/init/postgres/01-init.sql`](local/init/postgres/01-init.sql) | Bootstrap SQL for ADR-0013: audit roles + schema, applied on first boot only |
| [`local/otel-collector.yaml`](local/otel-collector.yaml) | Collector pipeline: OTLP receivers → batch → debug exporter (always) + forward to Jaeger when active |
| [`local/Caddyfile`](local/Caddyfile) | Reverse-proxy config for the `serve-static` profile — per-locale SPA fallback + smart `/` redirect (ADR-0019) |
### First-time setup
@@ -163,9 +164,10 @@ $EDITOR infra/local/.env
# 2. Bring up the core stack (postgres + redis + otel-collector).
./infra/local/dev.sh up
# 3. (Optional) Activate viewers when needed:
# 3. (Optional) Activate viewers / tooling when needed:
./infra/local/dev.sh up dbtools # adds pgweb
./infra/local/dev.sh up observability # adds Jaeger UI
./infra/local/dev.sh up serve-static # adds caddy serving the prod build
./infra/local/dev.sh up all # core + every profile
# 4. Verify health.
@@ -187,6 +189,7 @@ Run `./infra/local/dev.sh help` for the full reference. Cheat-sheet:
| `./infra/local/dev.sh up all` | Core + every profile |
| `./infra/local/dev.sh up dbtools` | Core + pgweb |
| `./infra/local/dev.sh up observability` | Core + Jaeger |
| `./infra/local/dev.sh up serve-static` | Core + Caddy serving `dist/.../browser/` per ADR-0019 |
| `./infra/local/dev.sh down` | Tear down the whole stack (every profile in scope) |
| `./infra/local/dev.sh down -v` | Tear down + wipe named volumes (incl. audit-roles bootstrap) |
| `./infra/local/dev.sh stop pgweb` | Stop one service (containers stay around) |
@@ -200,14 +203,15 @@ If you prefer to call `docker compose` directly, every example below shows the r
### Service endpoints (defaults)
| Service | Host port | Purpose |
| ------------------- | --------- | ------------------------------------------------------------------- |
| Postgres | 5432 | DB connection — `postgres://portal:<pwd>@localhost:5432/portal_dev` |
| Redis | 6379 | Sessions, OBO cache (per ADR-0010 / ADR-0014) |
| OTel Collector gRPC | 4317 | `OTEL_EXPORTER_OTLP_ENDPOINT` for the BFF and the SPA |
| OTel Collector HTTP | 4318 | OTLP/HTTP variant |
| pgweb (profile) | 8081 | http://localhost:8081 — Postgres GUI |
| Jaeger UI (profile) | 16686 | http://localhost:16686 — trace explorer |
| Service | Host port | Purpose |
| ---------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Postgres | 5432 | DB connection — `postgres://portal:<pwd>@localhost:5432/portal_dev` |
| Redis | 6379 | Sessions, OBO cache (per ADR-0010 / ADR-0014) |
| OTel Collector gRPC | 4317 | `OTEL_EXPORTER_OTLP_ENDPOINT` for the BFF and the SPA |
| OTel Collector HTTP | 4318 | OTLP/HTTP variant |
| pgweb (profile) | 8081 | http://localhost:8081 — Postgres GUI |
| Jaeger UI (profile) | 16686 | http://localhost:16686 — trace explorer |
| Caddy serve-static (profile) | 4200 | http://localhost:4200/ — production build with per-locale routing (`/fr/`, `/en/`) + smart `/` redirect, per ADR-0019. Run `pnpm exec nx build portal-shell --configuration=production` first or the proxy will 404 everything. |
All ports are overridable via `.env` if the host machine has conflicts.