Lo que esto construye
Un runner de CI conectado a tu propio forge, que descarga un repositorio, construye una imagen de contenedor sin daemon y sin root, y sube el resultado a un registro privado que corre en la misma instancia. No se monta ningún socket privilegiado en ningún sitio, y nada en el pipeline se ejecuta como uid zero.
La mayoría de las configuraciones de CI autoalojadas resuelven la construcción de imágenes montando el daemon de contenedores del host en el trabajo. Eso funciona y le da a cada pipeline, incluyendo el que alguien abrió con un pull request, control completo de la máquina. Buildah en modo rootless hace el mismo trabajo sin nada de eso, y en núcleos dedicados no es más lento.
Antes de empezar
- Un R-8. La construcción de imágenes es lo más ligado a un solo núcleo que la mayoría de los equipos ejecutan, y Zen 5 es el más rápido por núcleo que vendemos. Cuatrocientos gigabytes de NVMe alberga muchísimas capas.
- Un forge que ya ejecutes y que hable el protocolo Actions, y permiso para crear un token de registro de runner en él.
- Nombres de host:
ci.example.compara el runner yregistry.example.compara el registro.
1. Un usuario sin privilegios con un rango de namespaces
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 runnerEsos dos rangos son lo que hace posibles los contenedores rootless: la cuenta runner posee sesenta y cinco mil ids subordinados, así que un proceso que cree que es root dentro de un contenedor se mapea a un id sin privilegios fuera de él. Lingering mantiene viva la sesión del usuario para que las unidades de systemd bajo esa cuenta sobrevivan al cierre de sesión.
Node está instalado porque la mayoría de las acciones reutilizables son JavaScript y el runner las ejecuta en el host en esta configuración. Descubrir eso en el paso de checkout es un desvío común de diez minutos.
2. Almacenamiento rootless en el 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}}"Ese último comando debería responder overlay true. Un driver vfs en su lugar significa que falta fuse-overlayfs, y vfs copia cada capa completa en cada construcción, lo que convierte un pipeline de noventa segundos en uno de seis minutos.
3. Un registro privado
apt install -y docker-registry
htpasswd -c /etc/docker/registry/htpasswd ci
certbot certonly --standalone -d registry.example.comVincula el registro a loopback y deja que nginx sea dueño de todo lo que mira hacia fuera, incluida la autenticación. En /etc/docker/registry/config.yml:
version: 0.1
storage:
filesystem:
rootdirectory: /srv/registry
delete:
enabled: true
http:
addr: 127.0.0.1:5000El registro en sí no lleva credenciales porque nunca recibe una solicitud que no haya pasado ya por el proxy. Un solo lugar para comprobar una contraseña es mejor que dos lugares que pueden no estar de acuerdo.
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 elimina el límite de subida. Deja el valor por defecto de nginx y cada capa de más de un megabyte fallará con un 413 a unos dos tercios del camino de un push, lo cual es una tarde memorable.
systemctl enable --now docker-registry nginx4. El 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.yamlEdita el archivo generado para que los trabajos se ejecuten en el host en lugar de dentro de un contenedor, porque Buildah ya proporciona el aislamiento y anidar los dos no añade nada más que complejidad:
runner:
capacity: 2
timeout: 1h
labels:
- "debian-13:host"
host:
workdir_parent: /home/runner/work
cache:
enabled: true
dir: /home/runner/cacheCapacidad dos en ocho núcleos es deliberado: las construcciones son en gran parte seriales, y dos trabajos concurrentes con cuatro núcleos cada uno terminan antes que cuatro trabajos peleando por la misma caché. Regístrate contra tu forge con el token que generó:
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:hostLuego, una unidad, ejecutándose como la cuenta sin privilegios:
[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.targetSustituye el uid real de la cuenta runner en XDG_RUNTIME_DIR; id -u runner lo imprime. Podman rootless necesita que ese directorio exista, que es lo que garantiza el ajuste de linger del paso uno.
systemctl daemon-reload && systemctl enable --now act-runner5. Un workflow que construye y sube
En el repositorio, en .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 activa el caché de capas, que es la diferencia entre reconstruir tus dependencias en cada commit y reconstruirlas cuando cambian.
Verifícalo
El runner debería aparecer como online en la lista de runners del forge a los pocos segundos de que la unidad arranque. Luego haz push de un commit y observa el trabajo desde la máquina:
journalctl -fu act-runnerCuando termine, confirma que la imagen realmente llegó en lugar de solo informar éxito:
skopeo inspect --creds ci:<password> docker://registry.example.com/app:latest | head -20
skopeo list-tags --creds ci:<password> docker://registry.example.com/appQuieres el digest, la lista de capas y ambas etiquetas. Ahora demuestra que se ejecuta, en otra máquina si tienes una a mano:
podman run --rm registry.example.com/app:latest --versionPor último, la afirmación sobre la que descansa toda esta construcción. Mientras un trabajo se está ejecutando, mira a quién pertenecen los procesos:
ps -eo user,pid,comm | grep -E "buildah|podman" | head
sudo -u runner podman info --format "{{.Host.Security.Rootless}}"Todo proceso pertenece a runner, y la comprobación de seguridad responde true. Nada en el pipeline tiene root, lo que significa que un script de construcción comprometido obtiene una cuenta sin privilegios y un namespace, no tus claves de registro y tu hipervisor.
Después
El almacenamiento del registro crece sin límite a menos que algo elimine las etiquetas antiguas, así que ejecuta registry garbage-collect en un temporizador semanal una vez que tengas una regla de retención en la que creas. Si las construcciones se convierten en el cuello de botella en lugar de los tests, la página de comparación muestra cómo sería el siguiente tamaño; más núcleos ayudan mucho menos de lo que la mayoría espera, y los más rápidos ayudan mucho más.