feat(infra): add local-dev Docker Compose stack (#57)
## Summary Bring up Postgres + Redis + OTel Collector in one command so contributors can run the BFF end-to-end without manually wiring each service. Replaces the throwaway `docker run postgres:17-alpine` one-liner that was in `docs/development.md` §3. ### What lands - **`infra/local/dev.compose.yml`** — three core services (`postgres:17.2-alpine`, `redis:7.4-alpine`, `otel/opentelemetry-collector-contrib:0.115.0`) plus two viewers gated behind Compose profiles: - `--profile dbtools` → `sosedoff/pgweb:0.16.2` (Postgres GUI on port 8081) - `--profile observability` → `jaegertracing/all-in-one:1.62` (Jaeger UI on 16686) - All ports overridable via `.env`. State in named volumes. Healthchecks on data services. - **`infra/local/.env.example`** — credentials + ports template. `POSTGRES_PASSWORD` and `REDIS_PASSWORD` are mandatory (compose refuses to boot without them); other keys default sensibly. - **`infra/local/init/postgres/01-init.sql`** — bootstrap SQL per **ADR-0013**: `audit_owner` / `audit_writer` / `audit_reader` / `audit_archiver` roles + `audit` schema. Default privileges encode the append-only contract (INSERT to writer, SELECT to reader, DELETE to archiver, no UPDATE/TRUNCATE to anyone). Applied on first Postgres boot only; documented re-run procedure. - **`infra/local/otel-collector.yaml`** — pipeline: OTLP gRPC/HTTP → batch → debug exporter (always) + forward to `jaeger:4317`. When the observability profile is off, the Jaeger export logs warn-level retries but doesn't block the debug pipeline. ### Surrounding doc updates - **`infra/README.md`** — new "Local-dev stack" section: service inventory, port table, first-time setup walkthrough, persistence/bootstrap-replay tips. The previous `local/` placeholder line is removed. - **`docs/development.md`** §3 — rewritten to walk through the compose-based setup; cross-links to `infra/README.md` for the full reference. Roadmap entry for "Local infra recipe" removed from §8 (now implemented); "Observability dev-loop" line adjusted to point at the new Jaeger profile. ### Out of scope - **Production parity** — HA Postgres, Redis Sentinel, real OTel backend (Tempo / Loki / etc.) — defer to the on-prem infrastructure ADR (phase 3b). The dev-only nature of this stack is called out explicitly in `infra/README.md`. - **Wiring the BFF** to actually use these endpoints (NestJS config, Prisma datasource URL, OTel SDK init) — that's the **B — Observability foundations** chantier, next up. ## Test plan - [ ] `cd infra/local && cp .env.example .env && docker compose -f dev.compose.yml up -d` → all three core services come up healthy; verify with `docker compose ps`. - [ ] `psql postgres://portal:<pwd>@localhost:5432/portal_dev -c "\dn"` shows the `audit` schema; `\dg` shows the four audit roles. - [ ] `redis-cli -a <pwd> PING` → `PONG`. - [ ] Send a fake OTLP trace via grpcurl → see it printed by `docker compose logs otel-collector`. - [ ] `--profile dbtools up -d` → http://localhost:8081 shows pgweb UI, can navigate to the audit schema. - [ ] `--profile observability up -d` → http://localhost:16686 shows Jaeger UI; collector logs no longer report Jaeger export retries. - [ ] `docker compose down -v` cleanly removes everything; next `up -d` re-runs the bootstrap SQL. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #57
This commit was merged in pull request #57.
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
# Local-dev secrets and ports for `infra/local/dev.compose.yml`.
|
||||
# Copy to `.env` (which is git-ignored) and adjust as needed.
|
||||
#
|
||||
# cp .env.example .env
|
||||
# $EDITOR .env
|
||||
|
||||
# ---------------------------------------------------------------- Postgres
|
||||
# `POSTGRES_PASSWORD` is mandatory — the compose refuses to boot without it.
|
||||
POSTGRES_USER=portal
|
||||
POSTGRES_PASSWORD=portal_dev_change_me
|
||||
POSTGRES_DB=portal_dev
|
||||
POSTGRES_PORT=5432
|
||||
|
||||
# ---------------------------------------------------------------- Redis
|
||||
# Same — mandatory.
|
||||
REDIS_PASSWORD=redis_dev_change_me
|
||||
REDIS_PORT=6379
|
||||
|
||||
# ---------------------------------------------------------------- OTel
|
||||
OTEL_GRPC_PORT=4317
|
||||
OTEL_HTTP_PORT=4318
|
||||
|
||||
# ---------------------------------------------------------------- Optional viewers
|
||||
# Only consumed when the matching Compose profile is activated:
|
||||
# --profile dbtools → pgweb
|
||||
# --profile observability → Jaeger UI
|
||||
PGWEB_PORT=8081
|
||||
JAEGER_UI_PORT=16686
|
||||
@@ -0,0 +1,145 @@
|
||||
# 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.115.0
|
||||
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]
|
||||
environment:
|
||||
DATABASE_URL: postgres://${POSTGRES_USER:-portal}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-portal_dev}?sslmode=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.62
|
||||
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
|
||||
@@ -0,0 +1,48 @@
|
||||
-- Bootstrap SQL for local-dev Postgres, applied on FIRST boot only
|
||||
-- (Postgres image runs files in /docker-entrypoint-initdb.d once,
|
||||
-- when the data volume is empty).
|
||||
--
|
||||
-- Implements the role + schema layout from ADR-0013 (Audit trail,
|
||||
-- separated Postgres schema, append-only by Postgres role grants).
|
||||
-- Production deployment manifests (future infrastructure ADR) will
|
||||
-- replicate the same layout with real service accounts; in dev the
|
||||
-- default `portal` user (created automatically from POSTGRES_USER)
|
||||
-- is the schema owner, and the audit roles are granted to it for
|
||||
-- testing role-based access patterns locally.
|
||||
|
||||
-- ---------------------------------------------------------------- Audit roles
|
||||
-- NOLOGIN: these are permission containers, not login users. The BFF
|
||||
-- connects as a login user that has been GRANTed the role.
|
||||
CREATE ROLE audit_owner NOLOGIN;
|
||||
CREATE ROLE audit_writer NOLOGIN;
|
||||
CREATE ROLE audit_reader NOLOGIN;
|
||||
CREATE ROLE audit_archiver NOLOGIN;
|
||||
|
||||
-- ---------------------------------------------------------------- Audit schema
|
||||
-- Owned by audit_owner so DEFAULT PRIVILEGES below take effect for
|
||||
-- every future table created under the schema (Prisma migration etc.).
|
||||
CREATE SCHEMA audit AUTHORIZATION audit_owner;
|
||||
|
||||
GRANT USAGE ON SCHEMA audit TO audit_writer, audit_reader, audit_archiver;
|
||||
|
||||
-- ---------------------------------------------------------------- Append-only contract
|
||||
-- ADR-0013 §"Append-only by role grants":
|
||||
-- audit_writer → INSERT only
|
||||
-- audit_reader → SELECT only
|
||||
-- audit_archiver → DELETE only (used to prune past retention)
|
||||
-- nobody → UPDATE, TRUNCATE
|
||||
ALTER DEFAULT PRIVILEGES FOR ROLE audit_owner IN SCHEMA audit
|
||||
GRANT INSERT ON TABLES TO audit_writer;
|
||||
|
||||
ALTER DEFAULT PRIVILEGES FOR ROLE audit_owner IN SCHEMA audit
|
||||
GRANT SELECT ON TABLES TO audit_reader;
|
||||
|
||||
ALTER DEFAULT PRIVILEGES FOR ROLE audit_owner IN SCHEMA audit
|
||||
GRANT DELETE ON TABLES TO audit_archiver;
|
||||
|
||||
-- ---------------------------------------------------------------- Dev convenience
|
||||
-- The default `portal` superuser bypasses these grants anyway, but
|
||||
-- granting the audit roles explicitly lets us test role-based access
|
||||
-- with `SET ROLE audit_writer;` etc. from a psql session against the
|
||||
-- dev DB. Production never grants all four to one user.
|
||||
GRANT audit_owner, audit_writer, audit_reader, audit_archiver TO portal;
|
||||
@@ -0,0 +1,65 @@
|
||||
# OpenTelemetry Collector config for the local-dev stack.
|
||||
#
|
||||
# v1 scope (per ADR-0012):
|
||||
# - Accept OTLP from the BFF and the SPA on gRPC (4317) and HTTP
|
||||
# (4318). The default OTEL_EXPORTER_OTLP_ENDPOINT in dev points
|
||||
# here.
|
||||
# - Batch + log everything to stdout via the `debug` exporter — the
|
||||
# dev sees what their app emits without standing up a backend.
|
||||
# - When the `observability` Compose profile is active, additionally
|
||||
# forward to Jaeger (`jaeger:4317` on the internal Compose
|
||||
# network). When the profile is inactive, the OTLP→Jaeger export
|
||||
# fails at warn level but does not impact the debug pipeline.
|
||||
#
|
||||
# Production replaces the debug exporter with the proper backend
|
||||
# (chosen in the future on-prem infrastructure ADR — likely Tempo +
|
||||
# Loki + Mimir, or an OpenTelemetry-friendly all-in-one).
|
||||
|
||||
receivers:
|
||||
otlp:
|
||||
protocols:
|
||||
grpc:
|
||||
endpoint: 0.0.0.0:4317
|
||||
http:
|
||||
endpoint: 0.0.0.0:4318
|
||||
|
||||
processors:
|
||||
batch:
|
||||
# Small batches in dev so the developer sees output quickly.
|
||||
timeout: 5s
|
||||
send_batch_size: 100
|
||||
|
||||
exporters:
|
||||
debug:
|
||||
verbosity: detailed
|
||||
otlp/jaeger:
|
||||
endpoint: jaeger:4317
|
||||
tls:
|
||||
insecure: true
|
||||
sending_queue:
|
||||
enabled: true
|
||||
# When jaeger isn't running (profile off), retries fall back
|
||||
# to a small queue rather than blocking the pipeline.
|
||||
queue_size: 100
|
||||
retry_on_failure:
|
||||
enabled: true
|
||||
initial_interval: 5s
|
||||
max_interval: 30s
|
||||
|
||||
service:
|
||||
pipelines:
|
||||
traces:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [debug, otlp/jaeger]
|
||||
logs:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [debug]
|
||||
metrics:
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [debug]
|
||||
telemetry:
|
||||
logs:
|
||||
level: info
|
||||
Reference in New Issue
Block a user