Files
register-referentie/docs/runbooks/gitea-actions-gotchas.md
Niek Otten b349dff496
All checks were successful
CI / lint (pull_request) Successful in 49s
CI / build (pull_request) Successful in 44s
CI / unit (pull_request) Successful in 44s
CI / compose-smoke (pull_request) Successful in 4m15s
refactor(infra): use upstream images verbatim, seed config via docker cp (refs #30)
Drops the inline-build images for the upstream services. The compose now
references the published images directly (openzaak/open-zaak,
openzaak/open-notificaties, keycloak, curl, flowable-rest) with no build for
them, and the config they need is streamed into external named volumes by
infra/seed-config.sh:

  rr-oz-config  -> oz-init     /app/setup_configuration   (data.yaml)
  rr-kc-realms  -> keycloak    /opt/keycloak/data/import   (realm exports)
  rr-fl-bpmn    -> flowable-init /work                     (registratie.bpmn)

How: the seeder creates each volume, `docker create`s a throwaway helper that
mounts it, `docker cp`s the files in, and removes it. docker cp streams over the
Docker API, so it works in Docker-in-Docker (the CI runner) where bind mounts
mount empty. It uses plain `docker create`/`cp` — NOT `docker compose create`,
which podman-compose (local dev) lacks. `external: true` fixed names keep the
volumes identical across docker compose and podman-compose.

Consequence: bare `docker compose up` no longer self-seeds, so use `make up`
(seeds then starts). Every `*-up` target seeds first; `*-down` removes the
external volume. acl/bff are still built (they're our apps, not upstream images).

Verified end-to-end on podman-compose: `make keycloak-up` seeds rr-kc-realms,
the upstream Keycloak mounts it, and --import-realm imports all four realms
(digid realm returns 200). Seeder runs in ~2s.

Docs updated: gitea-actions-gotchas.md, ci.md, openzaak.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 10:22:14 +02:00

6.0 KiB

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

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: the upstream images are used verbatim (no build); config is streamed into external named volumes with docker cp. infra/seed-config.sh creates a fixed- name volume per asset, runs a throwaway helper container that mounts it, and docker cps the files in. docker cp streams bytes over the Docker API, so it works no matter where the daemon runs (including Docker-in-Docker). The services then mount those volumes:

Asset External volume Mounted by → at
OpenZaak setup_configuration/data.yaml rr-oz-config oz-init/app/setup_configuration
Keycloak realm exports rr-kc-realms keycloak/opt/keycloak/data/import
workflows/registratie.bpmn rr-fl-bpmn flowable-init/work

The volumes are declared external: true with fixed name:s so they resolve identically under docker compose and podman-compose. The seed step (make runs it before every up) recreates them fresh each time; make down / the per-service *-down targets remove them. Open Notificaties needs no config at all — nrc-init runs migrations only.

Two hard constraints drove this design:

  • Use plain docker volume create / docker run / docker cpnot docker compose create, which podman-compose (the local dev runtime) does not implement.
  • docker cp rather than a bind mount of the source dir, because that bind mount is exactly what fails on the containerized runner.

Consequence: bare docker compose up no longer self-seeds — the external volumes must be populated first, so use make up (or make <svc>-up), which seeds then starts. CI uses make smoke, which does the same.

Why not the alternatives.

  • Bake into a (possibly inline) image — clean and portable, but it's a build; rejected here because the goal was to use the upstream images verbatim.
  • Compose configs: with inline content — Compose materialises these as a temp file on the client side and bind-mounts it → same daemon-can't-see-it problem.
  • A self-hosted runner that runs jobs on the host — bind mounts would then work with zero seeding, but it reintroduces a bespoke runner and undoes the move to the hosted ubuntu-latest label.

No bind mounts of these config files remain, so the SELinux :z/:Z relabel flag is no longer needed anywhere in infra/ (named volumes don't need relabeling).

--wait fails on one-shot containers with no dependant

docker compose up --wait treats a service that exits as a failure of the "stay up" condition — unless another service depends on it with condition: service_completed_successfully. Our init jobs oz-init and nrc-init are fine (openzaak/nrc-web depend on their completion), but flowable-init deploys the BPMN and exits 0 with no dependant, so a whole-project --wait fails the moment it exits — even with everything else healthy. The symptom is a compose-smoke failure whose last compose line is:

container infra-flowable-init-1 exited (0)

Fix. The smoke does not --wait on the whole project. It starts everything with up -d, then up -d --wait <services> only for the durable, health-checked services (openzaak nrc-web acl bff — see WAIT_SVCS in the Makefile). One-shots still run (and deploy), they just don't gate --wait.

This also removed the old external curl http://localhost:8080/health check: the CI job runs in a container and can't reach published host ports at localhost, and the per-service healthchecks (which run inside the containers) already prove readiness, so --wait succeeding is the smoke.

--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.