halkyon/deploy/runner/forgejo-runner.service
Tizian 「ティツィアーン」 93b0c0a6b0
chore: the servers are named by the domain they serve
Files called the servers by their own host names, which are the
operator's and say nothing a reader needs. They now say which domain a
server serves: learn.halkyon.eu's server (the instance and its CI
runner), the halkyon.app server (Forgejo and the F-Droid repository).

Two names are settings rather than words. CI's runner label becomes
`instance` (`runs-on: instance`, `labels: ["instance:host"]`): the
runner reads its installed copy of deploy/runner/config.yml, so jobs
wait in the queue until that copy is updated and the runner restarted.
The SSH aliases the scripts and docs use become `learn-deploy` and
`fdroid-upload`. AGENTS.md says to keep it so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 00:33:50 +02:00

82 lines
3.3 KiB
Desktop File

# The Forgejo Actions runner of this repository's CI, on learn.halkyon.eu's server. Installed as
# /etc/systemd/system/forgejo-runner.service; its configuration is config.yml, next to
# this file.
#
# Jobs run directly on the host as the unprivileged user `runner` ("without any
# isolation" from that user, as Forgejo's security guide puts it), and they run our
# dependencies' build code. On the production server, so everything below keeps them
# away from production and makes sure CI can't starve the instance. Docker is out of
# reach entirely: `runner` isn't in the docker group, and the socket is hidden too.
#
# `runner` has user and group ID 61000, and must never share a user ID with a
# container's processes (PostgreSQL and the server run as 999; Docker doesn't remap
# them): processes of one user ID can read each other's environment and, through
# /proc, their files. `useradd --system` picks 999 when it is free, so create it with
# useradd --system --uid 61000 --user-group --create-home \
# --home-dir /var/lib/forgejo-runner --shell /usr/sbin/nologin runner
# CI's first step checks this sandbox and fails if it leaks.
[Unit]
Description=Forgejo Actions runner (CI of halkyon_learn; jobs on the host)
Documentation=https://forgejo.org/docs/latest/admin/actions/security/
After=network-online.target
Wants=network-online.target
[Service]
User=runner
Group=runner
WorkingDirectory=/var/lib/forgejo-runner
ExecStart=/usr/local/bin/forgejo-runner daemon --config /etc/forgejo-runner/config.yml
# The runner's token, from a root-only file.
LoadCredential=token:/etc/forgejo-runner/token
Restart=on-failure
RestartSec=30
# The runner waits up to 10 minutes for a running job (shutdown_timeout).
TimeoutStopSec=11min
# Away from production: /home (deploy's .env, the checkout, the database dumps) is
# invisible, the rest of the system read-only except the runner's own directory, other
# users' processes hidden, and Docker's container networks, where the database listens,
# unreachable.
ProtectHome=yes
ProtectSystem=strict
ReadWritePaths=/var/lib/forgejo-runner
PrivateTmp=yes
ProtectProc=invisible
InaccessiblePaths=-/run/docker.sock -/run/containerd -/var/lib/docker
IPAddressDeny=172.16.0.0/12 192.168.0.0/16
NoNewPrivileges=yes
CapabilityBoundingSet=
AmbientCapabilities=
SystemCallArchitectures=native
RestrictSUIDSGID=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
LockPersonality=yes
RestrictRealtime=yes
RestrictNamespaces=yes
RemoveIPC=yes
# Netlink stays: Dart lists network interfaces through it.
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
# System calls no CI job needs fail with an error rather than killing the job.
SystemCallFilter=~@clock @cpu-emulation @debug @module @mount @obsolete @raw-io @reboot @swap
SystemCallErrorNumber=EPERM
UMask=0077
# Never at the instance's expense: two of the four cores, at most 5 GB of the 8 (the
# kernel reclaims from CI first above 4 GB), and the lowest priority for CPU and disk.
# A cold `dart analyze` of the workspace with its lint plugin needs about 3.7 GB; with
# 3 GB the kernel killed it, and OOMPolicy's default then stopped the whole unit.
CPUQuota=200%
MemoryHigh=4G
MemoryMax=5G
TasksMax=2048
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7
[Install]
WantedBy=multi-user.target