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 (<TOKEN_FROM_GITEA_UI>,
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.
This commit is contained in:
262
docs/podman-runner-setup.md
Normal file
262
docs/podman-runner-setup.md
Normal file
@@ -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/<name>.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 <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: <https://gitea.com/gitea/act_runner/releases>.
|
||||||
|
|
||||||
|
## Step 1 — Switch to the runner user
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo machinectl shell <user>@ # or: sudo -iu <user>
|
||||||
|
id # capture $UID for later substitution
|
||||||
|
podman info --format '{{.Host.Security.Rootless}}' # must print "true"
|
||||||
|
```
|
||||||
|
|
||||||
|
If `loginctl show-user <user> | grep Linger` reports `Linger=no`, run as
|
||||||
|
root **before** going further:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo loginctl enable-linger <user>
|
||||||
|
```
|
||||||
|
|
||||||
|
Without linger the Podman user-mode socket dies when `<user>` 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=<TOKEN_FROM_GITEA_UI> \
|
||||||
|
-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
|
||||||
|
`<repo>/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+)".
|
||||||
@@ -139,6 +139,30 @@ None blocking, all deferred to sprint 1+.
|
|||||||
wires `c2_credential.config_fernet` (D-004). `config.py:32` accepts an empty
|
wires `c2_credential.config_fernet` (D-004). `config.py:32` accepts an empty
|
||||||
default at boot but `Fernet(b"")` raises `ValueError` at first use.
|
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
|
## Conventions
|
||||||
|
|
||||||
- Branches: `feature/<scope>`, `fix/<scope>`, `docs/<scope>`, `chore/<scope>`. Long-lived: `main`.
|
- Branches: `feature/<scope>`, `fix/<scope>`, `docs/<scope>`, `chore/<scope>`. Long-lived: `main`.
|
||||||
|
|||||||
Reference in New Issue
Block a user