feat(infra): local serve-static profile — Caddy reverse proxy for the prod build
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:
@@ -27,9 +27,11 @@ REDIS_PORT=6379
|
||||
OTEL_GRPC_PORT=4317
|
||||
OTEL_HTTP_PORT=4318
|
||||
|
||||
# ---------------------------------------------------------------- Optional viewers
|
||||
# ---------------------------------------------------------------- Optional viewers / tooling
|
||||
# Only consumed when the matching Compose profile is activated:
|
||||
# --profile dbtools → pgweb
|
||||
# --profile observability → Jaeger UI
|
||||
# --profile serve-static → Caddy serving the production build
|
||||
PGWEB_PORT=8081
|
||||
JAEGER_UI_PORT=16686
|
||||
SERVE_STATIC_PORT=4200
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
# Local manual-test reverse proxy for `portal-shell`'s production
|
||||
# build, simulating what the on-prem reverse proxy will do (per
|
||||
# ADR-0019 §"URL strategy"):
|
||||
#
|
||||
# - GET / → smart redirect to /{locale}/ based on
|
||||
# Accept-Language (cookie support lands when
|
||||
# the BFF route is in place). Falls back to
|
||||
# `/fr/` since the APF audience is francophone.
|
||||
# - GET /fr/<any> → serve `dist/.../browser/fr/<any>`, with SPA
|
||||
# fallback to `fr/index.html` for deep links.
|
||||
# - GET /en/<any> → mirror for the EN bundle.
|
||||
# - everything else → redirect to `/fr/` (avoids the http-server
|
||||
# directory-listing footgun we hit in the perf
|
||||
# gate while the previous fix landed).
|
||||
#
|
||||
# Used by:
|
||||
# - `./infra/local/dev.sh up serve-static` (after building the
|
||||
# production bundle with `nx build portal-shell -c=production`).
|
||||
# - Convenient for testing the locale switcher end-to-end, the
|
||||
# accessibility-route redirect, dark-mode preference plumbing,
|
||||
# etc. — anything that needs both locale folders served at their
|
||||
# own prefix.
|
||||
|
||||
{
|
||||
auto_https off
|
||||
admin off
|
||||
}
|
||||
|
||||
:4200 {
|
||||
encode gzip zstd
|
||||
|
||||
log {
|
||||
output stdout
|
||||
format console
|
||||
level INFO
|
||||
}
|
||||
|
||||
# Group the locale-routing logic in an explicit `route` block so
|
||||
# directive ordering follows source order, not Caddy's default
|
||||
# directive priority — handle / handle_path / redir would
|
||||
# otherwise interleave in surprising ways.
|
||||
route {
|
||||
# Smart redirect for the bare root.
|
||||
@root path /
|
||||
handle @root {
|
||||
@prefers_en header Accept-Language en*
|
||||
redir @prefers_en /en/ 302
|
||||
redir * /fr/ 302
|
||||
}
|
||||
|
||||
# French bundle. `handle_path` strips `/fr` before passing the
|
||||
# request through, so `try_files` looks at `<path>` relative
|
||||
# to the bundle root (`/srv/dist/fr/`). Missing files fall
|
||||
# back to `index.html`, exactly like a production deep-link
|
||||
# (e.g. /fr/accessibility) would do via a real proxy.
|
||||
handle_path /fr/* {
|
||||
root * /srv/dist/fr
|
||||
try_files {path} /index.html
|
||||
file_server
|
||||
}
|
||||
|
||||
# English bundle, mirror of the above.
|
||||
handle_path /en/* {
|
||||
root * /srv/dist/en
|
||||
try_files {path} /index.html
|
||||
file_server
|
||||
}
|
||||
|
||||
# Anything that didn't match a locale prefix falls back to FR
|
||||
# home — same defaulting as the bare root above. Keeps
|
||||
# typo'd URLs from landing on a 404.
|
||||
redir * /fr/ 302
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,13 @@
|
||||
# BFF and the SPA, debug-logs traces in
|
||||
# v1, forwards to Jaeger when activated)
|
||||
#
|
||||
# Optional viewers behind Compose profiles:
|
||||
# Optional viewers / tooling behind Compose profiles:
|
||||
# - --profile dbtools → pgweb (Postgres GUI)
|
||||
# - --profile observability → Jaeger UI (trace explorer)
|
||||
# - --profile serve-static → Caddy reverse proxy that serves the
|
||||
# production build with the per-locale
|
||||
# routing the production reverse proxy
|
||||
# will use (per ADR-0019)
|
||||
#
|
||||
# Usage from infra/local/:
|
||||
# cp .env.example .env
|
||||
@@ -151,6 +155,40 @@ services:
|
||||
networks:
|
||||
- apf-portal-dev
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Optional: Caddy reverse proxy for the production build
|
||||
# (`--profile serve-static`).
|
||||
#
|
||||
# Workflow:
|
||||
# pnpm exec nx build portal-shell --configuration=production
|
||||
# ./infra/local/dev.sh up serve-static
|
||||
# open http://localhost:${SERVE_STATIC_PORT:-4200}/
|
||||
#
|
||||
# Mirrors what the production reverse proxy will do per ADR-0019:
|
||||
# smart redirect at `/` (Accept-Language → `/fr/` or `/en/`),
|
||||
# per-locale SPA fallback, and `/{locale}/<deep-link>` routing.
|
||||
# No locking-down on TLS / auth — this is a local convenience that
|
||||
# boots in <1 s and binds to localhost only.
|
||||
# ------------------------------------------------------------------
|
||||
serve-static:
|
||||
image: caddy:2.10-alpine
|
||||
container_name: apf-portal-serve-static
|
||||
restart: unless-stopped
|
||||
profiles: [serve-static]
|
||||
command: ['caddy', 'run', '--config', '/etc/caddy/Caddyfile', '--adapter', 'caddyfile']
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
# Bind-mount the production build folder read-only. The path
|
||||
# resolves relative to this compose file, i.e.
|
||||
# <repo>/dist/apps/portal-shell/browser/. Empty until the
|
||||
# production build runs — caddy will 404 every request in that
|
||||
# state, which is the right signal.
|
||||
- ../../dist/apps/portal-shell/browser:/srv/dist:ro
|
||||
ports:
|
||||
- '${SERVE_STATIC_PORT:-4200}:4200'
|
||||
networks:
|
||||
- apf-portal-dev
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
name: apf-portal-postgres-data
|
||||
|
||||
+2
-1
@@ -16,7 +16,7 @@ COMPOSE_FILE="${SCRIPT_DIR}/dev.compose.yml"
|
||||
|
||||
# Profiles defined in dev.compose.yml. Keep in sync if a new profile
|
||||
# is added.
|
||||
ALL_PROFILES=(dbtools observability)
|
||||
ALL_PROFILES=(dbtools observability serve-static)
|
||||
|
||||
# Build "--profile p1 --profile p2 …" as separate arguments.
|
||||
build_all_profile_flags() {
|
||||
@@ -59,6 +59,7 @@ Commands:
|
||||
all core + all profiles
|
||||
dbtools core + pgweb
|
||||
observability core + jaeger
|
||||
serve-static core + caddy (production-build reverse proxy)
|
||||
Multiple targets allowed (e.g. `up dbtools observability`).
|
||||
|
||||
down [-v] Tear the stack down. Always runs with every
|
||||
|
||||
Reference in New Issue
Block a user