From c44f8b90ad6f75b01be60976076d6f9b6a4871f9 Mon Sep 17 00:00:00 2001 From: knacky Date: Sat, 23 May 2026 03:08:03 +0200 Subject: [PATCH] docs: archive Podman runner setup runbook + track F-D1..F-D5 Two changes scoped together since both stem from the post-PR2 wrap-up. docs/podman-runner-setup.md (new, ~190 LOC): Operational runbook for the gitea-runner host that drives CI. The first attempt at install hit four traps that this archived version documents so we don't lose the lesson: 1. `act_runner register` performs a sanity ping against the container daemon before writing the credential. Without the Podman socket mounted on the *register one-shot*, register fails silently and no .runner file is produced. The runbook mounts the socket on both register and daemon containers. 2. SELinux blocks rootless socket access by default. Quadlet SecurityLabelDisable=true (or --security-opt label=disable for the legacy CLI form) is the documented bypass. No-op on Debian, required on RHEL/Fedora hosts. 3. The runner user UID is not 1000 on every host (gitea = 1005 here). Quadlet `%U` substitution makes the unit portable; hardcoded UIDs are explicitly called out as a sprint 0 mistake. 4. `podman generate systemd` is officially deprecated. Quadlet is the only supported pattern going forward and is what this runbook ships; legacy alternative is omitted on purpose. Also captures: token placeholder convention (, never the real value in archived docs), single-use semantics, the "secrets via file, not chat" convention, the `:X.Y.Z` pin policy versus `:latest` in prod (ties into follow-up F-D1), and a decommissioning section that cleans up state without nuking the user-level Podman socket. tasks/todo.md: New section "Frontend follow-ups (sprint 1+)" with F-D1..F-D5 from code-reviewer on `chore/frontend-dockerfile` (649194b). All deferred, none blocking. F-D1 (digest pinning) is project-wide and explicitly references the backend image and the runner image alongside the frontend ones for a single chore commit. --- docs/podman-runner-setup.md | 262 ++++++++++++++++++++++++++++++++++++ tasks/todo.md | 24 ++++ 2 files changed, 286 insertions(+) create mode 100644 docs/podman-runner-setup.md diff --git a/docs/podman-runner-setup.md b/docs/podman-runner-setup.md new file mode 100644 index 0000000..6324c72 --- /dev/null +++ b/docs/podman-runner-setup.md @@ -0,0 +1,262 @@ +# Gitea Actions runner — Podman rootless runbook + +Archived setup procedure for the `gitea-runner` host that drives Mimic CI +(`.gitea/workflows/ci.yml`). Captures the corrections that emerged during +sprint 0 install so future operators don't re-discover the same traps. + +## Target architecture + +- **Host** : same VM as the Gitea server (sprint 0 deployment choice). +- **Container runtime** : Podman rootless under the existing `gitea` system + user. No new account, no rootful daemon. +- **Runner image** : `docker.io/gitea/act_runner:X.Y.Z` (pinned, see [Pin + policy](#pin-policy)). +- **Auto-start** : Quadlet (`~/.config/containers/systemd/.container`) + — the upstream-recommended pattern since Podman 4.4. `podman generate + systemd` is officially deprecated; do not introduce it. +- **Label exposed to workflows** : `linux` (single, kept short, matches the + `runs-on: linux` line in `.gitea/workflows/ci.yml`). + +## Prerequisites on the host + +| Component | Requirement | Verify | +| --- | --- | --- | +| Podman | ≥ 4.4 (Quadlet support) | `podman --version` | +| Rootless mode | enabled | `podman info --format '{{.Host.Security.Rootless}}'` → `true` | +| systemd user mode | linger on for the runner user | `loginctl show-user \| grep Linger` | +| `podman.socket` user unit | available | `ls /usr/lib/systemd/user/podman.socket` | +| Gitea Actions | enabled in `app.ini` | `[actions] ENABLED = true` then restart | + +If Gitea Actions was never activated, edit `/etc/gitea/app.ini`: + +```ini +[actions] +ENABLED = true + +[actions.log_compression] +ENABLED = true +``` + +Restart with `sudo systemctl restart gitea`. The UI exposes +`Site Administration → Actions → Runners` once enabled. + +## Pin policy + +**Never use `:latest` for the runner image in production.** Pin a concrete +`gitea/act_runner:X.Y.Z` tag and bump explicitly through this runbook. The +same policy is tracked for every other production image in +[`tasks/todo.md`](../tasks/todo.md) follow-up **F-D1** (digest pinning +roadmap). + +To find the current release: . + +## Step 1 — Switch to the runner user + +```bash +sudo machinectl shell @ # or: sudo -iu +id # capture $UID for later substitution +podman info --format '{{.Host.Security.Rootless}}' # must print "true" +``` + +If `loginctl show-user | grep Linger` reports `Linger=no`, run as +root **before** going further: + +```bash +sudo loginctl enable-linger +``` + +Without linger the Podman user-mode socket dies when `` logs out and +the runner stops accepting jobs. + +## Step 2 — Activate the Podman socket + +```bash +systemctl --user enable --now podman.socket +systemctl --user status podman.socket +ls -la /run/user/$(id -u)/podman/podman.sock # exists, mode 0660 +``` + +## Step 3 — Pull the runner image + +```bash +podman pull docker.io/gitea/act_runner:X.Y.Z # replace X.Y.Z +``` + +## Step 4 — Generate a baseline config + +```bash +mkdir -p ~/.config/act_runner ~/.local/share/act_runner +cd ~/.config/act_runner +podman run --rm docker.io/gitea/act_runner:X.Y.Z \ + act_runner generate-config > config.yaml +``` + +Edit `~/.config/act_runner/config.yaml` — only these keys matter: + +```yaml +runner: + capacity: 2 + envs: + DOCKER_HOST: "unix:///var/run/docker.sock" # path as seen by the container + labels: + - "linux:docker://node:22-alpine" + +container: + network: "bridge" + privileged: false + docker_host: "unix:///var/run/docker.sock" + options: "--security-opt label=disable" # see SELinux note below +``` + +## Step 5 — Register the runner (single-use token) + +> **Gotcha — register pings the container daemon.** +> Even though `act_runner register` writes no actual job, it sanity-checks +> at startup that it can reach a container runtime. Without the socket +> mounted, register fails with `cannot ping container daemon` and the +> credential file `~/.local/share/act_runner/.runner` is never written. +> Mount the socket on the register one-shot too — not only on the daemon. + +Generate a registration token in `Site Administration → Actions → Runners +→ Create new Runner`, then run **once**: + +```bash +podman run --rm \ + --security-opt label=disable \ + -v ~/.config/act_runner/config.yaml:/config.yaml \ + -v ~/.local/share/act_runner:/data \ + -v /run/user/$(id -u)/podman/podman.sock:/var/run/docker.sock \ + -w /data \ + -e GITEA_INSTANCE_URL=https://repo.try2get.in \ + -e GITEA_RUNNER_REGISTRATION_TOKEN= \ + -e GITEA_RUNNER_NAME=gitea-runner \ + -e GITEA_RUNNER_LABELS=linux \ + docker.io/gitea/act_runner:X.Y.Z \ + act_runner register --no-interactive +``` + +The token is **single-use**: invalidated the moment `register` succeeds. +Generate a fresh one for each re-registration. + +> **Secret handling convention.** +> Do not paste the registration token into chat, agent transcripts, or +> issue trackers. Drop it on disk (e.g. `~/runner-token.txt`), `cat` it +> into the environment for the one-shot above, then `shred -u` the file. +> This mirrors the team-lead "secrets via file, not chat" rule. + +Verify the runner appears in the Gitea UI at +`https://repo.try2get.in/-/admin/actions/runners` with status `idle`. + +## Step 6 — Quadlet unit (auto-start) + +`~/.config/containers/systemd/gitea-runner.container` : + +```ini +[Unit] +Description=Gitea Actions Runner (Mimic) — Podman rootless +After=podman.socket +Requires=podman.socket + +[Container] +Image=docker.io/gitea/act_runner:X.Y.Z +ContainerName=gitea-runner +SecurityLabelDisable=true +Volume=%h/.config/act_runner/config.yaml:/config.yaml,ro +Volume=%h/.local/share/act_runner:/data +Volume=/run/user/%U/podman/podman.sock:/var/run/docker.sock +WorkingDir=/data +Exec=act_runner daemon --config /config.yaml + +[Service] +Restart=on-failure +RestartSec=5 +TimeoutStartSec=900 + +[Install] +WantedBy=default.target +``` + +Notes: + +- `%h` expands to the runner user's `$HOME`, `%U` to the runner user's UID. + Hardcoding `1000` (or any specific UID) was a sprint 0 mistake — the + actual `gitea` UID on this host is **1005**. Quadlet substitution makes + the unit portable across hosts. +- `SecurityLabelDisable=true` is the Quadlet equivalent of + `--security-opt label=disable`. It bypasses SELinux container labelling + so the rootless container can `read+write` the host Podman socket. On + SELinux-disabled systems (Debian/Ubuntu vanilla) this is a no-op; on + RHEL/Fedora-like it is required — without it nginx-style "Permission + denied" appears on socket connect. + +Activate: + +```bash +systemctl --user daemon-reload +systemctl --user start gitea-runner.service # generated from .container +systemctl --user enable gitea-runner.service # persist across reboots +journalctl --user -u gitea-runner.service -e +``` + +Quadlet generates `gitea-runner.service` automatically; do not create it by +hand under `~/.config/systemd/user/`. + +## Step 7 — Smoke validation + +Push a transient workflow on a feature branch. Example used during sprint 0 +(file lived at `.gitea/workflows/smoke.yml` on `chore/podman-and-ci`, +removed after green): + +```yaml +name: smoke +on: + push: + branches: [chore/podman-and-ci] + workflow_dispatch: + +jobs: + hello: + runs-on: linux + steps: + - run: | + echo "host: $(uname -a)" + id + head -3 /etc/os-release +``` + +Job picked up and green within ~10 s on the Gitea Actions tab → runner is +operational. Failures usually trace back to one of the gotchas captured +above (`journalctl --user -u gitea-runner.service -e` is authoritative). + +## Step 8 — Repo secrets + +CI consumes the following secrets, configured per repo at +`/settings/actions/secrets`: + +| Secret | Use | Value | +| --- | --- | --- | +| `FERNET_KEY_TEST` | `MIMIC_FERNET_KEY` in CI jobs | `python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"` once, fixed thereafter | + +Never reuse production Fernet material in CI. + +## Decommissioning + +```bash +# As the runner user: +systemctl --user disable --now gitea-runner.service +rm ~/.config/containers/systemd/gitea-runner.container +systemctl --user daemon-reload +rm -rf ~/.config/act_runner ~/.local/share/act_runner +# Drop the runner entry in Gitea UI: Site Admin → Actions → Runners → Delete. +``` + +The Podman socket and linger setting stay — they are user-level and shared +with anything else the user runs. + +## Cross-references + +- Sprint 0 decisions: [`tasks/spec-decisions.md`](../tasks/spec-decisions.md) + (D-007 reverse proxy scope, D-010 Ansible playbook scope). +- CI workflow: [`.gitea/workflows/ci.yml`](../.gitea/workflows/ci.yml). +- Deferred CI work: [`tasks/todo.md`](../tasks/todo.md) section "CI + follow-ups (sprint 1+)". diff --git a/tasks/todo.md b/tasks/todo.md index 0d44247..57215f2 100644 --- a/tasks/todo.md +++ b/tasks/todo.md @@ -139,6 +139,30 @@ None blocking, all deferred to sprint 1+. wires `c2_credential.config_fernet` (D-004). `config.py:32` accepts an empty default at boot but `Fernet(b"")` raises `ValueError` at first use. +## Frontend follow-ups (sprint 1+) (`devops`) + +Raised by `code-reviewer` during review of `chore/frontend-dockerfile` +(`649194b`). None blocking, all deferred to sprint 1+. Some have a project- +wide reach (F-D1 also covers the backend image and the runner image). + +- [ ] F-D1 — Pin container images by minor + digest, not by tag alone. Scope : + `frontend/Dockerfile` (node:22-alpine, nginxinc/nginx-unprivileged:alpine), + `backend/Dockerfile` (python:3.12-slim-bookworm), and the runner image + referenced in `docs/podman-runner-setup.md` (gitea/act_runner:X.Y.Z). + Harmonise the policy in a single chore commit. +- [ ] F-D2 — Decide image-level `HEALTHCHECK` directives vs delegating health + probing to Caddy upstream. Document the choice in `docs/deploy.md`. +- [ ] F-D3 — Security response headers (`X-Content-Type-Options: nosniff`, + `Referrer-Policy: strict-origin-when-cross-origin`, `Content-Security-Policy`). + Arbitrate ownership between `frontend/nginx.conf` and the Caddy outer + layer to avoid duplication / conflict. +- [ ] F-D4 — Enable response compression. Either `gzip on` + `gzip_types` in + `frontend/nginx.conf` (runtime), or `vite-plugin-compression` (precompute + .br / .gz at build time, served via `gzip_static`). Pick one. +- [ ] F-D5 — OCI image labels (`org.opencontainers.image.source`, + `image.title`, `image.licenses`, `image.revision`) on every Dockerfile. + Useful for registry metadata and supply-chain attestation tooling. + ## Conventions - Branches: `feature/`, `fix/`, `docs/`, `chore/`. Long-lived: `main`.