Was hiermit entsteht
Ein CI-Runner, der an Ihre eigene Forge angebunden ist, ein Repository auscheckt, ein Container-Image ohne Daemon und ohne Root erstellt und das Ergebnis in eine private Registry auf derselben Instanz pusht. Es wird kein privilegierter Socket gemountet, und nichts in der Pipeline läuft als UID Null.
Die meisten Self-hosted-CI-Setups lösen Image-Builds, indem sie den Host-Container-Daemon in den Job mounten. Das funktioniert, gibt aber jeder Pipeline – auch der, die jemand per Pull Request eröffnet hat – die volle Kontrolle über die Maschine. Buildah im Rootless-Modus erledigt dieselbe Arbeit ohne all das und ist auf dedizierten Kernen nicht langsamer.
Bevor Sie beginnen
- Eine R-8. Image-Builds sind das Single-Core-lastigste, was die meisten Teams laufen lassen, und Zen 5 ist der schnellste Kern, den wir verkaufen. Vierhundert Gigabyte NVMe fassen eine Menge Layer.
- Eine Forge, die Sie bereits betreiben und die das Actions-Protokoll spricht, sowie die Berechtigung, darin ein Runner-Registrierungstoken zu erstellen.
- Hostnamen:
ci.example.comfür den Runner undregistry.example.comfür die Registry.
1. Ein unprivilegierter Benutzer mit einem Namensraum-Bereich
apt update && apt install -y podman buildah skopeo fuse-overlayfs uidmap slirp4netns git nodejs nginx apache2-utils
useradd -m -s /bin/bash runner
echo "runner:200000:65536" >> /etc/subuid
echo "runner:200000:65536" >> /etc/subgid
loginctl enable-linger runnerDiese beiden Bereiche ermöglichen Rootless-Container: Das Konto runner besitzt fünfundsechzigtausend Subordinate-IDs, sodass ein Prozess, der in einem Container glaubt, Root zu sein, außerhalb auf eine unprivilegierte ID abgebildet wird. Lingering hält die Benutzersitzung am Leben, sodass systemd-Units unter diesem Konto das Abmelden überleben.
Node ist installiert, weil die meisten wiederverwendbaren Aktionen JavaScript sind und der Runner sie in dieser Konfiguration auf dem Host ausführt. Das beim Checkout-Schritt zu entdecken, ist ein üblicher zehnminütiger Umweg.
2. Rootless-Speicher auf der NVMe
sudo -u runner mkdir -p /home/runner/.config/containers
sudo -u runner tee /home/runner/.config/containers/storage.conf <<EOF
[storage]
driver = "overlay"
graphroot = "/home/runner/.local/share/containers/storage"
[storage.options.overlay]
mount_program = "/usr/bin/fuse-overlayfs"
EOF
sudo -u runner podman info --format "{{.Store.GraphDriverName}} {{.Host.Security.Rootless}}"Der letzte Befehl sollte overlay true ausgeben. Ein vfs-Treiber stattdessen bedeutet, dass fuse-overlayfs fehlt, und vfs kopiert bei jedem Build jeden Layer vollständig, was aus einer neunzigsekündigen Pipeline eine sechsminütige macht.
3. Eine private Registry
apt install -y docker-registry
htpasswd -c /etc/docker/registry/htpasswd ci
certbot certonly --standalone -d registry.example.comBinden Sie die Registry an Loopback und lassen Sie nginx alles nach außen Gerichtete besitzen, einschließlich Authentifizierung. In /etc/docker/registry/config.yml:
version: 0.1
storage:
filesystem:
rootdirectory: /srv/registry
delete:
enabled: true
http:
addr: 127.0.0.1:5000Die Registry selbst führt keine Anmeldeinformationen, weil sie nie eine Anfrage erhält, die nicht bereits durch den Proxy gegangen ist. Ein Ort, um ein Passwort zu prüfen, ist besser als zwei Orte, die sich widersprechen können.
server {
listen 443 ssl;
server_name registry.example.com;
ssl_certificate /etc/letsencrypt/live/registry.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/registry.example.com/privkey.pem;
client_max_body_size 0;
chunked_transfer_encoding on;
location /v2/ {
auth_basic "restricted";
auth_basic_user_file /etc/docker/registry/htpasswd;
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 900s;
}
}client_max_body_size 0 entfernt das Upload-Limit. Lassen Sie die nginx-Standardeinstellung bestehen, und jeder Layer über einem Megabyte schlägt mit einem 413 etwa zwei Drittel durch einen Push fehl – das ist ein denkwürdiger Nachmittag.
systemctl enable --now docker-registry nginx4. Der Runner
cd /usr/local/bin
wget -O act_runner https://code.forgejo.org/forgejo/runner/releases/download/v6.3.1/forgejo-runner-6.3.1-linux-amd64
chmod +x act_runner
sudo -u runner mkdir -p /home/runner/.runner-cfg
cd /home/runner/.runner-cfg && sudo -u runner /usr/local/bin/act_runner generate-config > config.yamlBearbeiten Sie die generierte Datei, sodass Jobs auf dem Host statt in einem Container laufen, weil Buildah bereits die Isolation bietet und Verschachtelung beiden nur Komplexität hinzufügt:
runner:
capacity: 2
timeout: 1h
labels:
- "debian-13:host"
host:
workdir_parent: /home/runner/work
cache:
enabled: true
dir: /home/runner/cacheKapazität zwei auf acht Kernen ist beabsichtigt: Builds sind weitgehend seriell, und zwei gleichzeitige Jobs mit je vier Kernen werden schneller fertig als vier Jobs, die um denselben Cache kämpfen. Registrieren Sie sich bei Ihrer Forge mit dem generierten Token:
cd /home/runner/.runner-cfg
sudo -u runner /usr/local/bin/act_runner register --no-interactive \
--instance https://forge.example.com --token <registration token> \
--name ci-ams --labels debian-13:hostDann eine Unit, die als unprivilegiertes Konto läuft:
[Unit]
Description=Actions runner
After=network-online.target
[Service]
User=runner
WorkingDirectory=/home/runner/.runner-cfg
ExecStart=/usr/local/bin/act_runner daemon --config /home/runner/.runner-cfg/config.yaml
Restart=always
Environment=HOME=/home/runner
Environment=XDG_RUNTIME_DIR=/run/user/3001
NoNewPrivileges=yes
[Install]
WantedBy=multi-user.targetErsetzen Sie die echte UID des Kontos runner in XDG_RUNTIME_DIR; id -u runner gibt sie aus. Rootless Podman benötigt dieses Verzeichnis, was die Linger-Einstellung aus Schritt eins garantiert.
systemctl daemon-reload && systemctl enable --now act-runner5. Ein Workflow, der baut und pusht
Im Repository, unter .forgejo/workflows/image.yaml:
on:
push:
branches: [main]
jobs:
image:
runs-on: debian-13
steps:
- uses: actions/checkout@v4
- name: Build
run: |
buildah bud --layers --format oci -t app:${{ github.sha }} .
- name: Push
run: |
buildah login -u ci -p ${{ secrets.REGISTRY_PASSWORD }} registry.example.com
buildah push app:${{ github.sha }} docker://registry.example.com/app:${{ github.sha }}
buildah push app:${{ github.sha }} docker://registry.example.com/app:latest--layers aktiviert Layer-Caching – das ist der Unterschied zwischen dem Neuaufbau Ihrer Abhängigkeiten bei jedem Commit und dem Neuaufbau, wenn sie sich ändern.
Verifizieren
Der Runner sollte innerhalb von Sekunden nach dem Start der Unit in der Runner-Liste der Forge als online erscheinen. Dann pushen Sie einen Commit und beobachten den Job von der Maschine aus:
journalctl -fu act-runnerWenn er fertig ist, bestätigen Sie, dass das Image tatsächlich angekommen ist, statt nur Erfolg zu melden:
skopeo inspect --creds ci:<password> docker://registry.example.com/app:latest | head -20
skopeo list-tags --creds ci:<password> docker://registry.example.com/appSie möchten den Digest, die Layer-Liste und beide Tags. Dann beweisen Sie, dass es läuft, auf einer anderen Maschine, falls Sie eine zur Hand haben:
podman run --rm registry.example.com/app:latest --versionZuletzt die Behauptung, auf der dieser gesamte Build beruht. Während ein Job läuft, sehen Sie nach, wem die Prozesse gehören:
ps -eo user,pid,comm | grep -E "buildah|podman" | head
sudo -u runner podman info --format "{{.Host.Security.Rootless}}"Jeder Prozess gehört runner, und der Sicherheitscheck antwortet true. Nichts in der Pipeline hält Root, was bedeutet, dass ein kompromittiertes Build-Skript ein unprivilegiertes Konto und einen Namensraum bekommt, nicht Ihre Registry-Schlüssel und Ihren Hypervisor.
Danach
Das Registry-Wachstum ist unbegrenzt, wenn nichts alte Tags entfernt. Führen Sie daher registry garbage-collect wöchentlich per Timer aus, sobald Sie eine Aufbewahrungsregel haben, an die Sie glauben. Wenn Builds statt der Tests der Engpass werden, zeigt die Vergleichsseite, wie die nächste Größe aussieht; mehr Kerne helfen weit weniger als die meisten erwarten, und schnellere helfen weit mehr.