171f21b99b
## Summary `PGWEB_DATABASE_URL` (post-#62 rename) still fails at boot: ``` Error: Invalid URL. Valid format: postgres://user:password@host:port/db?sslmode=mode ``` Root cause: the userinfo portion of a Postgres URL must be URL-encoded — any `@`, `#`, `:`, `/`, `?`, `%`, `&`, `=`, `+`, `;` etc. in the password breaks the parser. Compose has no built-in URL encoding, so the URL we construct in YAML is fragile by design and depends on the developer happening to pick a URL-safe password. Switch to pgweb's discrete CLI flags (`--host`, `--port`, `--user`, `--pass`, `--db`, `--ssl`). Compose interpolates each value literally — no URL encoding required, any password works. The image's ENTRYPOINT already passes `--bind=0.0.0.0 --listen=8081`; our args are appended to those. ## Side benefit A missing password now yields an explicit Compose error (`POSTGRES_PASSWORD must be set in infra/local/.env`) rather than an opaque pgweb crash with a vague "Invalid URL" message. ## Test plan - [ ] After merge: `docker compose -f infra/local/dev.compose.yml --profile dbtools up -d` → pgweb stays up (no `Restarting (1)` loop). - [ ] http://localhost:8081 loads pgweb's UI; you can navigate to the `audit` schema. - [ ] Confirm with a password that contains a special char (e.g., `dev@pass#2026!`) — should still work. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #63
158 lines
5.4 KiB
YAML
158 lines
5.4 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 behind Compose profiles:
|
|
# - --profile dbtools → pgweb (Postgres GUI)
|
|
# - --profile observability → Jaeger UI (trace explorer)
|
|
#
|
|
# 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
|
|
|
|
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.
|
|
# ------------------------------------------------------------------
|
|
jaeger:
|
|
image: jaegertracing/all-in-one:1.76.0
|
|
container_name: apf-portal-jaeger
|
|
restart: unless-stopped
|
|
profiles: [observability]
|
|
environment:
|
|
COLLECTOR_OTLP_ENABLED: 'true'
|
|
ports:
|
|
- '${JAEGER_UI_PORT:-16686}:16686'
|
|
networks:
|
|
- apf-portal-dev
|
|
|
|
volumes:
|
|
postgres-data:
|
|
name: apf-portal-postgres-data
|
|
redis-data:
|
|
name: apf-portal-redis-data
|
|
|
|
networks:
|
|
apf-portal-dev:
|
|
name: apf-portal-dev
|