# 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 the BIG catalogus and note the zaaktype URL: # python infra/openzaak/seed_catalogus.py # Then set ACL_ZAAKTYPE_URL in a .env file or your shell and re-up the acl # service: # export ACL_ZAAKTYPE_URL=http://openzaak:8000/catalogi/api/v1/zaaktypen/ # 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 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 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 is streamed into this external volume by infra/seed-config.sh. volumes: - fl-bpmn:/work:ro command: - sh - -c - | base=http://flowable-rest:8080/flowable-rest/service/repository/deployments until curl -sf -u rest-admin:test "$$base" >/dev/null 2>&1; do echo "waiting for flowable-rest..."; sleep 3; done if curl -s -u rest-admin:test "$$base?name=registratie" | grep -q '"name":"registratie"'; then echo "registratie already deployed; skip" else curl -sf -u rest-admin:test -F 'file=@/work/registratie.bpmn;filename=registratie.bpmn' "$$base" >/dev/null && echo "deployed registratie" fi depends_on: flowable-rest: condition: service_started networks: [cg] # ── ACL ────────────────────────────────────────────────────────────────── acl: build: context: ../services/acl dockerfile: Dockerfile image: register-referentie/acl:dev environment: # 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 # Override with the real zaaktype URL after running seed_catalogus.py. Acl__Defaults__ZaaktypeUrl: ${ACL_ZAAKTYPE_URL:-http://openzaak:8000/catalogi/api/v1/zaaktypen/00000000-0000-0000-0000-000000000000} 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: 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: # 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 Downstream__Domain__BaseUrl: http://domain:8080/ Downstream__Projection__BaseUrl: http://projection-api: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: ConnectionStrings__Projection: Host=projection-db;Database=projection;Username=projection;Password=projection # 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 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: 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] volumes: oz-db: nrc-db: flowable-db: projection-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 networks: cg: