Published on

GitLab CI Docker 빌드 no space left 해결 가이드

Authors

서로 다른 프로젝트가 같은 GitLab Runner에서 돌기 시작하면, 어느 날 갑자기 Docker 빌드가 no space left on device로 터집니다. 문제는 단순히 “디스크를 늘리면 된다”가 아니라, **어디에 공간이 쌓이는지(레이어/캐시/워크스페이스/아티팩트/로그)**를 정확히 찍고, **Runner 실행 방식(docker executor, shell, Kubernetes)**에 맞춰 청소 전략을 넣는 것입니다.

이 글은 GitLab CI에서 Docker 빌드 중 발생하는 no space left재현 포인트 → 원인 분류 → 진단 명령 → Runner 타입별 해결 순서로 정리합니다.

1) 에러 메시지별로 원인 좁히기

no space left on device는 실제로는 여러 “장치”에서 발생합니다. 메시지에 따라 타겟이 달라집니다.

Docker 레이어/이미지 저장소가 꽉 찬 경우

  • 증상: failed to copy: write /var/lib/docker/... no space left on device
  • 의미: Docker의 스토리지 디렉터리(대개 /var/lib/docker)가 찼습니다.

overlay2 inode 고갈(파일 개수 한도)인 경우

  • 증상: 디스크 사용량은 남아 보이는데도 no space left 발생
  • 의미: inode가 고갈되면 용량이 남아도 파일을 더 못 만듭니다.

GitLab Runner 작업 디렉터리(빌드 워크스페이스)가 꽉 찬 경우

  • 증상: write ... no space left on device가 프로젝트 경로(예: /builds/...)에서 발생
  • 의미: 체크아웃/빌드 산출물/캐시가 runner workdir에 쌓였습니다.

Docker-in-Docker(dind)에서 dind 컨테이너의 디스크가 찬 경우

  • 증상: dind 서비스가 사용하는 볼륨/레이어가 급격히 증가
  • 의미: 매 빌드가 새 레이어를 쌓는데 정리가 없습니다.

2) 먼저 “어디가 찼는지” 2분 진단

아래 진단은 Runner 호스트에서 하는 것이 가장 정확합니다. (Kubernetes executor면 노드/파드 컨텍스트가 다를 수 있음)

# 용량/마운트 확인
sudo df -h

# inode 확인(용량이 남아도 inode가 없으면 실패)
sudo df -ih

# Docker가 어디에 데이터를 쌓는지
docker info | sed -n '1,120p'

# Docker 디스크 사용량 요약
docker system df

# 어떤 디렉터리가 큰지(호스트 기준)
sudo du -h -d 1 /var/lib/docker | sort -h
sudo du -h -d 1 /var/lib/gitlab-runner | sort -h

# journald 로그가 과도하게 쌓였는지(리눅스 호스트)
sudo journalctl --disk-usage

진단 결과로 크게 3갈래로 나뉩니다.

  1. /var/lib/docker가 큼 → 이미지/레이어/빌드 캐시 정리
  2. /builds 또는 runner cache가 큼 → 캐시/아티팩트 정책 조정
  3. inode 고갈 → 작은 파일 대량 생성(특히 node_modules, pip cache, yarn cache) 억제

3) 가장 흔한 원인: Docker 레이어/캐시 폭증

3.1 즉시 응급 처치(호스트에서)

서비스 영향이 있을 수 있으니(사용 중인 이미지 삭제 등) 운영 환경에서는 주의하세요.

# 사용하지 않는 컨테이너/네트워크/이미지/빌드 캐시 정리
sudo docker system prune -af

# 볼륨까지 정리(정말 필요 없는 경우에만)
sudo docker system prune -af --volumes

# 빌드 캐시만 정리(빌드가 느려질 수 있음)
sudo docker builder prune -af

docker system df로 정리 전/후를 비교하면 원인이 명확해집니다.

3.2 근본 해결: Dockerfile 레이어 최적화

레이어가 불필요하게 커지는 대표 패턴은 다음입니다.

  • RUN apt-get updateRUN apt-get install이 분리됨
  • 패키지 캐시/임시파일을 레이어에 남김
  • COPY . .가 너무 이른 단계에 있어 캐시가 자주 깨짐

