Files
apf_portal/infra/local/dev.compose.yml
T
Julien Gautier 7aa2683750
CI / scan (pull_request) Failing after 2m43s
CI / commits (pull_request) Successful in 2m52s
CI / check (pull_request) Successful in 3m3s
CI / a11y (pull_request) Successful in 1m51s
CI / perf (pull_request) Failing after 18m35s
feat(infra): single-file toggle between apps-profile dev modes
Reduce the apps-profile mode switch (localhost / VSCode tunnel vs
HTTPS hostname) to a single file: infra/local/.env. Today the
toggle requires editing both apps/portal-bff/.env (the four
ENTRA_*_REDIRECT_URI) and infra/local/.env (NX_SERVE_CONFIGURATION).
After this PR, apps/portal-bff/.env stays at its localhost defaults
regardless — native nx serve outside Docker keeps working untouched
— and infra/local/.env carries the entire mode-specific block.

- dev.compose.yml portal-bff service: the four ENTRA_*_REDIRECT_URI
  are added to the environment: block with
  ${VAR:-localhost-default} interpolation. Compose's environment:
  wins over env_file:, so the BFF inside the container sees the
  override when set and the localhost default otherwise.
- infra/local/.env.example: the trailing Apps block is restructured
  as two clearly-labelled profiles (Mode A — Localhost / DEFAULT;
  Mode B — HTTPS hostname, commented template).
- apps/portal-bff/.env.example: comment block above the redirect URI
  defaults now explains that they stay at localhost regardless and
  points at the compose-level override in infra/local/.env.
- infra/README.md: new "Switching between dev modes — localhost vs
  hostname" subsection between "Dockerised app dev mode" and
  "HTTPS dev-server setup" with a comparison table and per-mode
  step-by-step.
2026-06-02 01:30:23 +02:00

350 lines
13 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'
# OIDC redirect URIs. Driven by infra/local/.env so switching
# between the two access modes — `localhost` (default; VSCode
# Remote-SSH port-forwarding) and `https` hostname (the
# ADR-0030 mkcert path) — is a single-file change. Unset →
# localhost defaults; set in infra/local/.env to override with
# an `https://apf-portal.dev-XX.local:…` hostname. See the
# "Switching between dev modes" subsection of infra/README.md.
# Compose `environment` overrides `env_file`, so the BFF's
# own .env stays at localhost (so native `nx serve` works
# untouched) and only `infra/local/.env` carries the
# docker-apps-mode override.
ENTRA_REDIRECT_URI: ${ENTRA_REDIRECT_URI:-http://localhost:3000/api/auth/callback}
ENTRA_POST_LOGOUT_REDIRECT_URI: ${ENTRA_POST_LOGOUT_REDIRECT_URI:-http://localhost:4200/}
ENTRA_ADMIN_REDIRECT_URI: ${ENTRA_ADMIN_REDIRECT_URI:-http://localhost:3000/api/admin/auth/callback}
ENTRA_ADMIN_POST_LOGOUT_REDIRECT_URI: ${ENTRA_ADMIN_POST_LOGOUT_REDIRECT_URI:-http://localhost:4300/}
# 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