CI / build (pull_request) Successful in 4m31s
CI / lint (pull_request) Successful in 4m46s
CI / unit (pull_request) Successful in 1m35s
CI / frontend (pull_request) Successful in 4m0s
CI / mutation (pull_request) Successful in 6m47s
CI / verify-stack (pull_request) Failing after 17m35s
Builds the four pieces ADR-0028 deliberately left absent, and turns
`NOTIFICATIONS_DISABLED` back off:
- `objecten-celery`, a worker on the Objecten image (mirrors `oz-celery`), plus
`CELERY_BROKER_URL`/`RESULT_BACKEND` on objecten-redis db 1 (db 0 is the cache).
Without it `notifications_api_common` queues the send and nothing ever ships it.
- An `nrc` service + `notifications_config` in Objecten's setup_configuration,
reusing the `big-reference-seed` credential OpenZaak publishes with.
- The `objecten` kanaal in NRC's setup_configuration — the name is fixed by the
Objects API (`NOTIFICATIONS_KANAAL`), and publishing to an unregistered kanaal is
what the failing check reported first.
- `SITE_DOMAIN: objecten.local:8000` + an `objecten.local` network alias: NRC
validates `hoofdObject`/`resourceUrl` with Django's URLValidator, which rejects a
single-label host, so `objecten:8000` is refused with "Voer een geldige URL in."
The alias keeps the dotted host resolvable so the URL still dereferences.
ADR-0029 records it; ADR-0028's ceiling now points there.
Makes `make verify-objecten-notifications` (dc9ca2c) pass.
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.
|
||
Acl__Objecten__BaseUrl: http://objecten: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"
|
||
# NRC validates hoofdObject/resourceUrl with Django's URLValidator, which rejects a
|
||
# single-label host — so notifications built from `objecten:8000` are refused with
|
||
# "Voer een geldige URL in." SITE_DOMAIN fixes the host Objecten puts in its notifications
|
||
# (objects.utils.get_domain), and the `objecten.local` network alias below keeps that host
|
||
# resolvable in-network so a subscriber can actually fetch the record it points at (S-19b-2).
|
||
SITE_DOMAIN: objecten.local:8000
|
||
IS_HTTPS: "no"
|
||
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:
|
||
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:
|