아래는 Node 기반 예시입니다.

# syntax=docker/dockerfile:1.7
FROM node:20-bookworm AS deps
WORKDIR /app

# 의존성 파일만 먼저 복사해 캐시 효율 극대화
COPY package.json package-lock.json ./

# BuildKit 캐시 마운트로 npm 캐시를 레이어 밖으로
RUN --mount=type=cache,target=/root/.npm \
    npm ci

FROM node:20-bookworm AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:20-bookworm-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/.next ./.next
COPY --from=build /app/public ./public
COPY --from=build /app/package.json ./package.json
COPY --from=build /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "node_modules/next/dist/bin/next", "start"]

핵심은 BuildKit cache mount를 통해 캐시를 레이어에 굳히지 않고, COPY 순서를 조정해 캐시 적중률을 올리는 것입니다.

3.3 .dockerignore로 “빌드 컨텍스트” 줄이기

GitLab CI에서 흔히 놓치는 부분이 빌드 컨텍스트 크기입니다. 컨텍스트가 커지면 전송/압축 과정에서 디스크와 inode를 동시에 잡아먹습니다.

# .dockerignore
.git
node_modules
.next
dist
coverage
*.log
.cache
.env

컨텍스트를 줄이면 디스크뿐 아니라 빌드 시간도 줄어듭니다.

4) GitLab CI에서 특히 자주 터지는 패턴과 해결

4.1 Docker-in-Docker(dind) 사용 시: 캐시가 계속 쌓인다

docker:dind는 편하지만, 잘못 쓰면 빌드마다 레이어가 누적됩니다. 아래는 BuildKit + 레지스트리 캐시를 활용해 “빨라지면서도 디스크를 덜 쓰는” 방향의 예시입니다.

# .gitlab-ci.yml
image: docker:27
services:
  - name: docker:27-dind
    command: ["--mtu=1460"]

variables:
  DOCKER_HOST: tcp://docker:2375
  DOCKER_TLS_CERTDIR: ""
  DOCKER_BUILDKIT: "1"

stages: [build]

build-image:
  stage: build
  script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
    - |
      docker buildx create --use || true
      docker buildx build \
        --cache-from=type=registry,ref=$CI_REGISTRY_IMAGE:buildcache \
        --cache-to=type=registry,ref=$CI_REGISTRY_IMAGE:buildcache,mode=max \
        -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA \
        --push .
  after_script:
    # dind 내부 정리는 제한적이지만, 가능하면 수행
    - docker system df || true
    - docker system prune -af || true

포인트:

  • 캐시를 로컬 디스크가 아니라 레지스트리로 외부화
  • --push를 사용해 최종 이미지를 남기지 않고(로컬 적재 최소화) 바로 업로드

4.2 GitLab Runner 캐시/아티팩트가 디스크를 잠식

캐시는 편하지만, “키 전략”이 엉키면 계속 누적됩니다. 캐시 디렉터리가 커지는 현상은 GitHub Actions에서도 매우 유사하게 발생합니다. 캐시가 기대대로 안 먹거나 과도하게 쌓일 때는 아래 체크리스트도 함께 참고하면 좋습니다: GitHub Actions 캐시가 안 먹을 때 디버깅 체크리스트

GitLab CI에서는 다음을 점검하세요.

  • 캐시 키가 매 커밋마다 달라져 무한 증식하는가?
  • policy: pull-push로 매번 업로드/다운로드하면서 용량이 커지는가?
  • 아티팩트 expire_in이 너무 길어 저장소를 압박하는가?

예시: 브랜치 단위 캐시로 제한하고, 산출물은 짧게 보관

cache:
  key: "$CI_PROJECT_NAME-$CI_COMMIT_REF_SLUG"
  paths:
    - .npm/
  policy: pull-push

build:
  script:
    - npm ci --cache .npm --prefer-offline
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 3 days

4.3 로그/아티팩트 크기 때문에 간접적으로 터지는 경우

