# 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: 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 cp`s 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` — **not** `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 -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 ` 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.