From 9ff79370552aff6767ef303c589a87c10ae848ad Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Wed, 24 Jun 2026 13:06:56 +0200 Subject: [PATCH] fix(infra): bake config into images so compose-smoke passes on CI (refs #30) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of the compose-smoke failure (found in the runner logs): oz-init-1 | CommandError: Yaml file `/app/setup_configuration/data.yaml` does not exist. The ubuntu-latest runner runs the job inside a container, so `docker compose up` starts the stack as SIBLING containers via the host daemon. A relative bind mount (./openzaak/setup_configuration) resolves to a path inside the job container that the daemon can't see, so Docker mounts an empty dir and the init container can't find data.yaml. The same trap hit nrc-init (data.yaml), flowable-init (the BPMN) and keycloak (realm import). Fix: bake the assets into small derived images instead of bind-mounting: - infra/openzaak/Dockerfile -> register-referentie/openzaak:dev - infra/opennotificaties/Dockerfile-> register-referentie/opennotificaties:dev - infra/keycloak/Dockerfile -> register-referentie/keycloak:dev - flowable-init: build.dockerfile_inline bakes workflows/registratie.bpmn Base versions stay build args (OPENZAAK_TAG / OPENNOTIFICATIES_TAG), so the pinning is unchanged. Applied to both the consolidated compose and the per-service composes, so local Podman and CI use one mechanism — no bind mounts, no SELinux `:z`, no world-readable requirement. Verified locally: `podman build` of the OpenZaak and BPMN images produces the file at the expected in-container path. Docs: docs/runbooks/gitea-actions-gotchas.md explains the DinD bind-mount trap and the bake fix; openzaak.md and ci.md point at it. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/runbooks/ci.md | 6 ++ docs/runbooks/gitea-actions-gotchas.md | 83 +++++++++++++++++++++++ docs/runbooks/openzaak.md | 8 ++- infra/docker-compose.yml | 45 +++++++----- infra/flowable/docker-compose.yml | 12 +++- infra/keycloak/Dockerfile | 7 ++ infra/keycloak/docker-compose.yml | 7 +- infra/opennotificaties/Dockerfile | 7 ++ infra/opennotificaties/docker-compose.yml | 22 +++--- infra/openzaak/Dockerfile | 12 ++++ infra/openzaak/docker-compose.yml | 13 ++-- 11 files changed, 185 insertions(+), 37 deletions(-) create mode 100644 docs/runbooks/gitea-actions-gotchas.md create mode 100644 infra/keycloak/Dockerfile create mode 100644 infra/opennotificaties/Dockerfile create mode 100644 infra/openzaak/Dockerfile diff --git a/docs/runbooks/ci.md b/docs/runbooks/ci.md index 3758516..6432cc6 100644 --- a/docs/runbooks/ci.md +++ b/docs/runbooks/ci.md @@ -22,6 +22,12 @@ All `uses:` references are absolute, tag-pinned URLs (`https://github.com/action `https://github.com/actions/setup-dotnet@v4`) per CLAUDE.md §8.7 and §15 — Gitea Actions resolves them from GitHub. +> **`compose-smoke` runs on a containerized runner.** Workspace bind mounts do +> **not** reach the sibling containers Compose starts, so config/assets are baked +> into derived images instead of being mounted. If you add a service that needs a +> repo file at runtime, bake it — don't bind-mount it. See +> [gitea-actions-gotchas.md](gitea-actions-gotchas.md). + ## Running CI locally (`make ci`) Until the runner exists, run the full pipeline yourself before pushing: diff --git a/docs/runbooks/gitea-actions-gotchas.md b/docs/runbooks/gitea-actions-gotchas.md new file mode 100644 index 0000000..ebe6834 --- /dev/null +++ b/docs/runbooks/gitea-actions-gotchas.md @@ -0,0 +1,83 @@ +# Gitea Actions gotchas + +Known differences between Gitea Actions (our CI) and a plain local run, and the +workarounds we adopted. Referenced by `CLAUDE.md` §8.7 and §15. + +## Bind mounts don't reach Compose services on the hosted runner + +**Symptom.** `make smoke` is green locally but the `compose-smoke` CI job fails +with the OpenZaak init container exiting 1: + +``` +oz-init-1 | CommandError: Yaml file `/app/setup_configuration/data.yaml` does not exist. +``` + +Migrations run fine; only the step that reads a **mounted** file fails. The same +class of failure hits any service that bind-mounts a workspace path — +`nrc-init` (its `data.yaml`), `flowable-init` (the BPMN), `keycloak` (the realm +import dir). + +**Cause.** The `ubuntu-latest` runner executes the whole job **inside a +container** (`docker.gitea.com/runner-images:ubuntu-latest`). When the job then +runs `docker compose ... up`, Compose talks to the host's Docker daemon and +starts the stack as **sibling containers**. A relative bind mount such as + +```yaml +volumes: + - ./openzaak/setup_configuration:/app/setup_configuration:ro +``` + +is resolved by Compose to an absolute path **inside the job container** +(`/workspace/eho/register-referentie/infra/openzaak/setup_configuration`). The +daemon then looks for that path on **its own host**, doesn't find it, and +auto-creates an **empty directory** to mount. The container starts with an empty +mount point, so the file appears "missing". + +This is the classic Docker-in-Docker / sibling-container bind-mount trap. It does +not happen on a runner that executes jobs directly on the host (the previous +self-hosted `respellion-linux` setup), which is why switching to `ubuntu-latest` +exposed it. + +**Fix: bake assets into derived images instead of bind-mounting them.** Anything +a Compose service needs at runtime that lives in the repo is `COPY`-ed into a +small derived image, so it is present regardless of where the daemon runs: + +| Asset | Derived image | Dockerfile | +|---|---|---| +| OpenZaak `setup_configuration/data.yaml` | `register-referentie/openzaak:dev` | `infra/openzaak/Dockerfile` | +| Open Notificaties `setup_configuration/data.yaml` | `register-referentie/opennotificaties:dev` | `infra/opennotificaties/Dockerfile` | +| Keycloak realm exports | `register-referentie/keycloak:dev` | `infra/keycloak/Dockerfile` | +| `workflows/registratie.bpmn` | `register-referentie/flowable-init:dev` | `build.dockerfile_inline` in the compose files | + +The base image tag stays a build `arg` (`OPENZAAK_TAG`, `OPENNOTIFICATIES_TAG`) +so version pinning is unchanged. The three `*-init`/web/celery services that share +a base now share one built image tag, so the bake happens once per `up --build`. + +**Why not the alternatives.** + +- *Compose `configs:` with inline `content`* — Compose materialises these as a + temp file on the **client** side and bind-mounts it, so it hits the exact same + daemon-can't-see-the-path problem. +- *A self-hosted runner that runs jobs on the host* — works, but reintroduces a + bespoke runner and undoes the move to the hosted `ubuntu-latest` label. + +**Consequence for local dev.** There is now no bind mount of these config files, +so the SELinux `:z`/`:Z` relabel flag is no longer needed anywhere in `infra/`, +and rootless Podman no longer needs the files to be world-readable. One mechanism +(build) works on both Podman locally and Docker-in-Docker in CI. + +## `--wait` needs an explicit timeout + +`docker compose up --wait` defaults to a 60-second timeout in some Compose v2 +releases. A cold OpenZaak migrate alone takes ~50 s, so the smoke target passes +`--wait-timeout 300` (see `Makefile`). The 3-minute Definition-of-Done budget +still holds — this just stops `--wait` giving up before the stack is healthy. + +## PostGIS readiness vs. `pg_isready` + +`pg_isready` reports the server is accepting connections as soon as the TCP port +is open — **before** the `postgis/postgis` image has finished running its +`CREATE EXTENSION postgis` init scripts. An init container that starts migrating +in that window can fail on a missing PostGIS. The db healthchecks therefore add a +`SELECT PostGIS_Version()` probe so dependents wait for the extension, not just +the port. diff --git a/docs/runbooks/openzaak.md b/docs/runbooks/openzaak.md index 107a141..dc8b7fb 100644 --- a/docs/runbooks/openzaak.md +++ b/docs/runbooks/openzaak.md @@ -71,5 +71,9 @@ The Makefile auto-points `DOCKER_HOST` at the Podman socket when it exists, so t but OZ→NRC delivery wiring + re-enabling lands with **S-06**. - **Zaaktype is a concept**, not published (publishing needs roltypen/statustypen/ resultaattypen — beyond the lean seed). List with `?status=alles`. -- **Image tag.** Currently `openzaak/open-zaak:latest` via `${OPENZAAK_TAG}`; pin to - a known-good tag (ADR-0002 follow-up). +- **Image tag.** Pinned to `openzaak/open-zaak:1.28.2` via `${OPENZAAK_TAG}` (bump + deliberately, not via `:latest`). +- **Config is baked, not mounted.** `setup_configuration/data.yaml` is `COPY`-ed into + a derived image (`infra/openzaak/Dockerfile`) rather than bind-mounted, so the + init container finds it on Gitea's containerized CI runner too. See + [gitea-actions-gotchas.md](gitea-actions-gotchas.md). diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index 699813e..ac94a62 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -47,7 +47,12 @@ services: networks: [cg] oz-init: - image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-1.28.2} + build: + context: ./openzaak + dockerfile: Dockerfile + args: + OPENZAAK_TAG: ${OPENZAAK_TAG:-1.28.2} + image: register-referentie/openzaak:dev environment: &oz-env DJANGO_SETTINGS_MODULE: openzaak.conf.docker SECRET_KEY: ${OZ_SECRET_KEY:-dev-only-not-for-production} @@ -68,8 +73,6 @@ services: OPENZAAK_SUPERUSER_EMAIL: admin@localhost RUN_SETUP_CONFIG: "true" command: /setup_configuration.sh - volumes: - - ./openzaak/setup_configuration:/app/setup_configuration:ro depends_on: oz-db: condition: service_healthy @@ -78,7 +81,7 @@ services: networks: [cg] openzaak: - image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-1.28.2} + image: register-referentie/openzaak:dev 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)"] @@ -94,7 +97,7 @@ services: networks: [cg] oz-celery: - image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-1.28.2} + image: register-referentie/openzaak:dev environment: *oz-env command: /celery_worker.sh depends_on: @@ -124,7 +127,12 @@ services: networks: [cg] nrc-init: - image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-1.16.1} + build: + context: ./opennotificaties + dockerfile: Dockerfile + args: + OPENNOTIFICATIES_TAG: ${OPENNOTIFICATIES_TAG:-1.16.1} + image: register-referentie/opennotificaties:dev environment: &nrc-env DJANGO_SETTINGS_MODULE: nrc.conf.docker SECRET_KEY: ${NRC_SECRET_KEY:-dev-only-not-for-production} @@ -144,8 +152,6 @@ services: OPENNOTIFICATIES_SUPERUSER_EMAIL: admin@localhost RUN_SETUP_CONFIG: "true" command: /setup_configuration.sh - volumes: - - ./opennotificaties/setup_configuration:/app/setup_configuration:ro depends_on: nrc-db: condition: service_healthy @@ -156,7 +162,7 @@ services: networks: [cg] nrc-web: - image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-1.16.1} + image: register-referentie/opennotificaties:dev 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)"] @@ -172,7 +178,7 @@ services: networks: [cg] nrc-celery: - image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-1.16.1} + image: register-referentie/opennotificaties:dev environment: *nrc-env command: /celery_worker.sh depends_on: @@ -182,7 +188,10 @@ services: # ── Keycloak (S-02) ────────────────────────────────────────────────────── keycloak: - image: quay.io/keycloak/keycloak:26.1 + build: + context: ./keycloak + dockerfile: Dockerfile + image: register-referentie/keycloak:dev command: ["start-dev", "--import-realm"] environment: KC_BOOTSTRAP_ADMIN_USERNAME: admin @@ -193,8 +202,6 @@ services: KC_HTTP_ENABLED: "true" ports: - "8180:8080" - volumes: - - ./keycloak/realms:/opt/keycloak/data/import:ro networks: [cg] # ── Flowable (S-03) ────────────────────────────────────────────────────── @@ -228,10 +235,16 @@ services: networks: [cg] flowable-init: - image: docker.io/curlimages/curl:latest + # The BPMN is baked into a tiny curl image (build context = repo-root + # workflows/) instead of bind-mounted, so the deploy works on Gitea's + # containerized runner too. See docs/runbooks/gitea-actions-gotchas.md. + build: + context: ../workflows + dockerfile_inline: | + FROM docker.io/curlimages/curl:latest + COPY registratie.bpmn /work/registratie.bpmn + image: register-referentie/flowable-init:dev restart: "no" - volumes: - - ../workflows/registratie.bpmn:/work/registratie.bpmn:ro command: - sh - -c diff --git a/infra/flowable/docker-compose.yml b/infra/flowable/docker-compose.yml index 0aa879a..c66caeb 100644 --- a/infra/flowable/docker-compose.yml +++ b/infra/flowable/docker-compose.yml @@ -38,10 +38,16 @@ services: # Deploys workflows/registratie.bpmn via the REST API once flowable-rest is up. # Idempotent: skips if a deployment named "registratie" already exists. flowable-init: - image: docker.io/curlimages/curl:latest + # The BPMN is baked into a tiny curl image (build context = repo-root + # workflows/) instead of bind-mounted, so the deploy works on Gitea's + # containerized runner too. See docs/runbooks/gitea-actions-gotchas.md. + build: + context: ../../workflows + dockerfile_inline: | + FROM docker.io/curlimages/curl:latest + COPY registratie.bpmn /work/registratie.bpmn + image: register-referentie/flowable-init:dev restart: "no" - volumes: - - ../../workflows/registratie.bpmn:/work/registratie.bpmn:ro,z command: - sh - -c diff --git a/infra/keycloak/Dockerfile b/infra/keycloak/Dockerfile new file mode 100644 index 0000000..baec0c5 --- /dev/null +++ b/infra/keycloak/Dockerfile @@ -0,0 +1,7 @@ +# Derived Keycloak image with the four realm exports baked into the import dir. +# `start-dev --import-realm` reads /opt/keycloak/data/import at boot. Baking the +# realms in (rather than bind-mounting ./realms) keeps the import working on +# Gitea's containerized runner, where bind mounts do not reach sibling +# containers. See docs/runbooks/gitea-actions-gotchas.md. +FROM quay.io/keycloak/keycloak:26.1 +COPY realms /opt/keycloak/data/import diff --git a/infra/keycloak/docker-compose.yml b/infra/keycloak/docker-compose.yml index b47dcc9..eaf9088 100644 --- a/infra/keycloak/docker-compose.yml +++ b/infra/keycloak/docker-compose.yml @@ -9,7 +9,10 @@ # Admin console: http://localhost:8180/ (admin / admin — dev only) services: keycloak: - image: quay.io/keycloak/keycloak:26.1 + build: + context: . + dockerfile: Dockerfile + image: register-referentie/keycloak:dev command: ["start-dev", "--import-realm"] environment: KC_BOOTSTRAP_ADMIN_USERNAME: admin @@ -21,8 +24,6 @@ services: KC_HTTP_ENABLED: "true" ports: - "8180:8080" - volumes: - - ./realms:/opt/keycloak/data/import:ro,z networks: [cg] networks: diff --git a/infra/opennotificaties/Dockerfile b/infra/opennotificaties/Dockerfile new file mode 100644 index 0000000..f35f2ff --- /dev/null +++ b/infra/opennotificaties/Dockerfile @@ -0,0 +1,7 @@ +# Derived Open Notificaties image with the setup_configuration YAML baked in. +# Rationale is identical to infra/openzaak/Dockerfile: bind mounts do not reach +# sibling containers on Gitea's containerized runner, so nrc-init would not find +# data.yaml. See docs/runbooks/gitea-actions-gotchas.md. +ARG OPENNOTIFICATIES_TAG=1.16.1 +FROM docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG} +COPY setup_configuration /app/setup_configuration diff --git a/infra/opennotificaties/docker-compose.yml b/infra/opennotificaties/docker-compose.yml index 8f05ff8..d0b2836 100644 --- a/infra/opennotificaties/docker-compose.yml +++ b/infra/opennotificaties/docker-compose.yml @@ -19,10 +19,13 @@ services: volumes: - nrc-db:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U opennotificaties -d opennotificaties"] + # pg_isready only checks TCP; the second clause verifies PostGIS is installed + # so nrc-init migrations can safely start (avoids race on cold container start). + test: ["CMD-SHELL", "pg_isready -U opennotificaties -d opennotificaties && psql -U opennotificaties -d opennotificaties -c 'SELECT PostGIS_Version();' -q 2>/dev/null"] interval: 5s - timeout: 3s - retries: 10 + timeout: 5s + retries: 30 + start_period: 15s networks: [cg] nrc-redis: @@ -30,7 +33,12 @@ services: networks: [cg] nrc-init: - image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-1.16.1} + build: + context: . + dockerfile: Dockerfile + args: + OPENNOTIFICATIES_TAG: ${OPENNOTIFICATIES_TAG:-1.16.1} + image: register-referentie/opennotificaties:dev environment: &nrc-env DJANGO_SETTINGS_MODULE: nrc.conf.docker SECRET_KEY: ${NRC_SECRET_KEY:-dev-only-not-for-production} @@ -50,8 +58,6 @@ services: OPENNOTIFICATIES_SUPERUSER_EMAIL: admin@localhost RUN_SETUP_CONFIG: "true" command: /setup_configuration.sh - volumes: - - ./setup_configuration:/app/setup_configuration:ro,z depends_on: nrc-db: condition: service_healthy @@ -60,7 +66,7 @@ services: networks: [cg] nrc-web: - image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-1.16.1} + image: register-referentie/opennotificaties:dev 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)"] @@ -76,7 +82,7 @@ services: networks: [cg] nrc-celery: - image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-1.16.1} + image: register-referentie/opennotificaties:dev environment: *nrc-env command: /celery_worker.sh depends_on: diff --git a/infra/openzaak/Dockerfile b/infra/openzaak/Dockerfile new file mode 100644 index 0000000..f827a81 --- /dev/null +++ b/infra/openzaak/Dockerfile @@ -0,0 +1,12 @@ +# Derived OpenZaak image with the setup_configuration YAML baked in. +# +# Why bake instead of bind-mount: Gitea's containerized `ubuntu-latest` runner +# runs the job inside a container, so `docker compose` starts the stack as +# SIBLING containers via the host daemon. A bind mount of a workspace path is +# then resolved on the daemon host (where it does not exist), and Docker mounts +# an empty directory — oz-init would not find data.yaml and exits 1. Baking the +# file into the image makes it present regardless of runtime (local Podman or +# CI Docker-in-Docker). See docs/runbooks/gitea-actions-gotchas.md. +ARG OPENZAAK_TAG=1.28.2 +FROM docker.io/openzaak/open-zaak:${OPENZAAK_TAG} +COPY setup_configuration /app/setup_configuration diff --git a/infra/openzaak/docker-compose.yml b/infra/openzaak/docker-compose.yml index b61bfe9..4eee276 100644 --- a/infra/openzaak/docker-compose.yml +++ b/infra/openzaak/docker-compose.yml @@ -32,7 +32,12 @@ services: networks: [cg] oz-init: - image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-1.28.2} + build: + context: . + dockerfile: Dockerfile + args: + OPENZAAK_TAG: ${OPENZAAK_TAG:-1.28.2} + image: register-referentie/openzaak:dev environment: &oz-env DJANGO_SETTINGS_MODULE: openzaak.conf.docker SECRET_KEY: ${OZ_SECRET_KEY:-dev-only-not-for-production} @@ -55,8 +60,6 @@ services: OPENZAAK_SUPERUSER_EMAIL: admin@localhost RUN_SETUP_CONFIG: "true" command: /setup_configuration.sh - volumes: - - ./setup_configuration:/app/setup_configuration:ro depends_on: oz-db: condition: service_healthy @@ -65,7 +68,7 @@ services: networks: [cg] openzaak: - image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-1.28.2} + image: register-referentie/openzaak:dev 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)"] @@ -81,7 +84,7 @@ services: networks: [cg] oz-celery: - image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-1.28.2} + image: register-referentie/openzaak:dev environment: *oz-env command: /celery_worker.sh depends_on: