self-hosted runner 디스크 고갈 - Docker 고아 컨테이너 누적
ENOSPC로 CI 빌드가 실패했다. 최근에 프로비저닝한 머신이고 대용량 파일을 저장한 적도 없었는데 8GB 중 7.5GB가 사용 중이었다. 파고 들어가니 buildx가 매 빌드마다 builder 컨테이너를 새로 만들고 한 번도 정리하지 않았다는 것이 드러났다. 총 낭비: 2.3GB, 8GB 디스크의 29%.
환경: GitHub Actions self-hosted runner (EC2, 8GB gp3 EBS), Docker buildx
디스크가 꽉 찼는데 원인을 모른다
디스크 사용량부터 확인했다. 94%나 쓰고 있었다.
$ df -h
Filesystem Size Used Avail Use% Mounted on
/dev/nvme0n1p1 8.0G 7.5G 522M 94% /
이 runner는 Docker 빌드 외에 하는 일이 없다. docker system df로 현황을 봤다.
$ sudo docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 3 1 669.8MB 438.3MB (65%)
Containers 6 6 0B 0B
Local Volumes 6 6 1.839GB 0B (0%)
Build Cache 0 0 0B 0B
이미지가 670MB, 볼륨이 1.8GB였다. 그런데 눈에 들어온 건 숫자가 아니라 Containers 행이었다. 컨테이너 6개가 전부 Active 상태다. 이 runner는 빌드를 한 번에 하나씩만 처리하는데, 6개의 컨테이너가 동시에 살아있을 이유가 없다.
docker ps와 docker buildx ls 사이의 불일치
실행 중인 컨테이너 목록을 확인했다.
$ sudo docker ps
CONTAINER ID IMAGE NAMES
15f64444195a moby/buildkit:buildx-stable-1 buildx_buildkit_builder-5deeedd3-...0
ec379555992a moby/buildkit:buildx-stable-1 buildx_buildkit_builder-c2f87100-...0
4472433fc19e moby/buildkit:buildx-stable-1 buildx_buildkit_builder-56b7a07e-...0
... (총 6개, 전부 buildx builder)
6개 전부 moby/buildkit 이미지를 실행하는 buildx builder였다. 각각 UUID 기반 이름이 붙어 있었다. 그런데 buildx가 이 컨테이너들을 인식하는지 확인하니 결과가 달랐다.
$ sudo docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
default * docker
default default running v0.12.5 linux/arm64, linux/arm/v7, linux/arm/v6
buildx 관리 목록에는 아무것도 없었다. docker ps에서는 6개가 보이지만 docker buildx ls는 기본 builder만 인식한다. 이 컨테이너들은 buildx 관리에서 이탈한 고아 상태였고, docker system prune 같은 자동 정리 대상에서도 빠져 있었다.
인과 체인은 이렇게 흘렀다.
sequenceDiagram participant CI as CI 워크플로우 participant BX as docker buildx participant D as 디스크 (8GB) CI->>BX: 빌드 #1 (builder-aaa 생성) BX->>D: 컨테이너 + 볼륨 (~300MB) Note over BX: 빌드 완료, builder 미정리 CI->>BX: 빌드 #2 (builder-bbb 생성) BX->>D: 컨테이너 + 볼륨 (~300MB) Note over BX: 빌드 완료, builder 미정리 CI->>BX: 빌드 #N BX--xD: ENOSPC (디스크 포화)
매 빌드마다 buildx가 UUID 기반으로 새 builder 컨테이너를 생성하고, 빌드가 끝나도 이전 builder를 정리하지 않는다. 빌드당 약 300MB의 볼륨이 누적되므로, 8GB 디스크 기준 약 20~25회 빌드 후 디스크가 가득 찬다.
이 문제가 self-hosted runner에서만 발생하는 이유는 환경 차이에 있다. GitHub-hosted runner는 빌드마다 새 VM이 생성되고 빌드 후 VM 자체가 삭제되므로 Docker 잔여물이 남을 수가 없다. self-hosted runner는 같은 VM을 계속 재사용하므로, 정리 로직이 없으면 누적은 필연적이다.
| 구분 | GitHub-hosted | self-hosted |
|---|---|---|
| VM 수명 | 빌드마다 신규 생성 → 빌드 후 삭제 | 지속 재사용 |
| Docker 잔여물 | 자동 소멸 | 수동 정리 필요 |
| 디스크 고갈 가능성 | 없음 | 있음 |
즉시 정리 후 재발 방지 cron
수동 정리
고아 컨테이너와 볼륨을 직접 삭제했다.
아래 명령은 buildx builder 컨테이너를 중지·삭제하고, 남은 볼륨과 이미지를 일괄 정리한다.
# 고아 builder 컨테이너 중지 및 삭제
sudo docker stop $(sudo docker ps -q --filter "name=buildx_buildkit")
sudo docker rm $(sudo docker ps -aq --filter "name=buildx_buildkit")
# 고아 볼륨 및 미사용 이미지 정리
sudo docker volume rm $(sudo docker volume ls -q)
sudo docker system prune -af --volumes이후 실패했던 워크플로우를 re-run하니 빌드가 성공했다.
daily cleanup cron
같은 문제가 재발하지 않도록 runner에 daily cron을 등록했다.
sudo tee /etc/cron.daily/docker-cleanup << 'EOF'
#!/bin/bash
docker buildx rm --all-inactive --force || true
docker system prune -af --volumes || true
EOF
sudo chmod +x /etc/cron.daily/docker-cleanup워크플로우마다 cleanup 단계를 추가하는 방법도 있었지만, cron을 선택한 이유는 레포마다 워크플로우를 수정할 필요 없이 runner 레벨에서 일괄 적용되기 때문이다. 추후 이 cron을 runner AMI에 포함시키면 새 runner를 프로비저닝할 때 자동으로 설정된다.
버퍼 확보 차원에서 EBS도 8GB에서 30GB로 증설했다. 수치로는 사용률 67% → 19%.
이 장애가 남긴 것
self-hosted runner에서 CI가 갑자기 실패하면 코드보다 환경을 먼저 의심해야 한다. 같은 코드가 어제는 빌드되고 오늘 안 된다면, df -h를 먼저 확인하는 것이 최단 경로다. ENOSPC는 코드 버그가 아니라 환경 누적의 신호다.
docker ps와 docker buildx ls 결과가 다르다면 고아 컨테이너다. buildx 관리 목록에서 이탈한 컨테이너는 docker system prune 자동 정리 대상에서도 빠진다. 수동으로 삭제하거나 docker buildx rm --all-inactive로 일괄 처리해야 한다.
더 근본적으로는, self-hosted runner를 운영한다면 cleanup 자동화가 필수다. GitHub-hosted와 달리 VM이 재사용되므로, 정리 로직 없이는 어떤 도구든 잔여물이 쌓인다. Docker에 국한된 이야기가 아니다.
앞으로의 방향
현재 cron은 매일 한 번 전체 정리를 수행하는 방식이라, 하루에 빌드가 집중되면 그 사이 디스크가 다시 찰 수 있다. 더 촘촘한 대응으로는 워크플로우 레벨에서 post 단계에 docker buildx prune을 추가하는 방안이 있다. 다만 레포마다 워크플로우를 수정해야 하는 비용이 있어, composite action으로 공통화하는 것을 검토 중이다.
EBS 증설은 버퍼를 늘린 것일 뿐 근본 해결은 아니다. 디스크 사용량 메트릭에 CloudWatch 알람을 붙여서 일정 임계치 이상이 되면 사전에 감지하는 것이 다음 과제다.
정보 공백 요약
| # | 필요한 정보 | 이유 | 위치 |
|---|---|---|---|
| 1 | 디스크 사용량 추이 그래프 (CloudWatch 또는 df -h 시계열) | “빌드 반복 → 점진적 누적” 패턴의 시각적 증거 | 오프닝/원인 섹션 |
| 2 | cron 적용 전후 디스크 사용률 비교 수치 | ”재발 없이 안정화” 주장의 정량적 근거 | 해결 방법 섹션 |