`make smoke` errored locally because podman-compose doesn't implement
`docker compose up --wait` (`unrecognized arguments: --wait`).
Replace the `--wait` step with infra/wait-healthy.sh, which polls each durable
health-checked service ($(WAIT_SVCS)) via `docker ps` + `docker inspect
'{{.State.Health.Status}}'`. This:
- works on both docker compose (CI) and podman-compose (local) — only plain
docker primitives, no `--wait`;
- reads the in-container healthcheck, so it needs no host port access (the CI
runner can't reach published ports);
- ignores the one-shot init jobs, sidestepping the "--wait fails when a
consumer-less one-shot exits 0" issue (flowable-init).
Verified on podman-compose: wait-healthy.sh reports bff healthy (rc=0); podman
exposes .State.Health.Status (starting -> healthy) and the name filter matches
both `_` and `-` container naming.
Docs: gitea-actions-gotchas.md updated (the two `--wait` sections folded into one
"portable health poll" section).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5.6 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 cp— notdocker compose create, which podman-compose (the local dev runtime) does not implement. docker cprather 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 inlinecontent— 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-latestlabel.
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).
Readiness: a portable health poll, not docker compose up --wait
The smoke does not use docker compose up --wait, for three reasons:
- podman-compose doesn't implement
--wait(unrecognized arguments: --wait), so it would break local dev. - A whole-project
--waitfails when a one-shot with noservice_completed_successfullydependant exits —flowable-initdeploys the BPMN and exits 0, which--waittreats as the project failing (symptom: last compose linecontainer infra-flowable-init-1 exited (0)). - The containerized CI runner can't reach published host ports, so an
external
curl localhost:8080/healthdoesn't work either.
Fix. infra/wait-healthy.sh polls each durable, health-checked service
(openzaak nrc-web acl bff — WAIT_SVCS in the Makefile) with docker ps +
docker inspect '{{.State.Health.Status}}', waiting for healthy. That uses
only primitives both docker compose and podman-compose support, reads the
in-container healthcheck (no host port needed), and ignores the one-shots (they
only need to have run). WAIT_TIMEOUT (default 420 s) covers the cold
OpenZaak migrate (~90 s) plus app start.
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.