feat(infra): local serve-static profile — Caddy reverse proxy for the prod build #97

Merged
julien merged 1 commits from feat/infra/local-serve-static into main 2026-05-12 00:06:34 +02:00
Owner

Summary

Add a Caddy reverse proxy behind a new --profile serve-static so a contributor can exercise the production build locally with the per-locale routing the on-prem reverse proxy will use (per ADR-0019). Closes the gap surfaced by PR #96: the locale switcher / accessibility 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, exposes the http-server directory-listing footgun we hit during the perf-gate fix in PR #92).

What lands

  • infra/local/Caddyfile — explicit route block:
    • GET / → 302 to /{locale}/ based on Accept-Language, falling back to /fr/ (APF audience).
    • /fr/*dist/.../browser/fr/ with SPA fallback to fr/index.html.
    • /en/* → mirror.
    • Catch-all → 302 to /fr/.
  • infra/local/dev.compose.yml — new serve-static service on the serve-static profile. Bind-mounts the Caddyfile and dist/apps/portal-shell/browser/ read-only. Port 4200, overridable via SERVE_STATIC_PORT.
  • 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 — file row, workflow snippet, cheat-sheet row, and a service-endpoint row with the nx build … -c=production prerequisite called out.

Workflow

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

Decision worth flagging

Used Caddy rather than nginx or Traefik. Reason: minimal Caddyfile, single binary, no daemon config drift, sensible defaults (TLS off explicitly for local-only). Same family of choice as the rest of infra/local/ — small, single-purpose images.

redir in a Caddyfile is ambiguous when the first arg starts with /: Caddy reads it as a path matcher rather than a redirect target. Using redir * /fr/ 302 (explicit * matcher) avoids the gotcha. Documented inline in the Caddyfile via a comment block.

What this PR explicitly does NOT do

  • Wire TLS. Local convenience only, binds to localhost.
  • Replace nx run portal-shell:serve-static (still used by Lighthouse CI in ci:perf).
  • Set the __Host-portal_locale cookie or honour it for the smart redirect. Cookie handling needs the BFF route (ADR-0019 future PR).
  • Land an on-prem reverse-proxy ADR. The on-prem infra ADR is phase 3b.

Verified locally

Probe Expected Actual
GET / -H 'Accept-Language: fr' 302 /fr/
GET / -H '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 (asset under locale folder)
/fr/index.html markup lang="fr", <base href="/fr/">
/en/index.html markup lang="en", <base href="/en/">

Test plan

  • docker compose -f infra/local/dev.compose.yml --profile serve-static config validates clean.
  • dev.sh up serve-static brings the container up; dev.sh status lists it; dev.sh stop serve-static brings it down.
  • Routing probes above all pass.
  • Manual: build + serve-static + click the locale switcher → URL becomes /{other-locale}/, the matching bundle boots, no console errors. (Verifies PR #95 + #96 end-to-end against a prod-faithful proxy.)
  • Manual: /fr/accessibilite → router-level redirect to /fr/accessibility (verifies PR #94 under SPA fallback).
  • Manual: Accept-Language: en in browser settings → root URL lands on /en/.
## Summary Add a Caddy reverse proxy behind a new `--profile serve-static` so a contributor can exercise the production build locally with the per-locale routing the on-prem reverse proxy will use (per ADR-0019). Closes the gap surfaced by PR #96: the locale switcher / accessibility 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, exposes the `http-server` directory-listing footgun we hit during the perf-gate fix in PR #92). ## What lands - **[`infra/local/Caddyfile`](infra/local/Caddyfile)** — explicit `route` block: - `GET /` → 302 to `/{locale}/` based on `Accept-Language`, falling back to `/fr/` (APF audience). - `/fr/*` → `dist/.../browser/fr/` with SPA fallback to `fr/index.html`. - `/en/*` → mirror. - Catch-all → 302 to `/fr/`. - **[`infra/local/dev.compose.yml`](infra/local/dev.compose.yml)** — new `serve-static` service on the `serve-static` profile. Bind-mounts the Caddyfile and `dist/apps/portal-shell/browser/` read-only. Port 4200, overridable via `SERVE_STATIC_PORT`. - **[`infra/local/.env.example`](infra/local/.env.example)** — adds `SERVE_STATIC_PORT=4200`. - **[`infra/local/dev.sh`](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`](infra/README.md)** — file row, workflow snippet, cheat-sheet row, and a service-endpoint row with the `nx build … -c=production` prerequisite called out. ## Workflow ``` 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 ``` ## Decision worth flagging Used **Caddy** rather than nginx or Traefik. Reason: minimal Caddyfile, single binary, no daemon config drift, sensible defaults (TLS off explicitly for local-only). Same family of choice as the rest of `infra/local/` — small, single-purpose images. `redir` in a Caddyfile is **ambiguous** when the first arg starts with `/`: Caddy reads it as a path matcher rather than a redirect target. Using `redir * /fr/ 302` (explicit `*` matcher) avoids the gotcha. Documented inline in the Caddyfile via a comment block. ## What this PR explicitly does NOT do - Wire TLS. Local convenience only, binds to `localhost`. - Replace `nx run portal-shell:serve-static` (still used by Lighthouse CI in `ci:perf`). - Set the `__Host-portal_locale` cookie or honour it for the smart redirect. Cookie handling needs the BFF route (ADR-0019 future PR). - Land an on-prem reverse-proxy ADR. The on-prem infra ADR is phase 3b. ## Verified locally | Probe | Expected | Actual | |---|---|---| | `GET / -H 'Accept-Language: fr'` | 302 `/fr/` | ✓ | | `GET / -H '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 (asset under locale folder) | ✓ | | `/fr/index.html` markup | `lang="fr"`, `<base href="/fr/">` | ✓ | | `/en/index.html` markup | `lang="en"`, `<base href="/en/">` | ✓ | ## Test plan - [x] `docker compose -f infra/local/dev.compose.yml --profile serve-static config` validates clean. - [x] `dev.sh up serve-static` brings the container up; `dev.sh status` lists it; `dev.sh stop serve-static` brings it down. - [x] Routing probes above all pass. - [ ] Manual: build + serve-static + click the locale switcher → URL becomes `/{other-locale}/`, the matching bundle boots, no console errors. (Verifies PR #95 + #96 end-to-end against a prod-faithful proxy.) - [ ] Manual: `/fr/accessibilite` → router-level redirect to `/fr/accessibility` (verifies PR #94 under SPA fallback). - [ ] Manual: `Accept-Language: en` in browser settings → root URL lands on `/en/`.
julien added 1 commit 2026-05-12 00:06:16 +02:00
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
1a8e2cbb21
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).
julien merged commit fe180fd125 into main 2026-05-12 00:06:34 +02:00
julien deleted branch feat/infra/local-serve-static 2026-05-12 00:06:35 +02:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: julien/apf_portal#97