빌드 자체는 가벼운데도 runner 디스크가 차는 경우, 원인은 종종 로그 폭증입니다(테스트가 수만 줄 출력, debug 모드 고정 등). 특히 self-hosted runner면 journald가 커질 수 있습니다.

# journald 용량 제한(호스트)
sudo mkdir -p /etc/systemd/journald.conf.d
cat <<'EOF' | sudo tee /etc/systemd/journald.conf.d/size.conf
[Journal]
SystemMaxUse=1G
RuntimeMaxUse=512M
EOF
sudo systemctl restart systemd-journald

5) Runner 타입별 권장 처방

5.1 Docker executor (self-hosted VM)

  • /var/lib/docker를 별도 디스크로 분리(가장 효과 큼)
  • 주기적 prune를 systemd timer/cron으로 수행
  • BuildKit 사용 + 레지스트리 캐시

주기 정리 예시(새벽 3시):

sudo crontab -e
# 매일 03:00에 사용하지 않는 리소스 정리
0 3 * * * /usr/bin/docker system prune -af --volumes >/var/log/docker-prune.log 2>&1

운영 컨테이너를 같이 올리는 호스트라면 --volumes는 특히 신중해야 합니다.

5.2 Shell executor

Shell executor는 호스트에 직접 파일이 쌓입니다.

  • git clean -ffdx로 워크스페이스 정리(필요 시)
  • 빌드 산출물 경로를 명확히 하고, job 종료 시 삭제
  • 언어별 캐시 디렉터리를 프로젝트 내부로 모아 GitLab cache로만 관리
build:
  script:
    - make build
  after_script:
    - rm -rf .cache tmp || true

5.3 Kubernetes executor

K8s에서는 노드의 ephemeral storage가 병목입니다.

  • resources.requests/limitsephemeral-storage 지정
  • dind 대신 kaniko/buildkitd(rootless) 등 고려
  • 노드 디스크 압박 시 eviction 발생 가능
build:
  image: docker:27
  script:
    - docker build -t ...
  resources:
    requests:
      ephemeral-storage: "2Gi"
    limits:
      ephemeral-storage: "6Gi"

Kubernetes 환경에서 요청/응답 크기나 제한에 부딪히는 문제를 자주 겪는다면, 성격은 다르지만 “제한값 때문에 실패한다”는 점에서 Kubernetes API 413 Request Entity Too Large 해결도 함께 보면 원인 접근 방식에 도움이 됩니다.

6) 재발 방지 체크리스트(현장에서 바로 쓰는 버전)

빌드 컨텍스트/레이어

  • .dockerignore가 제대로 설정되어 컨텍스트가 작나?
  • COPY 순서가 의존성 캐시를 살리도록 구성됐나?
  • apt/npm/pip 캐시가 레이어에 남지 않나?
  • 멀티 스테이지로 런타임 이미지를 최소화했나?

CI 캐시/아티팩트

  • 캐시 키가 커밋 단위로 폭증하지 않나?
  • 아티팩트 expire_in이 과도하게 길지 않나?
  • 불필요한 디렉터리(coverage, logs, tmp)를 artifacts에 넣지 않았나?

Runner/호스트 운영

  • /var/lib/docker가 별도 볼륨인가?
  • 주기적 prune가 있는가?
  • inode 사용률을 모니터링하는가?
  • journald/로그가 비정상적으로 커지지 않게 제한했나?

7) 결론: “디스크 증설”은 마지막 카드

no space left on device는 대개 (1) Docker 레이어/캐시 누적, (2) CI 캐시/아티팩트 정책 실수, (3) inode 고갈 중 하나입니다. 먼저 df -h, df -ih, docker system df로 범인을 특정하고, BuildKit/레지스트리 캐시/.dockerignore/주기 정리를 조합하면 대부분 재발을 막을 수 있습니다.

CI가 안정화되면, 그 다음 단계는 “빨라지면서도 덜 쓰는 빌드”입니다. 캐시가 제대로 동작하는지 감이 안 올 때는 위에서 언급한 GitHub Actions 캐시 디버깅 체크리스트처럼, 캐시 적중/미스의 근거를 로그로 확인하는 습관이 장기적으로 가장 큰 비용을 줄여줍니다.