- Published on
Docker BuildKit 캐시 누수로 빌드 느림 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버나 CI에서 Docker 빌드가 “처음엔 빠르다가 시간이 갈수록 느려지는” 패턴을 보이면, 단순히 네트워크나 레지스트리 문제만 의심하기 쉽습니다. 하지만 실제로는 BuildKit 캐시가 누적되며 디스크 공간, inode, 레이어 메타데이터, 그리고 빌드 그래프가 비대해져 빌드가 점점 느려지는 경우가 많습니다.
이 글은 BuildKit 캐시가 왜 “누수처럼” 느껴지는지, 어떤 지표로 확인하는지, 그리고 안전하게 정리하면서도 캐시 효율을 유지하는 운영 방법을 정리합니다.
관련 이슈로 디스크가 꽉 찼는데 삭제해도 안 줄어드는 상황도 함께 엮여 나타날 수 있으니, 필요하면 리눅스 디스크 100%인데 삭제해도 안 줄 때도 같이 확인해두면 진단 속도가 빨라집니다.
BuildKit 캐시가 “누수”처럼 보이는 이유
BuildKit은 전통적인 docker build보다 훨씬 공격적으로 캐시를 활용합니다.
- 레이어 캐시뿐 아니라 빌드 중간 산출물, 다운로드한 의존성,
RUN --mount=type=cache로 만든 캐시 디렉터리까지 저장 - 멀티스테이지 빌드, 병렬 실행, 빌드 그래프 최적화로 인해 “재사용 가능한 조각”이 많이 생김
- CI에서 매 빌드마다 다른 태그, 다른 빌드 아규먼트, 다른 커밋 컨텍스트가 들어오면 캐시 키가 달라져 오래된 캐시가 계속 남음
즉, 엄밀히 말해 메모리 누수는 아니지만 “정리 정책이 없으면 계속 쌓이는 캐시”라서 운영 관점에서는 누수처럼 체감됩니다.
증상 체크리스트: 느려진 빌드가 캐시 때문인지
다음 증상이 함께 보이면 BuildKit 캐시 누적 가능성이 큽니다.
- 빌드 시간이 점진적으로 증가한다
docker build자체는 캐시 히트가 나는데도 I/O가 느리다- 디스크 사용량이 꾸준히 증가한다
- CI 워커에서 간헐적으로
no space left on device가 난다 - inode 부족, 파일 핸들 부족 같은 시스템 에러가 동반된다
특히 파일 핸들 제한 문제는 캐시 디렉터리와 많은 작은 파일을 다룰 때 겹칠 수 있습니다. 필요하면 리눅스 Too many open files 즉시 진단·해결도 같이 보세요.
1단계: 현재 BuildKit/빌더 상태 확인
빌더 인스턴스 확인
docker buildx ls
- 여러 builder가 남아 있으면 그만큼 캐시 저장소가 중복될 수 있습니다.
- 사용하지 않는 builder가 오래 남아 있으면 정리 대상입니다.
BuildKit 캐시 사용량 확인
Buildx를 쓰는 경우 아래가 가장 직관적입니다.
docker buildx du
출력에서 builder별로 캐시가 얼마나 쌓였는지 확인합니다.
추가로 Docker 전체 관점에서는 다음도 유용합니다.
docker system df
여기서 Build Cache가 비정상적으로 크면 캐시 정리로 체감 개선이 나는 경우가 많습니다.
2단계: 안전한 정리(Prune) 전략
정리는 크게 두 가지 축이 있습니다.
- “지금 당장 공간 확보”가 목적이면 강하게
- “캐시 효율 유지”가 목적이면 정책 기반으로
가장 안전한 기본: 오래된 캐시만 정리
docker builder prune --filter "until=168h" -f
- 최근 7일(
168h) 내에 사용된 캐시는 남기고, 오래된 캐시만 정리합니다. - 운영 서버나 공유 CI 워커에서 권장되는 기본값입니다.
buildx 환경에서 특정 builder만 정리
docker buildx prune --builder default --filter "until=168h" -f
- 여러 builder를 쓰는 환경에서 “문제 있는 builder만” 부분 정리할 수 있습니다.
급한 불 끄기: 전체 캐시 제거
docker builder prune -a -f
-a는 사용되지 않는 모든 캐시를 제거합니다.- 다음 빌드가 느려질 수 있지만, 디스크가 임계치에 도달했을 때는 효과가 즉각적입니다.
이미지/컨테이너까지 같이 정리(주의)
docker system prune -f
더 강하게 하려면 아래처럼 볼륨까지 포함할 수 있지만, 데이터 유실 위험이 큽니다.
docker system prune --volumes -f
- CI 전용 워커처럼 “상태가 없어도 되는 머신”에서만 고려하세요.
3단계: “캐시가 쌓이는 구조” 자체를 줄이기
정리만 주기적으로 해도 해결되지만, 근본적으로는 캐시가 과도하게 분기되는 원인을 줄이는 게 빌드 성능에 더 좋습니다.
Dockerfile 레이어 캐시가 깨지는 흔한 원인
COPY . .가 너무 이르게 등장해 작은 코드 변경에도 의존성 설치 레이어가 매번 무효화됨- 빌드 아규먼트가 자주 바뀌며 캐시 키를 바꿈
RUN apt-get update가 캐시를 매번 새로 만들며 레이어가 비대해짐
예시: Node.js에서 의존성 캐시를 살리는 구조
# syntax=docker/dockerfile:1.7
FROM node:20-alpine AS deps
WORKDIR /app
# 의존성 파일만 먼저 복사
COPY package.json package-lock.json ./
# npm 캐시 마운트로 다운로드 재사용
RUN \
npm ci
FROM node:20-alpine AS build
WORKDIR /app
COPY /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:20-alpine AS runtime
WORKDIR /app
COPY /app/dist ./dist
CMD ["node", "dist/server.js"]
핵심은 다음입니다.
- 의존성 설치 단계에서 소스 전체를 복사하지 않기
RUN --mount=type=cache로 “다운로드 캐시”를 재사용하되, 그 캐시가 무한정 쌓이지 않도록 prune 정책을 운영에 포함하기
멀티 아키텍처 빌드에서 캐시 폭증 주의
buildx로 멀티아키텍처 이미지를 만들면 아키텍처별 캐시가 분리되어 저장되면서 캐시가 빨리 커질 수 있습니다. 멀티아키 빌드 자체 이슈가 섞여 exec format error 같은 문제로 디버깅이 길어지기도 하니, 멀티아키 운영 중이면 Docker buildx 멀티아키 이미지 exec format error 해결도 함께 점검해두면 좋습니다.
4단계: CI에서 재발 방지(운영 레시피)
1) 정리 작업을 주기적으로 스케줄링
예: 매일 새벽 3시에 14일 이상 캐시 정리
0 3 * * * /usr/bin/docker builder prune --filter "until=336h" -f >/var/log/docker-builder-prune.log 2>&1
- “매 빌드마다 prune”은 오히려 캐시 효율을 깨서 느려질 수 있습니다.
- 대신 주기적인 정책 기반 정리가 안정적입니다.
2) 디스크 임계치 기반으로 정리 트리거
간단한 셸 스크립트로 디스크 사용률이 80%를 넘으면 prune하도록 만들 수 있습니다.
#!/usr/bin/env bash
set -euo pipefail
THRESHOLD=80
USE_PCT=$(df -P / | awk 'NR==2 {gsub(/%/,"",$5); print $5}')
if [ "$USE_PCT" -ge "$THRESHOLD" ]; then
echo "Disk usage ${USE_PCT}% >= ${THRESHOLD}%, pruning build cache..."
docker builder prune --filter "until=168h" -f
else
echo "Disk usage ${USE_PCT}% < ${THRESHOLD}%, skip."
fi
3) 원격 캐시/레지스트리 캐시로 로컬 캐시 의존 줄이기
로컬 디스크가 작은 CI 워커라면 “로컬 캐시를 최대한 유지”하는 전략이 오히려 독이 될 수 있습니다. 그럴 때는 레지스트리 기반 캐시를 쓰면 워커 교체에도 캐시를 재사용할 수 있습니다.
docker buildx build \
--cache-to type=registry,ref=registry.example.com/myapp:buildcache,mode=max \
--cache-from type=registry,ref=registry.example.com/myapp:buildcache \
-t registry.example.com/myapp:latest \
--push \
.
mode=max는 캐시를 더 많이 저장해 히트율을 높이지만, 캐시 레포지토리 용량이 커질 수 있습니다.- 조직 정책에 맞춰 캐시 레포지토리의 라이프사이클 관리도 같이 설계하세요.
5단계: “정리했는데도” 디스크가 안 줄어들 때
캐시를 prune했는데 df 상 디스크가 줄지 않는다면, 다음을 의심합니다.
- 삭제된 파일을 어떤 프로세스가 계속 열고 있어 공간이 반환되지 않는 경우
- Docker 데몬/컨테이너가 로그 파일을 계속 붙잡고 있는 경우
이때는 lsof로 열린 삭제 파일을 찾는 방식이 정석입니다. 절차는 리눅스 디스크 100%인데 삭제해도 안 줄 때에서 그대로 따라가면 됩니다.
정리: 가장 효과가 큰 해결 순서
docker buildx du와docker system df로 캐시가 원인인지 수치로 확인docker builder prune --filter "until=..."로 오래된 캐시부터 안전하게 정리- Dockerfile 레이어 구조를 바꿔 캐시 분기 자체를 줄이기
- CI에는 주기적 prune와(또는) 레지스트리 캐시를 도입해 재발 방지
BuildKit 캐시는 잘 쓰면 빌드를 극적으로 빠르게 만들지만, “정리 정책이 없는 캐시”는 결국 성능 저하와 장애로 돌아옵니다. 캐시를 자산으로 만들려면, 관찰(du), 정책(prune), 설계(Dockerfile/remote cache) 3가지를 한 세트로 운영하는 것이 핵심입니다.