fix(infra): bake config into images so compose-smoke passes on CI (refs #30)
Some checks failed
CI / lint (pull_request) Successful in 50s
CI / build (pull_request) Successful in 40s
CI / unit (pull_request) Successful in 45s
CI / compose-smoke (pull_request) Failing after 3m31s

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) <noreply@anthropic.com>
This commit is contained in:
2026-06-24 13:06:56 +02:00
parent 88de47d1bb
commit 9ff7937055
11 changed files with 185 additions and 37 deletions

View File

@@ -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:

View File

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

View File

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