db7e479dde
## Summary
Add an opt-in `https` configuration to the SPA dev-servers so the ADR-0030 dockerised dev mode can be reached over a hostname registered in Entra. Entra refuses `http:` redirect URIs for anything other than `localhost`, which made the hostname-based access pattern (`apf-portal.dev-jg.local`, `apf-portal.dev.local`, …) — the only stable way to share a VM-based dev stack with another developer — impossible to wire to OIDC. This PR closes that gap without touching the WSL-native + localhost flow.
## What lands
| File | Change |
| --- | --- |
| `apps/portal-shell/project.json`, `apps/portal-admin/project.json` | New `https` Nx serve configuration: inherits the `development` build, sets `ssl: true` + `sslKey: .secrets/dev-tls.key` + `sslCert: .secrets/dev-tls.pem`. `defaultConfiguration` stays `development`; the `https` config is purely opt-in. |
| `infra/local/dev.compose.yml` | The `portal-shell` and `portal-admin` commands now end with `--configuration=${NX_SERVE_CONFIGURATION:-development}`. Compose interpolates the value at YAML parse time from `infra/local/.env`. Default is `development` (no SSL), so behaviour is unchanged for anyone who doesn't opt in. |
| `infra/local/.env.example` | New commented-out `NX_SERVE_CONFIGURATION=https` block with the rationale + pointer to the mkcert setup. |
| `apps/portal-bff/.env.example` | Comment block above the `ENTRA_*_REDIRECT_URI` defaults shows the HTTPS hostname-based override pattern (the four URIs that go with the `apps` profile when accessing via a hostname) and reminds that each override must be registered Entra-side. |
| `infra/README.md` | New "HTTPS dev-server setup — remote-browser access via a hostname" subsection: mkcert install / `mkcert -install`, cert generation, `.secrets/dev-tls.{key,pem}` convention, Entra registration reminder, `NX_SERVE_CONFIGURATION=https` opt-in. Notes that WSL-native is unaffected and that the cert path stays the same when the corp CA eventually replaces mkcert. |
## Design notes
- **Convention `.secrets/dev-tls.{key,pem}` at repo root.** Matches the existing `apps/portal-bff/.secrets/jwks.pem` pattern (gitignored via `*.pem` + `*.key`). Workspace-relative path means project.json can hardcode it and each dev drops their per-host cert there.
- **Hardcoded path, per-dev cert content.** Each developer generates a cert for **their own** hostname; the cert sits at the same fixed path on every machine. Nothing dev-specific in `project.json`.
- **`https` is opt-in, not default.** Native `nx serve` keeps booting on HTTP (`localhost:4200`) without SSL key files, exactly as before. Compose default is also `development` — only setting `NX_SERVE_CONFIGURATION=https` in `infra/local/.env` switches it on, gated by the dev having actually run the mkcert step.
- **BFF stays plain HTTP.** Only the SPA dev-server terminates TLS — the proxy then hits `http://portal-bff:3000` on the internal Compose network. Entra still gets HTTPS at the browser-facing origin, which is what its policy enforces.
## What this PR deliberately does NOT do
- It does **not** force-enable HTTPS. Devs who don't care about hostname access continue working as before.
- It does **not** touch the BFF code, the Entra config helpers, or the auth flow itself. The whole change is config (project.json + compose + env-examples) + docs.
- It does **not** ship the shared VM cert story. That needs a corp-CA-signed cert (or a shared mkcert CA distributed across workstations); flagged in the README section as a follow-up.
## Test plan
- [x] Both `project.json` files parse as JSON; `nx show project` exposes the new `https` configuration with the expected SSL options.
- [x] `docker compose -f dev.compose.yml --profile apps config` validates with `NX_SERVE_CONFIGURATION=https` (resolves to `--configuration=https`) and without it (resolves to `--configuration=development`).
- [ ] **On vm-jg**: `mkcert -install` on the workstation, `mkcert` against `apf-portal.dev-jg.local`, copy `.secrets/dev-tls.{key,pem}` to the VM, set `NX_SERVE_CONFIGURATION=https` in `infra/local/.env`, register the four `https://apf-portal.dev-jg.local:*` URIs in Entra, restart `dev.sh up apps`, then open `https://apf-portal.dev-jg.local:4200/` from the workstation → SPA loads, no cert warning, sign-in completes and returns to the SPA via the OIDC callback.
- [ ] Native WSL flow unchanged: `nx serve portal-shell` still boots on `http://localhost:4200/`, OIDC against the existing `http://localhost:3000/...` Entra URIs still works.
## Related
- [ADR-0030](docs/decisions/0030-dockerised-dev-mode.md) — the dockerised dev mode this completes for the hostname-access case.
- [ADR-0018](docs/decisions/0018-environment-configuration-strategy.md) — per-environment SPA config strategy; `https` is a new Nx serve _configuration_, not a new `environment.ts` sibling, so 0018's build-time replacement story is unchanged.
- [ADR-0009](docs/decisions/0009-auth-flow-oidc-pkce-msal-node.md) — OIDC flow; no behaviour change, only the redirect-URI strings.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #263
335 lines
12 KiB
YAML
335 lines
12 KiB
YAML
# Local-dev infrastructure stack for `apf_portal`.
|
|
#
|
|
# Spins up the runtime dependencies the BFF and ADRs assume:
|
|
# - PostgreSQL 17 (per ADR-0006)
|
|
# - Redis 7 (per ADR-0010 — single node in dev,
|
|
# Sentinel HA in prod)
|
|
# - OpenTelemetry Collector (per ADR-0012 — receives OTLP from the
|
|
# BFF and the SPA, debug-logs traces in
|
|
# v1, forwards to Jaeger when activated)
|
|
#
|
|
# 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
|
|
# $EDITOR .env # set strong dev passwords
|
|
# docker compose -f dev.compose.yml up -d
|
|
#
|
|
# To bring up viewers too:
|
|
# docker compose -f dev.compose.yml --profile dbtools --profile observability up -d
|
|
#
|
|
# State persists in named volumes (`apf-portal-postgres-data`,
|
|
# `apf-portal-redis-data`). To wipe and start fresh:
|
|
# docker compose -f dev.compose.yml down -v
|
|
#
|
|
# Bootstrap SQL (audit roles + schema, per ADR-0013) is applied on
|
|
# first boot from init/postgres/. To replay after editing it, wipe
|
|
# the volume with `down -v` above.
|
|
|
|
name: apf-portal-dev
|
|
|
|
# Shared base for the ADR-0030 `apps` profile — dev servers for the three
|
|
# Nx apps. The repo is bind-mounted for hot reload; node_modules and the
|
|
# Nx cache live in named volumes so the container's install (native
|
|
# modules built for THIS image) is never shadowed by the host's.
|
|
x-app-base: &app-base
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile.dev
|
|
profiles: [apps]
|
|
working_dir: /workspace
|
|
volumes:
|
|
- ../../:/workspace
|
|
- app-node-modules:/workspace/node_modules
|
|
- app-nx-cache:/workspace/.nx
|
|
networks:
|
|
- apf-portal-dev
|
|
|
|
services:
|
|
postgres:
|
|
image: postgres:17.2-alpine
|
|
container_name: apf-portal-postgres
|
|
restart: unless-stopped
|
|
environment:
|
|
POSTGRES_USER: ${POSTGRES_USER:-portal}
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in infra/local/.env}
|
|
POSTGRES_DB: ${POSTGRES_DB:-portal_dev}
|
|
ports:
|
|
- '${POSTGRES_PORT:-5432}:5432'
|
|
volumes:
|
|
- postgres-data:/var/lib/postgresql/data
|
|
# Bootstrap SQL runs only on a fresh data volume.
|
|
- ./init/postgres:/docker-entrypoint-initdb.d:ro
|
|
networks:
|
|
- apf-portal-dev
|
|
healthcheck:
|
|
test:
|
|
['CMD-SHELL', 'pg_isready -U "${POSTGRES_USER:-portal}" -d "${POSTGRES_DB:-portal_dev}"']
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 5
|
|
|
|
redis:
|
|
image: redis:7.4-alpine
|
|
container_name: apf-portal-redis
|
|
restart: unless-stopped
|
|
# AOF (`--appendonly yes`) keeps sessions across container restarts —
|
|
# the dev equivalent of the persistence we want from the prod
|
|
# Sentinel cluster (per ADR-0010 §"Persistence").
|
|
command:
|
|
- redis-server
|
|
- --requirepass
|
|
- ${REDIS_PASSWORD:?REDIS_PASSWORD must be set in infra/local/.env}
|
|
- --appendonly
|
|
- 'yes'
|
|
ports:
|
|
- '${REDIS_PORT:-6379}:6379'
|
|
volumes:
|
|
- redis-data:/data
|
|
networks:
|
|
- apf-portal-dev
|
|
healthcheck:
|
|
test: ['CMD', 'redis-cli', '-a', '${REDIS_PASSWORD}', 'PING']
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 5
|
|
|
|
otel-collector:
|
|
image: otel/opentelemetry-collector-contrib:0.150.1
|
|
container_name: apf-portal-otel-collector
|
|
restart: unless-stopped
|
|
command: ['--config=/etc/otel-collector.yaml']
|
|
volumes:
|
|
- ./otel-collector.yaml:/etc/otel-collector.yaml:ro
|
|
ports:
|
|
# OTLP receivers — the BFF and the SPA send here.
|
|
- '${OTEL_GRPC_PORT:-4317}:4317'
|
|
- '${OTEL_HTTP_PORT:-4318}:4318'
|
|
networks:
|
|
- apf-portal-dev
|
|
|
|
# ------------------------------------------------------------------
|
|
# Optional: Postgres GUI (`--profile dbtools`).
|
|
# pgweb is a lightweight, no-config alternative to pgAdmin.
|
|
# ------------------------------------------------------------------
|
|
pgweb:
|
|
image: sosedoff/pgweb:0.16.2
|
|
container_name: apf-portal-pgweb
|
|
restart: unless-stopped
|
|
profiles: [dbtools]
|
|
# Use discrete CLI flags rather than `PGWEB_DATABASE_URL`. The URL
|
|
# form is fragile: any special character in POSTGRES_PASSWORD
|
|
# (`@`, `#`, `:`, `/`, `?`, `%`, `&`, …) breaks the userinfo
|
|
# parser, and the resulting "Invalid URL" error is opaque. Discrete
|
|
# flags accept any password verbatim. The image's ENTRYPOINT
|
|
# already passes `--bind=0.0.0.0 --listen=8081`; these args are
|
|
# appended.
|
|
command:
|
|
- --host=postgres
|
|
- --port=5432
|
|
- --user=${POSTGRES_USER:-portal}
|
|
- --pass=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in infra/local/.env}
|
|
- --db=${POSTGRES_DB:-portal_dev}
|
|
- --ssl=disable
|
|
ports:
|
|
- '${PGWEB_PORT:-8081}:8081'
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
networks:
|
|
- apf-portal-dev
|
|
|
|
# ------------------------------------------------------------------
|
|
# Optional: Jaeger UI for trace exploration (`--profile observability`).
|
|
# The collector pipeline above always tries to forward to `jaeger:4317`;
|
|
# when this profile is off, those exports fail silently (logged at
|
|
# warn level by the collector — no blocking effect on the debug
|
|
# exporter that prints traces to stdout).
|
|
# OTLP ports (4317/4318) are NOT published from this service to
|
|
# avoid colliding with the collector — the collector talks to
|
|
# Jaeger on the internal Compose network.
|
|
#
|
|
# Image is Jaeger v2 (`jaegertracing/jaeger`, distinct from the v1
|
|
# `jaegertracing/all-in-one`). v2 is the rewrite built on top of
|
|
# OpenTelemetry Collector and is the actively-developed line. OTLP
|
|
# receivers are enabled by default (no COLLECTOR_OTLP_ENABLED env
|
|
# needed) and the UI ships a built-in Light/Dark/Auto theme
|
|
# selector — features the v1 image did not have.
|
|
# ------------------------------------------------------------------
|
|
jaeger:
|
|
image: jaegertracing/jaeger:2.17.0
|
|
container_name: apf-portal-jaeger
|
|
restart: unless-stopped
|
|
profiles: [observability]
|
|
ports:
|
|
- '${JAEGER_UI_PORT:-16686}:16686'
|
|
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
|
|
|
|
# ------------------------------------------------------------------
|
|
# ADR-0030 dockerised full-stack dev mode (`--profile apps`).
|
|
#
|
|
# ./infra/local/dev.sh up apps → infra + the three dev servers,
|
|
# no native Node/pnpm on the host.
|
|
#
|
|
# Hot reload via the repo bind-mount. See docs/setup/ for the
|
|
# "which mode when" guidance (native / devcontainer / compose apps).
|
|
# NOTE: the SPA dev servers default to 4200/4300 — the same 4200 the
|
|
# `serve-static` profile uses; don't run `apps` and `serve-static`
|
|
# together, or override SHELL_PORT.
|
|
# ------------------------------------------------------------------
|
|
|
|
# One-shot: populate the shared node_modules volume once so the three
|
|
# app services don't race three concurrent installs on it. Exits when
|
|
# done; the apps gate on its successful completion.
|
|
apps-deps:
|
|
<<: *app-base
|
|
container_name: apf-portal-apps-deps
|
|
command: ['pnpm', 'install', '--frozen-lockfile']
|
|
|
|
portal-bff:
|
|
<<: *app-base
|
|
container_name: apf-portal-bff-dev
|
|
environment:
|
|
APF_ROLE: bff
|
|
NODE_ENV: development
|
|
PORT: '3000'
|
|
# Host-specific URLs rebuilt from infra/local/.env creds → Compose
|
|
# service names. Compose `environment` wins over `env_file`, so
|
|
# these override the localhost values in the BFF's own .env.
|
|
DATABASE_URL: 'postgresql://${POSTGRES_USER:-portal}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-portal_dev}?schema=public'
|
|
REDIS_URL: 'redis://default:${REDIS_PASSWORD}@redis:6379/0'
|
|
OTEL_EXPORTER_OTLP_ENDPOINT: 'http://otel-collector:4318/v1/traces'
|
|
# Secrets (Entra / session / jwks) come from the BFF's own dev env.
|
|
# `required: false` so SPA-only devs can `up` without it — the BFF
|
|
# then fails its own boot-time config validators with a clear message
|
|
# rather than Compose erroring on a missing file.
|
|
env_file:
|
|
- path: ../../apps/portal-bff/.env
|
|
required: false
|
|
command: ['pnpm', 'exec', 'nx', 'serve', 'portal-bff']
|
|
ports:
|
|
- '${BFF_PORT:-3000}:3000'
|
|
depends_on:
|
|
apps-deps:
|
|
condition: service_completed_successfully
|
|
postgres:
|
|
condition: service_healthy
|
|
redis:
|
|
condition: service_healthy
|
|
|
|
portal-shell:
|
|
<<: *app-base
|
|
container_name: apf-portal-shell-dev
|
|
environment:
|
|
# Read by apps/portal-shell/proxy.conf.js — points the dev-server
|
|
# /api proxy at the BFF container by name (Compose DNS). Native
|
|
# `nx serve` leaves BFF_TARGET unset and falls back to localhost.
|
|
BFF_TARGET: http://portal-bff:3000
|
|
# `--configuration=${NX_SERVE_CONFIGURATION:-development}` is
|
|
# interpolated by Compose at YAML parse time from
|
|
# `infra/local/.env`. Set `NX_SERVE_CONFIGURATION=https` there to
|
|
# serve over TLS — required when accessing via a hostname (Entra
|
|
# rejects `http:` for non-`localhost` redirect URIs). See the
|
|
# `https` configuration in apps/portal-shell/project.json + the
|
|
# "HTTPS dev-server setup" section in infra/README.md for the
|
|
# mkcert procedure.
|
|
command:
|
|
[
|
|
'pnpm',
|
|
'exec',
|
|
'nx',
|
|
'serve',
|
|
'portal-shell',
|
|
'--host',
|
|
'0.0.0.0',
|
|
'--port',
|
|
'4200',
|
|
'--configuration=${NX_SERVE_CONFIGURATION:-development}',
|
|
]
|
|
ports:
|
|
- '${SHELL_PORT:-4200}:4200'
|
|
depends_on:
|
|
apps-deps:
|
|
condition: service_completed_successfully
|
|
|
|
portal-admin:
|
|
<<: *app-base
|
|
container_name: apf-portal-admin-dev
|
|
environment:
|
|
# See portal-shell — same proxy target for the admin SPA.
|
|
BFF_TARGET: http://portal-bff:3000
|
|
# See portal-shell for the NX_SERVE_CONFIGURATION rationale.
|
|
command:
|
|
[
|
|
'pnpm',
|
|
'exec',
|
|
'nx',
|
|
'serve',
|
|
'portal-admin',
|
|
'--host',
|
|
'0.0.0.0',
|
|
'--port',
|
|
'4300',
|
|
'--configuration=${NX_SERVE_CONFIGURATION:-development}',
|
|
]
|
|
ports:
|
|
- '${ADMIN_PORT:-4300}:4300'
|
|
depends_on:
|
|
apps-deps:
|
|
condition: service_completed_successfully
|
|
|
|
volumes:
|
|
postgres-data:
|
|
name: apf-portal-postgres-data
|
|
redis-data:
|
|
name: apf-portal-redis-data
|
|
app-node-modules:
|
|
name: apf-portal-app-node-modules
|
|
app-nx-cache:
|
|
name: apf-portal-app-nx-cache
|
|
|
|
networks:
|
|
apf-portal-dev:
|
|
name: apf-portal-dev
|