왜 Self-Hosted Runner인가
GitHub-hosted runner는 편리하지만, 빌드 시간 제한(6시간)과 무료 분수 한도, 내부 네트워크 접근 불가 등 실무에서 벽에 부딪히는 순간이 옵니다. Self-hosted runner는 이런 한계를 깨고, 자체 하드웨어(GPU, 대용량 메모리)를 CI/CD에 직접 투입할 수 있는 공식 방법입니다.
Self-Hosted Runner의 핵심 구조
Self-hosted runner는 GitHub Actions 서비스와 long-poll 방식으로 연결됩니다. Runner 프로세스가 GitHub API를 주기적으로 호출하여 할당된 job을 가져오고, 로컬에서 실행한 뒤 결과를 다시 보고합니다.
- Runner Application: .NET 기반 에이전트로,
config.sh로 등록하고run.sh또는 systemd 서비스로 실행합니다. - Labels:
self-hosted,linux,x64등 기본 레이블 외에 커스텀 레이블(gpu,staging)을 붙여 워크플로에서runs-on으로 선택합니다. - Runner Group: Organization 레벨에서 runner를 그룹으로 묶어 특정 리포지토리에만 할당할 수 있습니다.
설치와 등록: 단계별 절차
아래는 Ubuntu 22.04 기준입니다. 공식 문서(Adding self-hosted runners)를 따릅니다.
1단계: 전용 사용자 생성
보안을 위해 root가 아닌 전용 사용자로 실행합니다.
sudo useradd -m -s /bin/bash ghrunner
sudo usermod -aG docker ghrunner
2단계: Runner 다운로드 및 등록
su - ghrunner
mkdir actions-runner && cd actions-runner
curl -o actions-runner-linux-x64-2.321.0.tar.gz -L
https://github.com/actions/runner/releases/download/v2.321.0/actions-runner-linux-x64-2.321.0.tar.gz
tar xzf ./actions-runner-linux-x64-2.321.0.tar.gz
./config.sh --url https://github.com/YOUR_ORG --token AXXXX...
--labels gpu,staging --name my-build-server
3단계: systemd 서비스 등록
sudo ./svc.sh install ghrunner
sudo ./svc.sh start
sudo ./svc.sh status
이렇게 하면 서버 재부팅 후에도 runner가 자동 시작됩니다.
보안: 반드시 지켜야 할 3가지 원칙
Self-hosted runner는 편리한 만큼 보안 위험도 큽니다. GitHub 공식 문서(Self-hosted runner security)에서도 아래 사항을 강조합니다.
- Public 리포지토리에 연결하지 마세요. 외부 PR의 워크플로가 runner에서 임의 코드를 실행할 수 있습니다. Fork PR을 통한 secret 탈취, 크립토마이닝 등 실제 피해 사례가 보고되었습니다.
- 최소 권한 원칙을 적용하세요. runner 사용자에게 sudo 권한을 주지 않고, Docker socket 접근도 필요한 경우에만 허용합니다.
- 작업 후 환경을 초기화하세요.
--ephemeral플래그로 1회용 runner를 운영하거나, 컨테이너 기반 runner(actions-runner-controller)를 사용하면 빌드 간 오염을 방지할 수 있습니다.
Ephemeral Runner: 1회용 실행으로 격리 강화
GitHub Actions runner v2.300 이상에서 --ephemeral 플래그를 지원합니다. 이 모드에서 runner는 job 하나를 실행한 뒤 자동으로 등록 해제됩니다.
./config.sh --url https://github.com/YOUR_ORG --token AXXXX...
--ephemeral --labels ephemeral,linux
Ephemeral runner는 외부에서 트리거하는 오케스트레이터(systemd timer, cron, Kubernetes controller)와 결합하면, 필요할 때만 runner를 띄우고 끝나면 즉시 정리하는 패턴을 구현할 수 있습니다.
Actions Runner Controller(ARC): Kubernetes 기반 자동 스케일링
Runner를 수동으로 관리하는 대신, Kubernetes 클러스터에서 Pod 단위로 자동 생성/삭제하는 것이 ARC(Actions Runner Controller)입니다. GitHub이 공식으로 지원하며, Helm chart로 설치합니다.
helm install arc
--namespace arc-systems --create-namespace
oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller
helm install arc-runner-set
--namespace arc-runners --create-namespace
-f values.yaml
oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set
ARC는 webhook 이벤트를 받아 runner Pod를 동적으로 생성합니다. job이 끝나면 Pod가 삭제되므로 ephemeral 특성이 자동 보장됩니다.
성능 최적화 팁
- Tool cache 활용:
actions/setup-node등이RUNNER_TOOL_CACHE디렉터리에 런타임을 캐시합니다. 이 경로를 persistent volume으로 마운트하면 매 빌드마다 다운로드를 건너뜁니다. - Docker layer cache:
docker buildx와--cache-from/--cache-to를 결합하면 이미지 빌드 시간을 줄일 수 있습니다. Self-hosted runner에서는 로컬 레지스트리를 캐시 백엔드로 쓰면 네트워크 비용도 절약됩니다. - 병렬 job 분산: 러너를 여러 대 두고 같은 레이블을 붙이면 GitHub이 자동으로 라운드로빈 할당합니다. ARC에서는
maxRunners로 상한을 지정합니다.
모니터링과 장애 대응
Runner가 offline 상태로 바뀌면 워크플로가 pending에서 멈춥니다. 아래 사항을 점검하세요.
systemctl status actions.runner.*.service로 프로세스 상태 확인_diag/디렉터리의 Runner 로그에서 연결 오류, 인증 만료 메시지 확인- runner 토큰은 1시간 유효이므로, 자동 재등록 스크립트(PAT 기반 REST API 호출)를 준비하면 안정적입니다.
- GitHub Status(githubstatus.com)에서 Actions 서비스 장애 여부도 함께 확인하세요.
실전 체크리스트
- 전용 사용자 생성, root 실행 금지
- Public repo에 self-hosted runner 연결 금지
--ephemeral또는 ARC로 빌드 간 격리 확보- systemd 서비스로 자동 재시작 설정
- tool cache를 persistent storage에 마운트
- Docker layer cache 전략 수립
- Runner offline 알림(Slack/Telegram webhook) 연동
- 토큰 자동 갱신 스크립트 준비
Self-hosted runner는 CI/CD 파이프라인의 성능과 유연성을 높이는 강력한 도구입니다. 보안 원칙을 지키고, ephemeral 패턴이나 ARC를 도입하면 운영 부담도 줄일 수 있습니다.
더 깊은 DevOps 주제가 궁금하다면 Kubernetes NetworkPolicy 심화 가이드와 Terraform Workspace 심화 가이드도 확인해 보세요.
온프레미스 CI/CD 구축이나 인프라 자동화에 대해 더 알고 싶으신가요?
문의 페이지에서 편하게 질문해 주세요.
7) 실전 docker-compose.yml: NestJS + MySQL + Redis
version: "3.9"
services:
api:
build:
context: .
dockerfile: Dockerfile
target: runner # Multi-Stage 빌드의 runner 스테이지
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=mysql://app:secret@mysql:3306/mydb
- REDIS_URL=redis://redis:6379
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/healthz"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
restart: unless-stopped
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_root_pw
MYSQL_DATABASE: mydb
MYSQL_USER: app
MYSQL_PASSWORD_FILE: /run/secrets/mysql_app_pw
volumes:
- mysql_data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
secrets:
- mysql_root_pw
- mysql_app_pw
restart: unless-stopped
redis:
image: redis:7-alpine
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
restart: unless-stopped
volumes:
mysql_data:
redis_data:
secrets:
mysql_root_pw:
file: ./secrets/mysql_root_pw.txt
mysql_app_pw:
file: ./secrets/mysql_app_pw.txt
8) Compose 운영 명령어 치트시트
# 기본 운영
docker compose up -d # 백그라운드 시작
docker compose down # 중지 + 네트워크 제거
docker compose down -v # + 볼륨도 제거 (주의!)
docker compose restart api # 특정 서비스만 재시작
# 로그
docker compose logs -f api # 실시간 로그
docker compose logs --tail=100 mysql # 최근 100줄
# 스케일링
docker compose up -d --scale api=3 # API 서비스 3개로
# 빌드
docker compose build --no-cache api # 캐시 없이 재빌드
docker compose up -d --build # 빌드 + 시작
# 상태 확인
docker compose ps # 서비스 상태
docker compose top # 프로세스 목록
docker compose stats # 리소스 사용량
9) 관련 글
- Docker Multi-Stage Build — Compose에서 사용할 최적화된 이미지 빌드 전략입니다.
- Redis 캐시 전략 — Compose에 포함된 Redis의 캐시 패턴을 다룹니다.
- InnoDB Buffer Pool — Compose MySQL의 성능 튜닝 포인트입니다.
- K8s Secrets 관리 — Compose secrets에서 K8s로 마이그레이션할 때 참고하세요.