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 psdocker 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-hostedself-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 psdocker 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 시계열)“빌드 반복 → 점진적 누적” 패턴의 시각적 증거오프닝/원인 섹션
2cron 적용 전후 디스크 사용률 비교 수치”재발 없이 안정화” 주장의 정량적 근거해결 방법 섹션