- Published on
Docker 빌드 중 no space left? 레이어·캐시 최적화
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
Docker로 이미지를 빌드하다 보면 어느 순간 no space left on device가 튀어나옵니다. 호스트 디스크 용량은 남아 있는데도 빌드가 실패하는 경우가 많고, CI에서는 더 자주 재현됩니다. 이 문제는 단순히 “디스크가 부족하다”가 아니라, Docker가 레이어와 캐시를 쌓는 방식, 빌드 컨텍스트가 전송되는 방식, 그리고 패키지 매니저 캐시가 레이어에 고착되는 방식이 겹쳐서 발생하는 경우가 대부분입니다.
이 글에서는 원인을 빠르게 구분하는 체크 방법부터, Dockerfile 레이어 구조 최적화, BuildKit 캐시 마운트 활용, 멀티스테이지 빌드로 산출물만 남기는 방법, 그리고 CI에서 캐시를 “살리되 비대해지지 않게” 운영하는 전략까지 정리합니다.
증상부터 분류하기: 어디의 공간이 부족한가
no space left on device는 크게 세 군데에서 발생합니다.
- Docker 데이터 디렉터리(예:
/var/lib/docker)가 꽉 찬 경우 - 빌드 컨테이너 내부 파일시스템(overlay2 상의 레이어)이 비대해진 경우
- 빌드 컨텍스트 전송 자체가 너무 커서(예: 수 GB) 중간에 터지는 경우
먼저 호스트에서 Docker가 실제로 얼마나 먹고 있는지 봅니다.
docker system df
이미지, 컨테이너, 볼륨, 빌드 캐시가 각각 얼마나 차지하는지 한 눈에 나옵니다. 특히 BuildKit을 쓰는 환경에서는 Build Cache가 생각보다 빠르게 커집니다.
디스크가 어디서 찼는지 더 정확히 보려면:
docker system df -v
여기서 특정 이미지가 레이어를 과도하게 쌓았는지, dangling 이미지가 많은지, 오래된 빌드 캐시가 남아있는지 확인할 수 있습니다.
가장 흔한 원인 1: 빌드 컨텍스트가 너무 큼
Docker 빌드는 “현재 디렉터리”를 빌드 데몬으로 전송하는데, 이 전송 단위를 빌드 컨텍스트라고 합니다. 컨텍스트에 node_modules, dist, .git, 대용량 로그, 테스트 데이터가 포함되면 빌드 시작부터 디스크와 시간 모두가 낭비됩니다.
.dockerignore는 공간 문제의 1순위 해결책
다음은 Node.js 프로젝트에서 자주 쓰는 예시입니다.
node_modules
.next
.dist
build
coverage
.git
.gitignore
Dockerfile*
README.md
*.log
.env
핵심은 “이미지에 들어갈 필요가 없는 것”을 최대한 빼는 것입니다. 특히 node_modules를 컨텍스트에서 제외하면, 빌드 중복과 디스크 폭증을 크게 줄일 수 있습니다.
가장 흔한 원인 2: 레이어가 커지고, 지워도 안 줄어듦
Docker 레이어는 “추가된 파일”을 기록합니다. 중요한 함정은 다음과 같습니다.
- 한 레이어에서 큰 파일을 만들고
- 다음 레이어에서 그 파일을 삭제해도
이전 레이어의 용량은 그대로 남습니다.
즉, RUN apt-get install ...로 패키지 캐시가 생겼는데 다음 레이어에서 지워도, 이미 한 번 커진 레이어는 줄지 않습니다.
잘못된 예: 레이어를 쪼개서 캐시가 고착됨
FROM ubuntu:22.04
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
apt-get update 결과와 패키지 인덱스가 레이어로 남을 수 있고, 삭제도 다른 레이어에서 일어나면 효과가 없습니다.
개선 예: 한 레이어에서 설치와 정리를 끝내기
FROM ubuntu:22.04
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
- 설치와 정리를 같은
RUN에 묶어 레이어에 “정리된 상태”만 남깁니다. --no-install-recommends로 불필요한 패키지를 줄입니다.
BuildKit을 켜면 해결되는 문제가 많다
BuildKit은 빌드 성능뿐 아니라 “캐시를 더 똑똑하게 쓰는 방법”을 제공합니다. 특히 RUN --mount=type=cache는 패키지 매니저 캐시를 레이어에 남기지 않고, 빌드 캐시 영역에 분리해 저장합니다.
BuildKit 활성화는 보통 다음처럼 합니다.
DOCKER_BUILDKIT=1 docker build -t myapp:local .
예시: apt 캐시를 레이어 밖으로 빼기
# syntax=docker/dockerfile:1.7
FROM ubuntu:22.04
RUN \
apt-get update \
&& apt-get install -y --no-install-recommends curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
이 방식은 다음 효과가 있습니다.
- 빌드 재실행 시 다운로드 재사용으로 빠름
- 레이어 자체는 깔끔하게 유지되어 이미지가 덜 비대해짐
Node.js, Python도 비슷하게 적용 가능합니다.
예시: npm 캐시 마운트
# syntax=docker/dockerfile:1.7
FROM node:20-bookworm AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN \
npm ci
COPY . .
RUN npm run build
멀티스테이지 빌드로 “빌드 도구”를 최종 이미지에서 제거
no space left는 빌드 중에도 터지지만, 최종 이미지가 커서 레지스트리 저장소나 노드 디스크를 압박하면서 운영 장애로 이어지기도 합니다. 멀티스테이지 빌드는 이 문제를 가장 확실하게 줄입니다.
Node.js 예시: 빌드 산출물만 런타임으로 복사
# syntax=docker/dockerfile:1.7
FROM node:20-bookworm AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-bookworm-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY /app/dist ./dist
COPY /app/package.json ./package.json
# 런타임 의존성만 설치하거나, 더 나아가 번들링 전략을 사용
RUN npm install --omit=dev
CMD ["node", "dist/index.js"]
포인트는 다음입니다.
- 빌드 단계에만 필요한 툴체인, 캐시, 중간 산출물을 최종 이미지에 포함하지 않음
- 최종 이미지가 작아져 디스크 압박과 배포 시간 감소
멀티아키텍처 빌드까지 얹으면 빌드 캐시가 더 커질 수 있는데, 이 경우 빌드 전략과 캐시 정책이 중요합니다. 필요하다면 Docker buildx 멀티아키 이미지 exec format error 해결 글도 함께 참고하면 좋습니다.
레이어 캐시를 “잘 타게” 만드는 Dockerfile 구조
디스크 문제의 반대편에는 “캐시 미스” 문제가 있습니다. 캐시가 자주 깨지면 매번 패키지 재설치가 일어나고, 그 과정에서 캐시와 레이어가 더 빠르게 쌓이며 CI 디스크를 갉아먹습니다.
다음 원칙을 지키면 캐시 효율이 좋아집니다.
- 변경이 적은 파일을 먼저 COPY
- 의존성 설치를 먼저 수행
- 소스 코드는 그 다음에 COPY
예를 들어 Node.js에서 COPY . .를 먼저 해버리면, 소스 한 줄 바뀔 때마다 npm ci 레이어가 매번 다시 실행됩니다.
캐시/레이어 정리: 안전한 청소 순서
빌드가 터졌을 때 급한 불을 끄려면 정리가 필요합니다. 다만 무작정 지우면 다른 프로젝트 빌드 캐시까지 날아가 빌드 시간이 급증할 수 있습니다.
1) 사용하지 않는 것부터 정리
docker image prune
dangling 이미지만 제거합니다.
2) 빌드 캐시만 정리
docker builder prune
BuildKit 캐시가 원인일 때 효과적입니다.
좀 더 강하게:
docker builder prune --all
3) 최후의 수단: 전부 정리
docker system prune -a
- 사용하지 않는 이미지까지 제거합니다.
- CI 러너나 개발 머신에서 신중히 사용하세요.
디스크가 꽉 찼을 때 시스템이 불안정해지고, 다른 프로세스에도 영향을 줄 수 있습니다. 리눅스에서 메모리뿐 아니라 디스크 압박이 OOM 상황과 함께 연쇄 장애를 만들기도 하니, 장애 대응 관점에서는 리눅스 OOM Killer로 프로세스 죽음 진단·방지 글의 체크리스트도 같이 보면 좋습니다.
CI에서 특히 자주 터지는 이유와 운영 팁
CI 러너는 다음 특성을 갖습니다.
- 여러 저장소 빌드가 같은 머신에서 돌며 캐시가 누적됨
- 워크스페이스가 커지고, 도커 캐시도 커짐
- 병렬 빌드로 레이어 다운로드와 생성이 동시에 발생
운영 팁 1: 캐시 상한을 둔다
BuildKit 캐시는 무한정 커질 수 있으니, 주기적으로 정리하거나 상한을 둡니다.
예를 들어 주기 작업으로:
docker builder prune --filter until=168h
최근 7일보다 오래된 캐시를 정리합니다.
운영 팁 2: 레지스트리 캐시를 쓰되, 범위를 좁힌다
buildx의 캐시 내보내기와 가져오기를 쓰면 빌드 속도는 빨라지지만, 캐시 자체가 거대해질 수 있습니다. 프로젝트 단위로 캐시 키를 분리하고, 브랜치별 캐시를 무분별하게 만들지 않는 것이 중요합니다.
운영 팁 3: 빌드 산출물 경로를 통제한다
테스트가 만들어내는 아티팩트(예: coverage, e2e 스냅샷)가 컨텍스트에 포함되면 디스크가 급격히 증가합니다. .dockerignore와 CI 아티팩트 경로를 분리하세요.
트러블슈팅 체크리스트
다음 순서로 보면 원인 파악이 빠릅니다.
docker system df -v로 무엇이 큰지 확인.dockerignore로 컨텍스트 크기 줄이기- Dockerfile에서 설치와 정리를 같은
RUN에 묶기 - 멀티스테이지 빌드로 최종 이미지에서 빌드 도구 제거
- BuildKit 캐시 마운트로 패키지 캐시를 레이어 밖으로 빼기
- CI에서는
docker builder prune --filter until=...같은 정책으로 캐시 상한 관리
마무리: 공간 최적화는 “지우기”보다 “쌓이지 않게”
no space left on device를 만났을 때 가장 쉬운 해결은 prune이지만, 근본 해결은 “디스크를 먹는 구조”를 바꾸는 것입니다. 빌드 컨텍스트를 줄이고, 레이어를 덜 만들고, 캐시를 레이어 밖으로 분리하고, 최종 이미지에는 런타임에 필요한 것만 남기는 방향으로 Dockerfile을 정리하면, 빌드 실패도 줄고 배포 속도도 같이 좋아집니다.
특히 멀티스테이지 빌드와 BuildKit 캐시 마운트는 체감 효과가 커서, CI 환경에서 반복적으로 터지는 no space left를 장기적으로 안정화하는 데 가장 좋은 조합입니다.