82d8c6972f
Entra refuses http: redirect URIs for any host other than localhost,
which blocked the ADR-0030 dockerised dev mode the moment a developer
tried to share the stack with another teammate via a hostname (e.g.
apf-portal.dev.local). Add an opt-in 'https' Nx serve configuration
on both SPAs so the dev-server can terminate TLS using a mkcert cert,
and let the apps Compose profile pick the configuration via an env
var so behaviour stays unchanged by default.
- apps/portal-{shell,admin}/project.json: new https configuration
inheriting development + ssl/sslKey/sslCert at .secrets/dev-tls.*.
- infra/local/dev.compose.yml: shell + admin commands gain
--configuration=${NX_SERVE_CONFIGURATION:-development}, interpolated
from infra/local/.env at parse time.
- infra/local/.env.example: NX_SERVE_CONFIGURATION block + rationale.
- apps/portal-bff/.env.example: comment block above the ENTRA_*_
REDIRECT_URI defaults shows the https hostname override pattern and
reminds that each URI must be registered Entra-side.
- infra/README.md: new HTTPS dev-server setup subsection covering
mkcert install, cert generation, the .secrets/ convention, the
Entra step, and how to flip NX_SERVE_CONFIGURATION on.
Native WSL / localhost is unaffected: defaultConfiguration stays
development, no SSL files required, existing Entra localhost URIs
work as before. BFF stays plain HTTP behind the SPA proxy.
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
|