## What & why S-19b-1. A write to the Objecten API now produces a **delivered** notification on the `objecten` kanaal in Open Notificaties. ADR-0028 switched Objecten's notifications off on purpose — there was no broker, worker, kanaal or abonnement, so wiring only the client side would have dropped every message on the floor. This slice builds the real path and turns it back on. - `objecten-celery` worker (mirrors `oz-celery`) + `CELERY_BROKER_URL`/`RESULT_BACKEND` on objecten-redis db 1 (db 0 is already the cache). `notifications_api_common` only *queues* the send; without a worker every register write is silently undelivered. - `nrc` service + `notifications_config` in Objecten's `setup_configuration`, reusing the `big-reference-seed` credential OpenZaak publishes with (NRC authorizes it via OpenZaak's AC, which grants it `heeft_alle_autorisaties` — no second credential needed). - The `objecten` kanaal in NRC's `setup_configuration`. The name is fixed by the Objects API (`NOTIFICATIONS_KANAAL`), not chosen here; publishing to an unregistered kanaal is exactly what the red check reported first. - `NOTIFICATIONS_DISABLED: "false"` in both compose files. - Writers address Objecten as `objecten.local` — see *Notes for reviewers*. - `make verify-objecten-notifications` — registers an abonnement on `objecten` pointing at a throwaway sink, writes a `RegisterRecord` exactly as the ACL does on approval, asserts the delivery. One assertion covering the whole chain: Objecten -> objecten-celery -> NRC -> nrc-beat -> callback. Wired into the CI `verify-stack` job and the summary table. **ADR-0029** records the decisions; ADR-0028's ceiling now points at it. Closes #152 ## Definition of Done - [x] Linked Gitea issue (above). - [x] Failing test committed before the implementation (dc9ca2c, red at the first hop: `NRC POST /api/v1/abonnement -> 400 "Kanaal met deze naam bestaat niet."`). - [x] Implementation makes the test pass (4488962, + two fixes found by CI, below). - [x] Conventional Commits referencing the issue (`refs #152`). - [x] CI green — all six jobs ona5fd47e, including `verify-stack` end to end (e2e included). - [x] `docker compose up` from a fresh clone reaches green health checks within 3 minutes (`verify-stack`'s bring-up step). - [x] Docs updated — ADR-0029 added, ADR-0028's ceiling annotated, BACKLOG.md split. - [x] ADR added in `docs/architecture/`. - [x] Demo note in `docs/demo-script.md` if user-visible — n/a, infrastructure only; nothing consumes the kanaal until S-19b-2 (#153). ## Notes for reviewers **The one genuinely non-obvious bit: writers address Objecten as `objecten.local:8000`, not `objecten:8000`.** NRC types a notification's `hoofdObject`/`resourceUrl` as DRF `URLField`, so Django's `URLValidator` runs on them — and it rejects a **single-label** host. Objecten fills both from the object url DRF built with `request.build_absolute_uri`, i.e. *the Host the caller used*. Writing via the plain service name returns 201 and then fails every publish in the background, forever, with ``` 400 {"hoofdObject":["Voer een geldige URL in."],"resourceUrl":["Voer een geldige URL in."]} ``` So the `objecten` service carries an `objecten.local` network alias and every writer uses it — `Acl__Objecten__BaseUrl`, `ObjectenGatewayIntegrationTests`, this slice's verify driver. An alias rather than a bare dotted `SITE_DOMAIN` so the host still *resolves*: a subscriber following `resourceUrl` reaches the record, which S-19b-2 will do. Readers keep the plain name. Same class of constraint as ADR-0028's Objecttypen base-URL rule. **Ceiling, stated in the ADR:** nothing enforces the alias — a future writer using `objecten:8000` gets a 201 and silently no notification. If a second writer ever appears, rename the compose service rather than adding a lint. **Two CI-only failures on the way here**, both worth knowing: 1. `SITE_DOMAIN` was my first guess at the mechanism and is simply not what builds those URLs — dropped ind76abf2. 2. The check correlated the delivery on the `reference` inside the record it wrote. An NRC notification carries `kanaal`/`resource`/`kenmerken`/`hoofdObject`/`resourceUrl` and **never the record data**, so it correlates on the object URL now (a5fd47e). **Cost:** one more long-running container on the memory-tight runner. It inherits the capped `UWSGI_PROCESSES: "1"` env, which the celery command ignores; if `verify-stack` gets tight again, celery concurrency is the next knob. **Follow-up:** S-19b-2 (#153) sources the projection from these events. Nothing subscribes to the `objecten` kanaal in the product yet — only the verify check does.Reviewed-on: #154
861 lines
34 KiB
YAML
861 lines
34 KiB
YAML
# Development stack — boots all infra services plus the ACL and BFF.
|
||
#
|
||
# Consolidates infra/openzaak/, infra/opennotificaties/, infra/keycloak/,
|
||
# and infra/flowable/ and adds the ACL and BFF services.
|
||
#
|
||
# Port map (host):
|
||
# 8000 OpenZaak ZGW API (admin: admin / admin)
|
||
# 8001 Open Notificaties (admin: admin / admin)
|
||
# 8080 BFF GET /health → Healthy
|
||
# 8090 Flowable REST http://localhost:8090/flowable-rest/service/
|
||
# 8100 ACL GET /health → Healthy POST /zaken
|
||
# 8110 Event Subscriber GET /health → Healthy POST /notifications
|
||
# 8120 projection-api GET /health → Healthy GET /register
|
||
# 8180 Keycloak (admin: admin / admin)
|
||
#
|
||
# docker compose -f infra/docker-compose.yml up -d --build --wait
|
||
#
|
||
# After first boot, seed + publish the BIG catalogus:
|
||
# OZ_PUBLISH=1 python infra/openzaak/seed_catalogus.py
|
||
# The ACL discovers the zaaktype by identificatie (S-27, ADR-0021), so there is no URL to inject —
|
||
# just point its BaseUrl at an OpenZaak host OpenZaak accepts on zaak-create (a container IP; a
|
||
# single-label host is rejected):
|
||
# ACL_OPENZAAK_BASEURL=http://<openzaak-ip>:8000/ docker compose -f infra/docker-compose.yml up -d acl
|
||
|
||
services:
|
||
|
||
# ── OpenZaak (S-01) ──────────────────────────────────────────────────────
|
||
oz-db:
|
||
image: docker.io/postgis/postgis:17-3.5
|
||
environment:
|
||
POSTGRES_USER: openzaak
|
||
POSTGRES_PASSWORD: openzaak
|
||
POSTGRES_DB: openzaak
|
||
command: postgres -c max_connections=300
|
||
volumes:
|
||
- oz-db:/var/lib/postgresql/data
|
||
healthcheck:
|
||
# pg_isready only checks TCP; the second clause verifies PostGIS is installed
|
||
# so oz-init migrations can safely start (avoids race on cold container start).
|
||
test: ["CMD-SHELL", "pg_isready -U openzaak -d openzaak && psql -U openzaak -d openzaak -c 'SELECT PostGIS_Version();' -q 2>/dev/null"]
|
||
interval: 5s
|
||
timeout: 5s
|
||
retries: 30
|
||
start_period: 15s
|
||
networks: [cg]
|
||
|
||
oz-redis:
|
||
image: docker.io/library/redis:7
|
||
networks: [cg]
|
||
|
||
oz-init:
|
||
image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-1.28.2}
|
||
environment: &oz-env
|
||
# 1 uWSGI worker, not the image default of 4×4 (#147, same lever as #145): OpenZaak serves
|
||
# single-request smoke checks here and is not load-tested, so 4 idle Django workers just pin
|
||
# ~800 MB and pressure the shared runner. The -init (setup_configuration) and -celery containers
|
||
# share this anchor and ignore it — they don't run uwsgi.
|
||
UWSGI_PROCESSES: "1"
|
||
UWSGI_THREADS: "2"
|
||
DJANGO_SETTINGS_MODULE: openzaak.conf.docker
|
||
SECRET_KEY: ${OZ_SECRET_KEY:-dev-only-not-for-production}
|
||
DB_HOST: oz-db
|
||
DB_NAME: openzaak
|
||
DB_USER: openzaak
|
||
DB_PASSWORD: openzaak
|
||
IS_HTTPS: "no"
|
||
ALLOWED_HOSTS: "*"
|
||
CACHE_DEFAULT: oz-redis:6379/0
|
||
CACHE_AXES: oz-redis:6379/0
|
||
CELERY_BROKER_URL: redis://oz-redis:6379/1
|
||
CELERY_RESULT_BACKEND: redis://oz-redis:6379/1
|
||
DISABLE_2FA: "true"
|
||
# Publish notifications to NRC (always present in this full stack). The NRC
|
||
# service + notifications_config are provisioned by setup_configuration
|
||
# (infra/openzaak/setup_configuration/data.yaml). See ADR-0007 / S-01-c.
|
||
NOTIFICATIONS_DISABLED: "false"
|
||
OPENZAAK_SUPERUSER_USERNAME: admin
|
||
DJANGO_SUPERUSER_PASSWORD: admin
|
||
OPENZAAK_SUPERUSER_EMAIL: admin@localhost
|
||
RUN_SETUP_CONFIG: "true"
|
||
command: /setup_configuration.sh
|
||
# data.yaml is streamed into this external volume by infra/seed-config.sh
|
||
# before start (bind mounts don't reach sibling containers on the CI runner).
|
||
volumes:
|
||
- oz-config:/app/setup_configuration:ro
|
||
depends_on:
|
||
oz-db:
|
||
condition: service_healthy
|
||
oz-redis:
|
||
condition: service_started
|
||
networks: [cg]
|
||
|
||
openzaak:
|
||
image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-1.28.2}
|
||
environment: *oz-env
|
||
healthcheck:
|
||
test: ["CMD", "python", "-c", "import requests,sys; sys.exit(0 if requests.head('http://localhost:8000/admin/').status_code in (200,302) else 1)"]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 10
|
||
start_period: 30s
|
||
ports:
|
||
- "8000:8000"
|
||
depends_on:
|
||
oz-init:
|
||
condition: service_completed_successfully
|
||
networks: [cg]
|
||
|
||
oz-celery:
|
||
image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-1.28.2}
|
||
environment: *oz-env
|
||
command: /celery_worker.sh
|
||
depends_on:
|
||
oz-init:
|
||
condition: service_completed_successfully
|
||
networks: [cg]
|
||
|
||
# ── Open Notificaties / NRC (S-01-c) ─────────────────────────────────────
|
||
nrc-db:
|
||
image: docker.io/postgis/postgis:17-3.5
|
||
environment:
|
||
POSTGRES_USER: opennotificaties
|
||
POSTGRES_PASSWORD: opennotificaties
|
||
POSTGRES_DB: opennotificaties
|
||
command: postgres -c max_connections=300
|
||
volumes:
|
||
- nrc-db:/var/lib/postgresql/data
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "pg_isready -U opennotificaties -d opennotificaties"]
|
||
interval: 5s
|
||
timeout: 3s
|
||
retries: 10
|
||
networks: [cg]
|
||
|
||
nrc-redis:
|
||
image: docker.io/library/redis:7
|
||
networks: [cg]
|
||
|
||
nrc-init:
|
||
# Plain base image — nrc-init runs migrations only (see command below), so it
|
||
# needs no baked config.
|
||
image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-1.16.1}
|
||
environment: &nrc-env
|
||
# 1 uWSGI worker, not the image default of 4×4 (#147) — see the oz-env note above.
|
||
UWSGI_PROCESSES: "1"
|
||
UWSGI_THREADS: "2"
|
||
DJANGO_SETTINGS_MODULE: nrc.conf.docker
|
||
SECRET_KEY: ${NRC_SECRET_KEY:-dev-only-not-for-production}
|
||
DB_HOST: nrc-db
|
||
DB_NAME: opennotificaties
|
||
DB_USER: opennotificaties
|
||
DB_PASSWORD: opennotificaties
|
||
IS_HTTPS: "no"
|
||
ALLOWED_HOSTS: "*"
|
||
CACHE_DEFAULT: nrc-redis:6379/0
|
||
CACHE_AXES: nrc-redis:6379/0
|
||
CELERY_BROKER_URL: redis://nrc-redis:6379/1
|
||
CELERY_RESULT_BACKEND: redis://nrc-redis:6379/1
|
||
DISABLE_2FA: "true"
|
||
OPENNOTIFICATIES_SUPERUSER_USERNAME: admin
|
||
DJANGO_SUPERUSER_PASSWORD: admin
|
||
OPENNOTIFICATIES_SUPERUSER_EMAIL: admin@localhost
|
||
RUN_SETUP_CONFIG: "true"
|
||
# nrc-beat fires `execute_notifications` this often to drain scheduled
|
||
# notifications to subscribers (upstream default 20s). See ADR-0007.
|
||
NOTIFICATION_SEC_INTERVAL: "5"
|
||
# Runs migrations + setup_configuration (S-01-c): the JWT credential, the
|
||
# Autorisaties-API delegation, and the `zaken` kanaal that let OpenZaak publish.
|
||
# data.yaml is streamed into rr-nrc-config by infra/seed-config.sh (bind mounts
|
||
# don't reach sibling containers on the CI runner). See data.yaml + ADR-0007.
|
||
command: /setup_configuration.sh
|
||
volumes:
|
||
- nrc-config:/app/setup_configuration:ro
|
||
depends_on:
|
||
nrc-db:
|
||
condition: service_healthy
|
||
nrc-redis:
|
||
condition: service_started
|
||
openzaak:
|
||
condition: service_healthy
|
||
networks: [cg]
|
||
|
||
nrc-web:
|
||
image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-1.16.1}
|
||
environment: *nrc-env
|
||
healthcheck:
|
||
test: ["CMD", "python", "-c", "import requests,sys; sys.exit(0 if requests.head('http://localhost:8000/admin/').status_code in (200,302) else 1)"]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 10
|
||
start_period: 30s
|
||
ports:
|
||
- "8001:8000"
|
||
depends_on:
|
||
nrc-init:
|
||
condition: service_completed_successfully
|
||
networks: [cg]
|
||
|
||
nrc-celery:
|
||
image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-1.16.1}
|
||
environment: *nrc-env
|
||
command: /celery_worker.sh
|
||
depends_on:
|
||
nrc-init:
|
||
condition: service_completed_successfully
|
||
networks: [cg]
|
||
|
||
# Celery beat drains the ScheduledNotification rows the API creates on publish
|
||
# and hands them to the worker. Without it, notifications are accepted but never
|
||
# delivered to subscribers — required, not optional. See ADR-0007.
|
||
nrc-beat:
|
||
image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-1.16.1}
|
||
environment: *nrc-env
|
||
command: /celery_beat.sh
|
||
depends_on:
|
||
nrc-init:
|
||
condition: service_completed_successfully
|
||
networks: [cg]
|
||
|
||
# ── Keycloak (S-02) ──────────────────────────────────────────────────────
|
||
keycloak:
|
||
image: quay.io/keycloak/keycloak:26.1
|
||
command: ["start-dev", "--import-realm"]
|
||
environment:
|
||
KC_BOOTSTRAP_ADMIN_USERNAME: admin
|
||
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
|
||
KEYCLOAK_ADMIN: admin
|
||
KEYCLOAK_ADMIN_PASSWORD: admin
|
||
KC_HEALTH_ENABLED: "true"
|
||
KC_HTTP_ENABLED: "true"
|
||
ports:
|
||
- "8180:8080"
|
||
# realm exports are streamed into this external volume by infra/seed-config.sh.
|
||
volumes:
|
||
- kc-realms:/opt/keycloak/data/import:ro
|
||
networks: [cg]
|
||
|
||
# ── Flowable (S-03) ──────────────────────────────────────────────────────
|
||
flowable-db:
|
||
image: docker.io/library/postgres:16
|
||
environment:
|
||
POSTGRES_USER: flowable
|
||
POSTGRES_PASSWORD: flowable
|
||
POSTGRES_DB: flowable
|
||
volumes:
|
||
- flowable-db:/var/lib/postgresql/data
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "pg_isready -U flowable -d flowable"]
|
||
interval: 5s
|
||
timeout: 3s
|
||
retries: 10
|
||
networks: [cg]
|
||
|
||
flowable-rest:
|
||
image: docker.io/flowable/flowable-rest:latest
|
||
environment:
|
||
SPRING_DATASOURCE_DRIVER-CLASS-NAME: org.postgresql.Driver
|
||
SPRING_DATASOURCE_URL: jdbc:postgresql://flowable-db:5432/flowable
|
||
SPRING_DATASOURCE_USERNAME: flowable
|
||
SPRING_DATASOURCE_PASSWORD: flowable
|
||
ports:
|
||
- "8090:8080"
|
||
depends_on:
|
||
flowable-db:
|
||
condition: service_healthy
|
||
networks: [cg]
|
||
|
||
flowable-init:
|
||
image: docker.io/curlimages/curl:latest
|
||
restart: "no"
|
||
# registratie.bpmn + diploma-eligibility.dmn are streamed into this external volume by
|
||
# infra/seed-config.sh.
|
||
volumes:
|
||
- fl-bpmn:/work:ro
|
||
command:
|
||
- sh
|
||
- -c
|
||
- |
|
||
svc=http://flowable-rest:8080/flowable-rest/service/repository/deployments
|
||
dmn=http://flowable-rest:8080/flowable-rest/dmn-api/dmn-repository/deployments
|
||
until curl -sf -u rest-admin:test "$$svc" >/dev/null 2>&1; do echo "waiting for flowable-rest..."; sleep 3; done
|
||
# Deploy the DMN to the DMN engine and the BPMN to the process engine as SEPARATE deployments:
|
||
# flowable-rest does NOT cascade a .dmn bundled in a process .bar into the DMN engine, so the DMN
|
||
# must go via dmn-api. The process's DMN service task then resolves the decision across deployments
|
||
# by key (S-13, ADR-0016). Both steps are idempotent (skip if already deployed).
|
||
if curl -s -u rest-admin:test "$$dmn" | grep -q '"name":"diploma-eligibility.dmn"'; then
|
||
echo "diploma-eligibility DMN already deployed; skip"
|
||
else
|
||
curl -sf -u rest-admin:test -F 'file=@/work/diploma-eligibility.dmn;filename=diploma-eligibility.dmn' "$$dmn" >/dev/null && echo "deployed diploma-eligibility DMN"
|
||
fi
|
||
if curl -s -u rest-admin:test "$$svc?name=registratie" | grep -q '"name":"registratie"'; then
|
||
echo "registratie BPMN already deployed; skip"
|
||
else
|
||
curl -sf -u rest-admin:test -F 'file=@/work/registratie.bpmn;filename=registratie.bpmn' "$$svc" >/dev/null && echo "deployed registratie BPMN"
|
||
fi
|
||
depends_on:
|
||
flowable-rest:
|
||
condition: service_started
|
||
networks: [cg]
|
||
|
||
# ── ACL ──────────────────────────────────────────────────────────────────
|
||
acl:
|
||
build:
|
||
context: ../services/acl
|
||
dockerfile: Dockerfile
|
||
image: register-referentie/acl:dev
|
||
environment:
|
||
# OpenTelemetry traces → Tempo (S-16b, ADR-0023).
|
||
OTEL_EXPORTER_OTLP_ENDPOINT: http://tempo:4317
|
||
OTEL_EXPORTER_OTLP_PROTOCOL: grpc
|
||
OTEL_SERVICE_NAME: acl
|
||
# Overridable so verify-domain can point the ACL at the same OpenZaak host that
|
||
# owns the seeded zaaktype URL (host-consistent zaak creation, ADR-0009).
|
||
Acl__OpenZaak__BaseUrl: ${ACL_OPENZAAK_BASEURL:-http://openzaak:8000/}
|
||
Acl__OpenZaak__ClientId: big-reference-seed
|
||
Acl__OpenZaak__Secret: insecure-dev-secret-change-me
|
||
Acl__Defaults__Bronorganisatie: "517439943"
|
||
Acl__Defaults__VerantwoordelijkeOrganisatie: "517439943"
|
||
Acl__Defaults__Vertrouwelijkheidaanduiding: openbaar
|
||
# The ACL resolves the (server-assigned) zaaktype + diploma informatieobjecttype URLs from the
|
||
# Catalogi API by these stable business keys (S-27, ADR-0021) — no URL to capture and inject.
|
||
# BaseUrl above stays overridable because OpenZaak rejects a single-label host on zaak creation,
|
||
# so verify-domain still points the ACL at OpenZaak's container IP.
|
||
Acl__Defaults__ZaaktypeIdentificatie: BIG-REGISTRATIE
|
||
Acl__Defaults__InformatieobjecttypeOmschrijving: Diploma
|
||
# Objecten holds the register, OpenZaak holds the process (S-19a, ADR-0028). Both APIs take a
|
||
# static token, not a ZGW JWT. The objecttype URL is assigned at seed time, so the ACL resolves
|
||
# it by name — lazily, on the first approval, so no depends_on is needed here.
|
||
# Dotted host on purpose — see the `objecten.local` alias below (ADR-0029).
|
||
Acl__Objecten__BaseUrl: http://objecten.local:8000/
|
||
Acl__Objecten__Token: ${OBJECTEN_TOKEN:-1234567890abcdef1234567890abcdef12345678}
|
||
Acl__Objecten__ObjecttypenBaseUrl: http://objecttypen:8000/
|
||
Acl__Objecten__ObjecttypenToken: ${OBJECTTYPEN_TOKEN:-0123456789abcdef0123456789abcdef01234567}
|
||
Acl__Objecten__ObjecttypeName: RegisterRecord
|
||
ports:
|
||
- "8100:8080"
|
||
healthcheck:
|
||
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
|
||
interval: 5s
|
||
timeout: 3s
|
||
retries: 5
|
||
start_period: 10s
|
||
depends_on:
|
||
openzaak:
|
||
condition: service_healthy
|
||
networks: [cg]
|
||
|
||
# ── BIG Domain Service (S-05) ──────────────────────────────────────────────
|
||
# Orchestrates a registration: POST /registrations creates the aggregate and
|
||
# starts the registratie Flowable process; a hosted worker acquires the
|
||
# OpenZaakAanmaken job, opens a zaak via the ACL and completes it (ADR-0009).
|
||
# Talks only to Flowable (Workflow Client, §8.2) and the ACL (§8.1).
|
||
domain:
|
||
build:
|
||
context: ../services/domain
|
||
dockerfile: Dockerfile
|
||
image: register-referentie/domain:dev
|
||
environment:
|
||
# OpenTelemetry traces → Tempo (S-16b, ADR-0023).
|
||
OTEL_EXPORTER_OTLP_ENDPOINT: http://tempo:4317
|
||
OTEL_EXPORTER_OTLP_PROTOCOL: grpc
|
||
OTEL_SERVICE_NAME: domain
|
||
Flowable__BaseUrl: http://flowable-rest:8080/flowable-rest/
|
||
Flowable__Username: rest-admin
|
||
Flowable__Password: test
|
||
Acl__BaseUrl: http://acl:8080/
|
||
ports:
|
||
- "8130:8080"
|
||
healthcheck:
|
||
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
|
||
interval: 5s
|
||
timeout: 3s
|
||
retries: 5
|
||
start_period: 10s
|
||
depends_on:
|
||
acl:
|
||
condition: service_healthy
|
||
flowable-init:
|
||
condition: service_completed_successfully
|
||
networks: [cg]
|
||
|
||
# ── BFF ──────────────────────────────────────────────────────────────────
|
||
bff:
|
||
build:
|
||
context: ../services/bff
|
||
dockerfile: Dockerfile
|
||
image: register-referentie/bff:dev
|
||
environment:
|
||
# OpenTelemetry traces → Tempo (S-16b, ADR-0023).
|
||
OTEL_EXPORTER_OTLP_ENDPOINT: http://tempo:4317
|
||
OTEL_EXPORTER_OTLP_PROTOCOL: grpc
|
||
OTEL_SERVICE_NAME: bff
|
||
# The BFF is the portals' only backend; it validates digid tokens and fans out (ADR-0010).
|
||
# Keycloak (start-dev) derives the issuer from the request host, so the BFF authority and the
|
||
# verify token request both use keycloak:8080 to keep the issuer consistent.
|
||
Keycloak__Authority: http://keycloak:8080/realms/digid
|
||
# Behandelaars authenticate against the medewerker realm; the BFF validates it for /behandel/* (S-12c).
|
||
Keycloak__MedewerkerAuthority: http://keycloak:8080/realms/medewerker
|
||
Downstream__Domain__BaseUrl: http://domain:8080/
|
||
Downstream__Projection__BaseUrl: http://projection-api:8080/
|
||
# The beheer catalogus read reaches the ACL directly (S-15a, ADR-0025).
|
||
Downstream__Acl__BaseUrl: http://acl:8080/
|
||
ports:
|
||
- "8080:8080"
|
||
healthcheck:
|
||
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
|
||
interval: 5s
|
||
timeout: 3s
|
||
retries: 5
|
||
start_period: 10s
|
||
depends_on:
|
||
domain:
|
||
condition: service_healthy
|
||
projection-api:
|
||
condition: service_healthy
|
||
keycloak:
|
||
condition: service_started
|
||
networks: [cg]
|
||
|
||
# ── Read projection (S-06) ────────────────────────────────────────────────
|
||
# One Postgres DB backing the rebuildable read projection (PRD §8.4): the Event
|
||
# Subscriber writes it, projection-api reads it. See ADR-0008.
|
||
projection-db:
|
||
image: docker.io/library/postgres:16
|
||
environment:
|
||
POSTGRES_USER: projection
|
||
POSTGRES_PASSWORD: projection
|
||
POSTGRES_DB: projection
|
||
volumes:
|
||
- projection-db:/var/lib/postgresql/data
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "pg_isready -U projection -d projection"]
|
||
interval: 5s
|
||
timeout: 3s
|
||
retries: 10
|
||
networks: [cg]
|
||
|
||
# Consumes NRC notifications (abonnement callback) and projects zaak-created events
|
||
# into register_projection. Build context is the repo root: it shares the read model
|
||
# in services/projection-api/Projection.ReadModel.
|
||
event-subscriber:
|
||
build:
|
||
context: ..
|
||
dockerfile: services/event-subscriber/Dockerfile
|
||
image: register-referentie/event-subscriber:dev
|
||
environment:
|
||
# OpenTelemetry traces → Tempo (S-16b, ADR-0023).
|
||
OTEL_EXPORTER_OTLP_ENDPOINT: http://tempo:4317
|
||
OTEL_EXPORTER_OTLP_PROTOCOL: grpc
|
||
OTEL_SERVICE_NAME: event-subscriber
|
||
ConnectionStrings__Projection: Host=projection-db;Database=projection;Username=projection;Password=projection
|
||
# The subscriber enriches the projection with each zaak's reference (identificatie) by asking
|
||
# the ACL — the only code allowed to read ZGW (§8.1, #78).
|
||
Acl__BaseUrl: http://acl:8080/
|
||
# The bearer Open Notificaties must present on the abonnement callback. NRC's
|
||
# registration probe expects a 401 without it (ADR-0007). Dev-only token.
|
||
EventSubscriber__Webhook__AuthToken: ${NOTIFICATION_WEBHOOK_TOKEN:-Bearer big-reference-notifications}
|
||
ports:
|
||
- "8110:8080"
|
||
healthcheck:
|
||
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
|
||
interval: 5s
|
||
timeout: 3s
|
||
retries: 5
|
||
start_period: 15s
|
||
depends_on:
|
||
projection-db:
|
||
condition: service_healthy
|
||
acl:
|
||
condition: service_healthy
|
||
networks: [cg]
|
||
|
||
# The read side of the projection. Shares Projection.ReadModel, so build context is root.
|
||
projection-api:
|
||
build:
|
||
context: ..
|
||
dockerfile: services/projection-api/Dockerfile
|
||
image: register-referentie/projection-api:dev
|
||
environment:
|
||
# OpenTelemetry traces → Tempo (S-16b, ADR-0023).
|
||
OTEL_EXPORTER_OTLP_ENDPOINT: http://tempo:4317
|
||
OTEL_EXPORTER_OTLP_PROTOCOL: grpc
|
||
OTEL_SERVICE_NAME: projection-api
|
||
ConnectionStrings__Projection: Host=projection-db;Database=projection;Username=projection;Password=projection
|
||
ports:
|
||
- "8120:8080"
|
||
healthcheck:
|
||
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
|
||
interval: 5s
|
||
timeout: 3s
|
||
retries: 5
|
||
start_period: 15s
|
||
depends_on:
|
||
projection-db:
|
||
condition: service_healthy
|
||
networks: [cg]
|
||
|
||
# ── Self-Service portal (S-08d) ────────────────────────────────────────────
|
||
# nginx serves the Angular app and reverse-proxies /self-service + /openbaar to the BFF
|
||
# (same-origin, no CORS). The Playwright e2e drives it inside this network so the DigiD
|
||
# token issuer (keycloak:8080) matches the BFF's authority (ADR-0010).
|
||
self-service:
|
||
build:
|
||
context: ..
|
||
dockerfile: apps/self-service/Dockerfile
|
||
image: register-referentie/self-service:dev
|
||
ports:
|
||
- "8140:80"
|
||
healthcheck:
|
||
# 127.0.0.1, not localhost: nginx listens on IPv4 only, but localhost resolves to ::1 first.
|
||
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1/ || exit 1"]
|
||
interval: 5s
|
||
timeout: 3s
|
||
retries: 5
|
||
start_period: 10s
|
||
depends_on:
|
||
bff:
|
||
condition: service_healthy
|
||
keycloak:
|
||
condition: service_started
|
||
networks: [cg]
|
||
|
||
# The openbaar (public) register portal: nginx serves the Angular app and reverse-proxies
|
||
# /openbaar to the BFF. Anonymous — no DigiD, no Keycloak dependency (S-09).
|
||
openbaar:
|
||
build:
|
||
context: ..
|
||
dockerfile: apps/openbaar/Dockerfile
|
||
image: register-referentie/openbaar:dev
|
||
ports:
|
||
- "8141:80"
|
||
healthcheck:
|
||
# 127.0.0.1, not localhost: nginx listens on IPv4 only, but localhost resolves to ::1 first.
|
||
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1/ || exit 1"]
|
||
interval: 5s
|
||
timeout: 3s
|
||
retries: 5
|
||
start_period: 10s
|
||
depends_on:
|
||
bff:
|
||
condition: service_healthy
|
||
networks: [cg]
|
||
|
||
# The behandel portal: nginx serves the Angular app and reverse-proxies /behandel to the BFF.
|
||
# Behandelaars log in against the Keycloak medewerker realm (ADR-0013; S-12).
|
||
behandel:
|
||
build:
|
||
context: ..
|
||
dockerfile: apps/behandel/Dockerfile
|
||
image: register-referentie/behandel:dev
|
||
ports:
|
||
- "8142:80"
|
||
healthcheck:
|
||
# 127.0.0.1, not localhost: nginx listens on IPv4 only, but localhost resolves to ::1 first.
|
||
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1/ || exit 1"]
|
||
interval: 5s
|
||
timeout: 3s
|
||
retries: 5
|
||
start_period: 10s
|
||
depends_on:
|
||
bff:
|
||
condition: service_healthy
|
||
keycloak:
|
||
condition: service_started
|
||
networks: [cg]
|
||
|
||
# The beheer portal: nginx serves the Angular app and reverse-proxies /beheer to the BFF.
|
||
# Beheerders log in against the Keycloak medewerker realm (same realm as behandel, S-15a).
|
||
beheer:
|
||
build:
|
||
context: ..
|
||
dockerfile: apps/beheer/Dockerfile
|
||
image: register-referentie/beheer:dev
|
||
ports:
|
||
- "8143:80"
|
||
healthcheck:
|
||
# 127.0.0.1, not localhost: nginx listens on IPv4 only, but localhost resolves to ::1 first.
|
||
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1/ || exit 1"]
|
||
interval: 5s
|
||
timeout: 3s
|
||
retries: 5
|
||
start_period: 10s
|
||
depends_on:
|
||
bff:
|
||
condition: service_healthy
|
||
keycloak:
|
||
condition: service_started
|
||
networks: [cg]
|
||
|
||
# ── Objecttypen API (S-18a) — upstream Maykin image, verbatim ──────────────
|
||
# The register's objecttype catalogue. Same shape as the other CG modules: own DB + redis, an
|
||
# `-init` that runs setup_configuration (RUN_SETUP_CONFIG → migrate + provision a static API token)
|
||
# from the external config volume streamed in by infra/seed-config.sh, and a health-checked web
|
||
# service that depends on init completing.
|
||
objecttypen-db:
|
||
image: docker.io/library/postgres:17-alpine
|
||
environment:
|
||
POSTGRES_USER: objecttypes
|
||
POSTGRES_PASSWORD: objecttypes
|
||
POSTGRES_DB: objecttypes
|
||
volumes:
|
||
- objecttypen-db:/var/lib/postgresql/data
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "pg_isready -U objecttypes"]
|
||
interval: 5s
|
||
timeout: 3s
|
||
retries: 10
|
||
networks: [cg]
|
||
|
||
objecttypen-redis:
|
||
image: docker.io/library/redis:7
|
||
networks: [cg]
|
||
|
||
objecttypen-init:
|
||
image: docker.io/maykinmedia/objecttypes-api:${OBJECTTYPES_TAG:-3.4.2}
|
||
environment: &objecttypen-env
|
||
# 1 uWSGI worker, not the image default of 4×4: this API only serves single-request smoke
|
||
# checks and sits idle during the e2e step — 4 idle Django workers each pin ~200 MB and starve
|
||
# the shared CI runner (#144). Init ignores this (it runs setup_configuration, not uwsgi).
|
||
UWSGI_PROCESSES: "1"
|
||
UWSGI_THREADS: "2"
|
||
DJANGO_SETTINGS_MODULE: objecttypes.conf.docker
|
||
SECRET_KEY: ${OBJECTTYPES_SECRET_KEY:-dev-only-not-for-production}
|
||
DB_HOST: objecttypen-db
|
||
DB_NAME: objecttypes
|
||
DB_USER: objecttypes
|
||
DB_PASSWORD: objecttypes
|
||
ALLOWED_HOSTS: "*"
|
||
CACHE_DEFAULT: objecttypen-redis:6379/0
|
||
CACHE_AXES: objecttypen-redis:6379/0
|
||
DISABLE_2FA: "true"
|
||
OTEL_SDK_DISABLED: "true"
|
||
RUN_SETUP_CONFIG: "true"
|
||
command: /setup_configuration.sh
|
||
# data.yaml is streamed into this external volume by infra/seed-config.sh before start.
|
||
volumes:
|
||
- objecttypen-config:/app/setup_configuration:ro
|
||
depends_on:
|
||
objecttypen-db:
|
||
condition: service_healthy
|
||
objecttypen-redis:
|
||
condition: service_started
|
||
networks: [cg]
|
||
|
||
objecttypen:
|
||
image: docker.io/maykinmedia/objecttypes-api:${OBJECTTYPES_TAG:-3.4.2}
|
||
environment: *objecttypen-env
|
||
healthcheck:
|
||
test: ["CMD", "python", "-c", "import requests,sys; sys.exit(0 if requests.head('http://localhost:8000/admin/').status_code in (200,302) else 1)"]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 10
|
||
start_period: 30s
|
||
ports:
|
||
- "8020:8000"
|
||
depends_on:
|
||
objecttypen-init:
|
||
condition: service_completed_successfully
|
||
networks: [cg]
|
||
|
||
# ── RegisterRecord objecttype (S-18c) — API-seeded one-shot ────────────────
|
||
# The Objecttypen setup_configuration (3.4.2) can only provision tokens — no declarative objecttype
|
||
# step — so this one-shot creates the RegisterRecord objecttype + a published version over the API
|
||
# once Objecttypen is healthy (idempotent; ADR-0020 self-seed, ADR-0027 schema). The schema + script
|
||
# are streamed into the external config volume by infra/seed-config.sh, like the *-init volumes.
|
||
registerrecord-init:
|
||
image: docker.io/library/python:3-slim
|
||
environment:
|
||
OBJECTTYPEN: http://objecttypen:8000
|
||
OBJECTTYPEN_TOKEN: ${OBJECTTYPEN_TOKEN:-0123456789abcdef0123456789abcdef01234567}
|
||
SCHEMA: /config/registerrecord.schema.json
|
||
command: python /config/register.py
|
||
volumes:
|
||
- registerrecord-config:/config:ro
|
||
depends_on:
|
||
objecttypen:
|
||
condition: service_healthy
|
||
networks: [cg]
|
||
|
||
# ── Objecten API (S-18b) — upstream Maykin image, verbatim ─────────────────
|
||
# The authoritative object store. Same shape as Objecttypen (own DB + redis, an `-init` that runs
|
||
# setup_configuration from the external config volume, a health-checked web). Two differences: the
|
||
# DB is PostGIS (objects carry geometry), and setup_configuration registers the Objecttypen API
|
||
# (S-18a) as a trusted service so an object can reference its objecttype.
|
||
objecten-db:
|
||
image: docker.io/postgis/postgis:17-3.5
|
||
environment:
|
||
POSTGRES_USER: objects
|
||
POSTGRES_PASSWORD: objects
|
||
POSTGRES_DB: objects
|
||
volumes:
|
||
- objecten-db:/var/lib/postgresql/data
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "pg_isready -U objects"]
|
||
interval: 5s
|
||
timeout: 3s
|
||
retries: 10
|
||
networks: [cg]
|
||
|
||
objecten-redis:
|
||
image: docker.io/library/redis:7
|
||
networks: [cg]
|
||
|
||
objecten-init:
|
||
image: docker.io/maykinmedia/objects-api:${OBJECTS_TAG:-3.4.0}
|
||
environment: &objecten-env
|
||
# 1 uWSGI worker, not the image default of 4×4 — see the objecttypen note above (#144).
|
||
UWSGI_PROCESSES: "1"
|
||
UWSGI_THREADS: "2"
|
||
DJANGO_SETTINGS_MODULE: objects.conf.docker
|
||
SECRET_KEY: ${OBJECTS_SECRET_KEY:-dev-only-not-for-production}
|
||
DB_HOST: objecten-db
|
||
DB_NAME: objects
|
||
DB_USER: objects
|
||
DB_PASSWORD: objects
|
||
ALLOWED_HOSTS: "*"
|
||
CACHE_DEFAULT: objecten-redis:6379/0
|
||
CACHE_AXES: objecten-redis:6379/0
|
||
DISABLE_2FA: "true"
|
||
OTEL_SDK_DISABLED: "true"
|
||
CELERY_BROKER_URL: redis://objecten-redis:6379/1
|
||
CELERY_RESULT_BACKEND: redis://objecten-redis:6379/1
|
||
# Publish register-record events to NRC on the `objecten` kanaal (S-19b-1, ADR-0029). The NRC
|
||
# service + notifications_config are provisioned by setup_configuration
|
||
# (infra/objecten/setup_configuration/data.yaml), and objecten-celery below actually sends
|
||
# them — notifications_api_common only queues the task. See ADR-0028 for why S-19a left this
|
||
# off until all four pieces existed.
|
||
NOTIFICATIONS_DISABLED: "false"
|
||
RUN_SETUP_CONFIG: "true"
|
||
command: /setup_configuration.sh
|
||
# data.yaml is streamed into this external volume by infra/seed-config.sh before start.
|
||
volumes:
|
||
- objecten-config:/app/setup_configuration:ro
|
||
depends_on:
|
||
objecten-db:
|
||
condition: service_healthy
|
||
objecten-redis:
|
||
condition: service_started
|
||
# Objecten's setup_configuration registers the Objecttypen service; that service only needs to
|
||
# exist as config, but wait for Objecttypen to be up so the register is meaningful end to end.
|
||
objecttypen:
|
||
condition: service_healthy
|
||
networks: [cg]
|
||
|
||
objecten:
|
||
image: docker.io/maykinmedia/objects-api:${OBJECTS_TAG:-3.4.0}
|
||
environment: *objecten-env
|
||
healthcheck:
|
||
test: ["CMD", "python", "-c", "import requests,sys; sys.exit(0 if requests.head('http://localhost:8000/admin/').status_code in (200,302) else 1)"]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 10
|
||
start_period: 30s
|
||
ports:
|
||
- "8021:8000"
|
||
depends_on:
|
||
objecten-init:
|
||
condition: service_completed_successfully
|
||
networks:
|
||
cg:
|
||
# Objecten reflects the *request* Host into the `url` it returns, and
|
||
# notifications_api_common publishes that url as the notification's hoofdObject /
|
||
# resourceUrl — which NRC types as a URLField, and Django's URLValidator rejects a
|
||
# single-label host ("Voer een geldige URL in."). So every caller whose writes must be
|
||
# notified addresses Objecten by this dotted alias instead of `objecten` (ADR-0029).
|
||
# Reads are unaffected and still use the plain service name.
|
||
aliases:
|
||
- objecten.local
|
||
|
||
# The celery worker that actually delivers Objecten's notifications to NRC (S-19b-1, ADR-0029).
|
||
# notifications_api_common only schedules the send on transaction commit; without a worker the
|
||
# task sits in redis forever and every register write is silently undelivered. Mirrors oz-celery.
|
||
# No beat: Objecten is a publisher, not a subscriber — nrc-beat drains the delivery queue.
|
||
objecten-celery:
|
||
image: docker.io/maykinmedia/objects-api:${OBJECTS_TAG:-3.4.0}
|
||
environment: *objecten-env
|
||
command: /celery_worker.sh
|
||
depends_on:
|
||
objecten-init:
|
||
condition: service_completed_successfully
|
||
networks: [cg]
|
||
|
||
# ── Observability backplane (S-16a, ADR-0023) ──────────────────────────────
|
||
# Grafana-native stack: Tempo ingests OTLP traces (the .NET services export
|
||
# straight to it — no collector hop, S-16b), Prometheus scrapes service
|
||
# /metrics (S-16c), and Grafana reads both with datasources auto-provisioned.
|
||
# Config is baked into small built images (COPY) rather than streamed into
|
||
# external config volumes like the upstream CG modules — these aren't verbatim
|
||
# peer images, so a built image is the simpler path that still reaches sibling
|
||
# containers on the CI runner. Not in WAIT_SVCS: run-observability-check.sh
|
||
# polls Grafana itself, so no in-image healthcheck tool is needed.
|
||
tempo:
|
||
build:
|
||
context: ./observability/tempo
|
||
image: register-referentie/tempo:dev
|
||
command: ["-config.file=/etc/tempo.yaml"]
|
||
# Cap the backplane's footprint so it can't starve the app stack + the Playwright browser on the
|
||
# memory-tight CI runner (verify-e2e OOM history, commit d5e5fa2). Generous vs idle (~150M).
|
||
mem_limit: 400m
|
||
networks: [cg]
|
||
|
||
prometheus:
|
||
build:
|
||
context: ./observability/prometheus
|
||
image: register-referentie/prometheus:dev
|
||
mem_limit: 400m
|
||
ports:
|
||
- "9090:9090"
|
||
networks: [cg]
|
||
|
||
grafana:
|
||
build:
|
||
context: ./observability/grafana
|
||
image: register-referentie/grafana:dev
|
||
mem_limit: 512m
|
||
environment:
|
||
GF_SECURITY_ADMIN_USER: admin
|
||
GF_SECURITY_ADMIN_PASSWORD: admin
|
||
GF_AUTH_ANONYMOUS_ENABLED: "true"
|
||
ports:
|
||
- "3000:3000"
|
||
depends_on:
|
||
- tempo
|
||
- prometheus
|
||
networks: [cg]
|
||
|
||
volumes:
|
||
oz-db:
|
||
nrc-db:
|
||
flowable-db:
|
||
projection-db:
|
||
objecttypen-db:
|
||
objecten-db:
|
||
# Config volumes — created and populated out-of-band by infra/seed-config.sh
|
||
# (docker cp), because bind mounts don't reach sibling containers on the CI
|
||
# runner. `external` keeps the names deterministic; the seed step manages them.
|
||
oz-config:
|
||
external: true
|
||
name: rr-oz-config
|
||
nrc-config:
|
||
external: true
|
||
name: rr-nrc-config
|
||
kc-realms:
|
||
external: true
|
||
name: rr-kc-realms
|
||
fl-bpmn:
|
||
external: true
|
||
name: rr-fl-bpmn
|
||
objecttypen-config:
|
||
external: true
|
||
name: rr-objecttypen-config
|
||
registerrecord-config:
|
||
external: true
|
||
name: rr-registerrecord-config
|
||
objecten-config:
|
||
external: true
|
||
name: rr-objecten-config
|
||
|
||
networks:
|
||
cg:
|