diff --git a/container/README.md b/container/README.md
index 1fd15f16..8937d7a9 100644
--- a/container/README.md
+++ b/container/README.md
@@ -54,25 +54,61 @@ The `reuse` job stays on the upstream `fsfe/reuse-action` (a Docker action the
Docker executor launches as a sibling container) — nothing REUSE-related is
baked into the SDK.
-## CI runner (VM)
+## CI runner (Alpine / OpenRC)
-The runner runs in a small VM using the **Docker executor** (not the host
-executor). This gives per-job container isolation and standard `ubuntu-latest`
-semantics, and caps CI resources at the VM boundary.
+The runner uses the **Docker executor** (not the host executor): per-job
+container isolation and standard `ubuntu-latest` semantics. `act_runner` runs
+as an OpenRC service. Files provided:
-1. **Size the VM to your thread budget.** 2 vCPUs = a hard 2-thread ceiling.
- Pin them to specific host threads so CI never touches the rest (libvirt/KVM):
+| File | Purpose |
+|------|---------|
+| `act_runner.openrc` | OpenRC init script (`supervise-daemon`, depends on docker). |
+| `act_runner.confd.example` | Per-instance `conf.d` settings for multi-runner hosts. |
- ```xml
- 2
-
-
-
-
- ```
+**Cap the thread budget.** In a VM, pin its vCPUs to specific host threads
+(libvirt/KVM):
-2. **Inside the VM:** install Docker (moby) and `act_runner`.
-3. **Register one runner per project** (separate tokens from each repo's
- *Settings → Actions → Runners*; scope keeps their jobs apart).
-4. **Configure** with `runner-config.example.yaml` — `capacity: 1` and
- `container.options: "--cpus=2"` keep usage inside the 2-thread budget.
+```xml
+2
+
+
+
+
+```
+
+On bare metal, the `container.options: "--cpus=2"` and `capacity: 1` in
+`runner-config.example.yaml` already bound each runner.
+
+**Set it up:**
+
+```bash
+# 1. Docker + a dedicated user with socket access
+apk add docker docker-cli
+rc-update add docker default && rc-service docker start
+adduser -S -D -H -h /var/lib/act_runner act
+addgroup act docker
+
+# 2. act_runner binary (static Go build, works on musl)
+curl -fsSL -o /usr/local/bin/act_runner \
+ https://gitea.com/gitea/act_runner/releases/download/v0.2.11/act_runner-0.2.11-linux-amd64
+chmod +x /usr/local/bin/act_runner
+
+# 3. Config + register one runner per project (scope keeps their jobs apart)
+install -Dm644 container/runner-config.example.yaml /etc/act_runner/trx-rs.yaml
+install -d -o act /var/lib/act_runner/trx-rs
+su act -s /bin/sh -c 'cd /var/lib/act_runner/trx-rs && \
+ act_runner register --no-interactive \
+ --instance https://git.haxx.space --token \
+ --name trx-rs-ci \
+ --labels "ubuntu-latest:docker://catthehacker/ubuntu:act-latest"'
+
+# 4. OpenRC service (repeat the symlink+conf.d for the second project)
+install -m755 container/act_runner.openrc /etc/init.d/act_runner
+ln -s act_runner /etc/init.d/act_runner.trx-rs
+install -m644 container/act_runner.confd.example /etc/conf.d/act_runner.trx-rs
+rc-update add act_runner.trx-rs default
+rc-service act_runner.trx-rs start
+```
+
+Check it with `rc-service act_runner.trx-rs status` and
+`tail -f /var/log/act_runner.trx-rs.log`.
diff --git a/container/act_runner.confd.example b/container/act_runner.confd.example
new file mode 100644
index 00000000..b3ee409e
--- /dev/null
+++ b/container/act_runner.confd.example
@@ -0,0 +1,14 @@
+# SPDX-FileCopyrightText: 2026 Stan Grams
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# Per-instance settings for an act_runner OpenRC service.
+# Copy to /etc/conf.d/, e.g. /etc/conf.d/act_runner.trx-rs
+# (the name must match the /etc/init.d/ symlink).
+
+# User that runs the daemon. Must be a member of the `docker` group.
+runner_user="act"
+
+# Per-instance state dir (holds the .runner registration) and config file,
+# so two runners on one host stay independent.
+runner_dir="/var/lib/act_runner/trx-rs"
+runner_config="/etc/act_runner/trx-rs.yaml"
diff --git a/container/act_runner.openrc b/container/act_runner.openrc
new file mode 100644
index 00000000..5c87aff3
--- /dev/null
+++ b/container/act_runner.openrc
@@ -0,0 +1,44 @@
+#!/sbin/openrc-run
+# SPDX-FileCopyrightText: 2026 Stan Grams
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# OpenRC service for a Gitea act_runner (Docker executor) on Alpine.
+#
+# Install as /etc/init.d/act_runner (chmod +x). Single instance uses
+# /etc/act_runner/config.yaml. For one runner per project, symlink this script
+# and add a matching conf.d file:
+#
+# ln -s act_runner /etc/init.d/act_runner.trx-rs
+# cp container/act_runner.confd.example /etc/conf.d/act_runner.trx-rs
+# $EDITOR /etc/conf.d/act_runner.trx-rs # set runner_dir / runner_config
+# rc-update add act_runner.trx-rs default
+# rc-service act_runner.trx-rs start
+
+description="Gitea Actions runner"
+
+: "${runner_user:=act}"
+: "${runner_dir:=/var/lib/act_runner}"
+: "${runner_config:=/etc/act_runner/config.yaml}"
+
+command="/usr/local/bin/act_runner"
+command_args="daemon --config ${runner_config}"
+# No group given, so supplementary groups (incl. docker) are initialised.
+command_user="${runner_user}"
+directory="${runner_dir}"
+
+supervisor="supervise-daemon"
+respawn_delay=5
+respawn_max=0
+pidfile="/run/${RC_SVCNAME}.pid"
+output_log="/var/log/${RC_SVCNAME}.log"
+error_log="/var/log/${RC_SVCNAME}.log"
+
+depend() {
+ need docker
+ use net dns
+}
+
+start_pre() {
+ checkpath -d -m 0750 -o "${runner_user}" "${runner_dir}"
+ checkpath -f -m 0640 -o "${runner_user}" "${output_log}"
+}