- Published on
GitHub Actions self-hosted runner 멈춤 원인 8가지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버/VM에 self-hosted runner를 올려두면, 어느 날부터 잡이 큐에 쌓이는데 runner가 안 받거나, 잡이 시작은 했는데 끝까지 못 가고 멈춘 듯 보이는 상황이 종종 발생합니다. 이때 문제는 GitHub Actions 자체보다도 runner 호스트의 서비스 관리(systemd), 네트워크, 리소스(디스크/메모리/CPU), 컨테이너 런타임, 잡 동시성 설정에서 터지는 경우가 많습니다.
이 글은 “멈춤”을 크게 두 유형으로 나눠 봅니다.
- A. 잡을 아예 안 받음: GitHub UI에는 runner가 Online인데, 큐에 잡이 남아있음
- B. 잡 실행 중 정지/무한 대기: 특정 step에서 영원히 끝나지 않거나, runner 프로세스가 죽어 잡이 중단됨
아래 8가지 원인을 순서대로 확인하면, 대부분의 케이스는 30분 내로 원인 좁히기가 가능합니다.
0) 먼저 확인할 관측 포인트(필수)
원인 분석 전에 “어디서 멈췄는지”를 정확히 잡아야 합니다.
GitHub UI에서 확인
- Repository/Org Settings → Actions → Runners
- 상태(Online/Offline), Labels, Busy 여부
- Workflow run 로그
- 어느 step에서 멈췄는지
post단계(예:Post job cleanup.)에서 멈추는지
호스트에서 확인(리눅스 기준)
# runner 서비스 상태
sudo systemctl status actions.runner.*
# 최근 로그
sudo journalctl -u actions.runner.* -n 200 --no-pager
# 러너 프로세스/자식 프로세스 확인
ps auxf | grep -E "Runner.Listener|Runner.Worker" | grep -v grep
# 디스크/메모리/로드
df -h
free -m
uptime
# 네트워크/DNS
curl -I https://api.github.com
curl -I https://pipelines.actions.githubusercontent.com
getent hosts api.github.com
이 관측 포인트는 아래 8가지 원인에서 공통으로 계속 사용합니다.
1) systemd/서비스가 죽었는데 “Online”처럼 보이는 착시
가장 흔한 케이스 중 하나는 Runner.Listener가 죽거나 멈췄는데, GitHub UI에선 잠깐 Online으로 남아 “살아있는 것처럼” 보이는 상황입니다(하트비트/상태 반영 지연).
증상
- UI에는 Online인데 잡을 안 받음
systemctl status에서inactive (dead)또는 재시작 루프
점검/해결
- 서비스 재시작 및 자동 재시작 정책 확인
sudo systemctl restart actions.runner.<ORG>-<REPO>.<NAME>.service
sudo systemctl cat actions.runner.<ORG>-<REPO>.<NAME>.service
Restart=always/RestartSec=... 같은 정책이 없거나 너무 공격적으로 설정돼 crash loop가 난다면 조정합니다.
또한 러너 업데이트/재설치 후 서비스 파일이 꼬이는 경우가 있어, 러너 디렉터리에서 재설치 스크립트를 다시 적용합니다.
cd /opt/actions-runner
sudo ./svc.sh uninstall
sudo ./svc.sh install
sudo ./svc.sh start
2) 디스크 부족(특히 /, /var, Docker overlay2)로 인한 “무한 대기”
self-hosted runner는 체크아웃/캐시/아티팩트/도커 레이어로 디스크를 빠르게 소모합니다. 디스크가 100%에 가까워지면 다음이 발생합니다.
git checkout이 느려지거나 실패- Docker build/pull이 멈춘 듯 진행이 안 됨
npm ci,pip install등에서 I/O 대기 증가
이는 Kubernetes 노드가 리소스 문제로 비정상 상태가 되는 것과 유사한 패턴을 보이기도 합니다. (노드 레벨 헬스 점검 관점은 EKS kubelet NotReady - PLEG is not healthy 7가지에서도 참고할 만합니다.)
점검
df -h
sudo du -xh /opt/actions-runner/_work | sort -h | tail -n 20
sudo du -xh /var/lib/docker | sort -h | tail -n 20
해결
_work정리(잡마다 workspace를 유지하면 폭증)- Docker prune(주의: 다른 서비스와 공유 시 영향)
# 러너 작업 디렉터리 정리(러너 전용 서버라는 전제에서만)
sudo rm -rf /opt/actions-runner/_work/*
# Docker 정리
sudo docker system prune -af --volumes
워크플로우에도 정리 step을 넣어 “장기 운영”에서 누적을 막습니다.
- name: Cleanup workspace
if: always()
run: |
rm -rf "$GITHUB_WORKSPACE"/*
3) 메모리 부족/OOM Killer로 Runner.Worker가 강제 종료
잡이 중간에 멈춘 것처럼 보이는데 실제로는 Worker 프로세스가 OOM으로 죽고, Runner.Listener만 살아있는 케이스가 많습니다.
증상
- 로그가 특정 step에서 뚝 끊김
- 재시도하면 가끔 성공(메모리 사용량 타이밍 이슈)
- 커널 로그에 OOM 기록
점검
# OOM 흔적
sudo dmesg -T | grep -i -E "killed process|out of memory|oom"
# 메모리 확인
free -m
ps aux --sort=-%mem | head
해결
- 러너 호스트 메모리 증설 또는 swap 구성
- 빌드/테스트 동시성 제한(병렬 빌드 옵션 축소)
- Node/Java 빌드라면 힙 제한 명시
# 예: Node 메모리 상한
export NODE_OPTIONS=--max-old-space-size=4096
4) CPU 스로틀링/로드 폭증으로 “진행은 하는데 매우 느림”
runner가 멈춘 게 아니라 CPU가 포화라서 로그 출력/네트워크 핸드셰이크/압축 작업이 극도로 느려지는 경우도 많습니다.
점검
uptime
mpstat 1 5 2>/dev/null || true
top -o %CPU
해결
- 대형 monorepo 테스트/빌드에서 병렬도 조정
- 러너를 1대에 몰아넣지 말고 스케일 아웃(여러 runner + 라벨 분리)
5) 네트워크/프록시/방화벽/DNS 문제로 GitHub 엔드포인트 연결이 끊김
runner는 다음과 같은 GitHub 엔드포인트들과 지속적으로 통신합니다.
api.github.compipelines.actions.githubusercontent.comresults-receiver.actions.githubusercontent.com
기업망/프록시/SSL inspection 환경에서 TLS가 깨지거나, DNS가 간헐적으로 실패하면 “잡을 못 받음” 또는 “업로드 단계에서 멈춤”이 발생합니다.
점검
curl -sv https://api.github.com -o /dev/null
curl -sv https://pipelines.actions.githubusercontent.com -o /dev/null
# DNS
getent hosts api.github.com
getent hosts pipelines.actions.githubusercontent.com
해결
- 프록시 환경 변수(
HTTP_PROXY,HTTPS_PROXY,NO_PROXY)를 runner 서비스에 정확히 주입 - SSL inspection 예외 처리 또는 신뢰 CA 설치
- DNS를 안정적인 리졸버로 변경(시스템/컨테이너 모두)
네트워크 문제가 “서비스는 Running인데 요청이 503/timeout” 같은 형태로 드러날 때는, 마이크로서비스에서 503/데드라인을 추적하듯이 타임아웃 지점과 재시도 정책을 분리해서 보는 접근이 유효합니다. (관점 참고: gRPC 마이크로서비스 503·데드라인 초과 디버깅)
6) Docker/컨테이너 런타임 데드락(overlay2, buildx, hung pull)
self-hosted runner에서 가장 많은 “멈춤 체감”은 Docker step에서 발생합니다.
docker build가 특정 레이어에서 멈춤docker pull이 다운로드 0B/s로 고정buildx가 builder 인스턴스 문제로 대기
점검
sudo systemctl status docker
sudo journalctl -u docker -n 200 --no-pager
docker ps
docker info
# 빌드 중인 프로세스/네트워크 대기 확인
ps auxf | grep -E "docker|buildkit" | grep -v grep
해결
- Docker 데몬 재시작(러너 전용 머신이면 특히 효과적)
sudo systemctl restart docker
- buildx 사용 시 builder를 명시적으로 생성/초기화하고, 잡 종료 시 정리
- name: Setup buildx
run: |
docker buildx create --use --name gha-builder || docker buildx use gha-builder
docker buildx inspect --bootstrap
- name: Cleanup buildx
if: always()
run: docker buildx rm gha-builder || true
7) 러너 버전/Node 런타임 불일치로 액션이 비정상 동작
GitHub Actions의 많은 JavaScript 액션은 Node 런타임에 민감합니다. self-hosted runner는 환경이 제각각이라, 다음 문제가 생길 수 있습니다.
- 시스템 Node가 너무 최신/구형이라 액션이 깨짐
- 프로젝트가 ESM 전환 중인데 러너 환경에서
require기반 스크립트가 실패 - 캐시된 의존성이 꼬여서 특정 step에서 멈춘 듯 보임(실제로는 재시도/백오프)
특히 Node 22 전후로 ESM/require 이슈를 겪는 팀이 많습니다. 프로젝트 레벨에서의 전환 포인트는 Node 22에서 require가 안 될 때 ESM 전환법을 함께 참고하면 좋습니다.
점검
node -v || true
npm -v || true
# runner 버전
cd /opt/actions-runner
./run.sh --version 2>/dev/null || true
해결
actions/setup-node로 워크플로우에서 Node 버전을 고정- 러너를 최신 릴리스로 업데이트
cd /opt/actions-runner
sudo ./svc.sh stop
# 새 러너 패키지로 교체 후
sudo ./svc.sh start
8) 동시성/라벨/그룹 설정 오류로 “잡이 영원히 대기”
runner가 멈춘 게 아니라 스케줄링이 안 되는 경우입니다.
대표 케이스
- 워크플로우가 요구하는
runs-on라벨이 실제 runner 라벨과 불일치 - runner group 접근 제한(Org runner 그룹이 특정 repo에만 허용)
- 한 러너에
--ephemeral/단일 실행 전략을 기대했는데 실제로는 고정 러너라서 작업 디렉터리가 오염 - 동일 리소스를 잡들이 경쟁하면서 교착(예: iOS 빌드 머신 1대에 동시 실행)
점검
- Workflow의
runs-on값과 Settings의 runner Labels를 1:1로 비교 - Org 레벨 runner group 권한 확인
- 동시성 제한이 필요한 작업은
concurrency로 제어
concurrency:
group: ios-build-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: [self-hosted, macos, ios]
steps:
- uses: actions/checkout@v4
- run: xcodebuild -version
또한 “잡이 끝나는데도 러너가 다음 잡을 못 받는” 경우는 작업 공간 오염/락 파일 때문에 다음 잡이 대기하는 패턴일 수 있습니다. 예를 들어 iOS/CocoaPods 계열은 락 충돌이 빌드 파이프라인을 막는 전형적인 사례입니다(맥 러너 운영 시 참고: Flutter iOS 빌드 Podfile.lock 충돌 해결 가이드).
재발 방지 체크리스트(운영 관점)
1) 러너 머신을 “빌드 전용”으로 단순화
- 다른 서비스와 Docker/디스크를 공유하지 않기
- 러너 전용 볼륨/파티션 분리(
/opt/actions-runner,/var/lib/docker)
2) 주기적 헬스 체크 + 자동 복구
- 디스크 80% 초과 시 알림
- OOM 감지 시 서비스 재시작
- Docker 데몬 헬스 체크
간단한 예시(크론/시스템 타이머로 실행):
#!/usr/bin/env bash
set -euo pipefail
# 디스크 90% 이상이면 runner 중지 후 정리(정책은 환경에 맞게 조정)
usage=$(df -P / | awk 'NR==2{gsub(/%/,"",$5); print $5}')
if [ "$usage" -ge 90 ]; then
systemctl stop actions.runner.* || true
rm -rf /opt/actions-runner/_work/* || true
docker system prune -af --volumes || true
systemctl start actions.runner.* || true
fi
3) 워크플로우에 타임아웃을 명시
무한 대기를 “멈춤”으로 인지하게 만들기 전에, 실패로 전환해 재시도/알림이 가능하게 합니다.
jobs:
build:
timeout-minutes: 45
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- name: Build
run: make build
마무리
self-hosted runner 멈춤은 대체로 서비스 상태(systemd), 리소스(디스크/OOM/CPU), 네트워크, Docker 런타임, 스케줄링(라벨/그룹/동시성) 네 축에서 원인이 갈립니다. “잡을 안 받는지(A)”와 “잡 실행 중 멈추는지(B)”를 먼저 구분하고, journalctl, dmesg, df, docker logs 같은 기본 관측을 붙이면 재현 없이도 원인을 빠르게 좁힐 수 있습니다.
운영이 길어질수록 핵심은 ‘한 번 고치는 것’이 아니라 **누적(디스크/캐시/워크스페이스 오염)**과 **간헐(네트워크/DNS/OOM)**을 자동으로 감지하고 복구하는 체계를 갖추는 것입니다.