만들 결과물
자체 포지에 연결된 CI 러너로, 저장소를 체크아웃하고, 데몬이나 루트 없이 컨테이너 이미지를 빌드하고, 결과물을 같은 인스턴스에서 실행되는 프라이빗 레지스트리에 푸시합니다. 특권 소켓은 어디에도 마운트되지 않으며, 파이프라인에서 uid 0으로 실행되는 것은 없습니다.
대부분의 셀프 호스팅 CI 설정은 호스트 컨테이너 데몬을 작업에 마운트하여 이미지 빌드를 해결합니다. 그렇게 하면 작동하고 모든 파이프라인(누군가 풀 리퀘스트를 연 것 포함)에 머신의 완전한 제어권을 부여합니다. 루트리스 모드의 Buildah는 그런 것 없이 동일한 작업을 수행하며, 전용 코어에서는 더 느리지 않습니다.
시작하기 전에
- R-8. 이미지 빌드는 대부분의 팀이 실행하는 가장 단일 코어 바운드 작업이며, Zen 5는 우리가 판매하는 코어당 가장 빠른 제품입니다. 400GB NVMe는 많은 레이어를 저장할 수 있습니다.
- 이미 실행 중이며 Actions 프로토콜을 지원하는 포지, 그리고 그곳에서 러너 등록 토큰을 생성할 권한.
- 호스트 이름: 러너용
ci.example.com, 레지스트리용registry.example.com.
1. 네임스페이스 범위를 가진 권한 없는 사용자
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 runner이 두 범위가 루트리스 컨테이너를 가능하게 합니다: runner 계정은 6만 5천 개의 하위 ID를 소유하므로, 컨테이너 내부에서 루트라고 믿는 프로세스는 외부에서는 권한 없는 ID에 매핑됩니다. Lingering은 사용자 세션을 활성 상태로 유지하여 해당 계정의 systemd 유닛이 로그아웃 후에도 살아 있게 합니다.
Node가 설치된 이유는 대부분의 재사용 가능한 작업이 JavaScript이고 러너가 이 구성에서 호스트에서 실행하기 때문입니다. 체크아웃 단계에서 이를 발견하는 것은 흔한 10분짜리 우회로입니다.
2. 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}}"마지막 명령은 overlay true라고 답해야 합니다. vfs 드라이버 대신이면 fuse-overlayfs가 없다는 뜻이며, vfs는 모든 빌드에서 모든 레이어를 전체 복사하므로 90초 파이프라인이 6분짜리가 됩니다.
3. 프라이빗 레지스트리
apt install -y docker-registry
htpasswd -c /etc/docker/registry/htpasswd ci
certbot certonly --standalone -d registry.example.com레지스트리를 루프백에 바인딩하고 nginx가 인증을 포함한 모든 외부 요청을 처리하게 하세요. /etc/docker/registry/config.yml에서:
version: 0.1
storage:
filesystem:
rootdirectory: /srv/registry
delete:
enabled: true
http:
addr: 127.0.0.1:5000레지스트리 자체에는 자격 증명이 없습니다. 프록시를 통과하지 않은 요청을 받지 않기 때문입니다. 비밀번호를 확인할 곳이 두 군데가 아니라 한 곳인 것이 더 좋습니다.
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는 업로드 제한을 제거합니다. nginx 기본값을 그대로 두면 1MB 이상의 모든 레이어가 푸시 중 약 3분의 2 지점에서 413 오류와 함께 실패하며, 이는 기억에 남는 오후가 됩니다.
systemctl enable --now docker-registry nginx4. 러너
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.yamlBuildah가 이미 격리를 제공하므로 작업이 컨테이너 내부가 아닌 호스트에서 실행되도록 생성된 파일을 편집하세요. 중첩은 복잡성만 더할 뿐입니다:
runner:
capacity: 2
timeout: 1h
labels:
- "debian-13:host"
host:
workdir_parent: /home/runner/work
cache:
enabled: true
dir: /home/runner/cache8코어에서 용량 2는 의도적입니다: 빌드는 대체로 직렬이며, 각각 4코어를 가진 동시 작업 2개가 동일한 캐시를 두고 경쟁하는 4개 작업보다 빨리 끝납니다. 생성된 토큰으로 포지에 등록하세요:
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:host그런 다음 권한 없는 계정으로 실행되는 유닛:
[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.targetrunner 계정의 실제 uid를 XDG_RUNTIME_DIR에 대체하세요; id -u runner이 출력합니다. 루트리스 podman은 해당 디렉토리가 존재해야 하며, 이는 1단계의 linger 설정이 보장합니다.
systemctl daemon-reload && systemctl enable --now act-runner5. 빌드하고 푸시하는 워크플로우
저장소의 .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은 레이어 캐싱을 켜며, 이는 모든 커밋에서 의존성을 재빌드하는 것과 변경될 때 재빌드하는 것의 차이입니다.
검증
유닛이 시작된 후 몇 초 안에 러너가 포지의 러너 목록에 온라인으로 표시되어야 합니다. 그런 다음 커밋을 푸시하고 머신에서 작업을 지켜보세요:
journalctl -fu act-runner완료되면 성공 보고만 하는 것이 아니라 이미지가 실제로 도착했는지 확인하세요:
skopeo inspect --creds ci:<password> docker://registry.example.com/app:latest | head -20
skopeo list-tags --creds ci:<password> docker://registry.example.com/app다이제스트, 레이어 목록, 두 태그를 확인하세요. 이제 실행을 증명하세요. 가까운 다른 머신이 있다면 사용하세요:
podman run --rm registry.example.com/app:latest --version마지막으로, 이 전체 빌드가 기반하는 주장입니다. 작업이 실행 중일 때 프로세스 소유자를 확인하세요:
ps -eo user,pid,comm | grep -E "buildah|podman" | head
sudo -u runner podman info --format "{{.Host.Security.Rootless}}"모든 프로세스는 runner에 속하며, 보안 검사는 true라고 답합니다. 파이프라인 어디에도 루트가 없으며, 이는 손상된 빌드 스크립트가 레지스트리 키와 하이퍼바이저 대신 권한 없는 계정과 네임스페이스를 얻는다는 뜻입니다.
이후
레지스트리 스토리지는 오래된 태그를 제거하지 않으면 무한정 늘어나므로, 신뢰할 수 있는 보존 규칙이 생기면 주간 타이머로 registry garbage-collect을 실행하세요. 빌드가 테스트보다 병목이 된다면 비교 페이지에서 다음 크기가 어떤지 확인하세요; 대부분의 예상보다 코어 수가 덜 도움이 되고, 더 빠른 코어가 훨씬 더 도움이 됩니다.