- Published on
Docker 빌드가 느릴 때 BuildKit 캐시 깨짐 복구
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론
Docker 빌드가 평소엔 30초~2분 안에 끝났는데, 어느 날부터 RUN npm ci나 apt-get 단계가 매번 처음부터 다시 돌고 전체 빌드가 10분 이상 걸린다면 대부분은 “캐시가 안 타는 상태”입니다. 특히 Docker BuildKit을 쓰는 환경에서는 캐시가 단순히 “없다”기보다, 캐시 메타데이터가 꼬였거나(깨짐), 캐시 키가 매번 달라져서(무효화), 또는 원격 캐시/GC 정책 때문에 증발하는 경우가 많습니다.
이 글에서는 다음을 목표로 합니다.
- BuildKit 캐시가 깨졌을 때의 전형적인 증상 구분
- 로컬/CI 환경에서 캐시를 **안전하게 정리(prune)**하고 **복구(rebuild)**하는 절차
- Dockerfile을 캐시 친화적으로 바꿔 “다시는 느려지지 않게” 만드는 패턴
- 원격 캐시(registry/GHA cache 등)로 캐시를 지속시키는 실전 설정
CI/CD 최적화 관점은 모놀리포 빌드 캐시와도 닮아 있습니다. GitHub Actions에서 캐시를 살리는 전략은 GitHub Actions로 Nx 모놀리포 CI/CD 최적화 글과도 결이 같습니다.
1) BuildKit 캐시가 “깨졌을 때” 나타나는 증상
캐시 문제는 크게 3종류로 나뉩니다.
1-1. 캐시가 아예 적용되지 않음(항상 재실행)
- 로그에서
CACHED가 거의 안 보임 - 같은 커밋, 같은 명령인데도 매번
RUN단계가 실행 COPY . .이후 단계가 전부 재실행(전형적인 캐시 무효화)
1-2. 캐시가 있었는데 갑자기 빌드가 느려짐(캐시 손실/GC)
- 이전까지는 빨랐는데 어느 순간부터 느려짐
- CI에서 runner가 바뀌거나, 디스크 압박으로 GC가 발생
docker builder prune같은 정리 작업이 주기적으로 돌고 있었음
1-3. 캐시 메타데이터 꼬임(“깨짐”에 가까움)
- 빌드 도중
failed to solve류의 오류가 간헐적으로 발생 - 특정 스텝에서만 캐시가 비정상적으로 무시되거나, 캐시 hit가 불안정
- builder 인스턴스/스토어가 여러 개 섞여 혼선
BuildKit은 내부적으로 content-addressable storage와 메타데이터를 조합해 캐시를 관리합니다. 이때 builder 인스턴스가 바뀌거나(예: docker build vs buildx build), 드라이버가 다르거나(docker driver vs containerd), 또는 GC로 일부 레이어만 날아가면 “있는 듯 없는” 상태가 됩니다.
2) 먼저 확인할 것: BuildKit 사용 여부와 builder 상태
2-1. BuildKit 활성화 확인
# BuildKit 활성화(세션 단위)
DOCKER_BUILDKIT=1 docker build -t myapp:dev .
# buildx 사용 시
docker buildx version
Docker Desktop/최신 Docker에서는 기본적으로 BuildKit이 켜져 있는 경우가 많지만, CI나 서버는 다릅니다.
2-2. 현재 builder 목록/활성 builder 확인
docker buildx ls
여기서 중요한 포인트:
*표시된 builder가 현재 사용 중- builder가 여러 개면 캐시가 builder마다 분리될 수 있음
docker드라이버 vsdocker-container드라이버에 따라 캐시 저장 위치/동작이 다름
2-3. 빌드 로그에서 캐시 힌트 보기
docker buildx build --progress=plain -t myapp:dev .
--progress=plain은 캐시가 왜 안 타는지 추적하는 데 필수입니다.
3) “캐시 깨짐” 복구의 정석: 안전한 정리와 재구성
캐시 복구는 무작정 system prune -a부터 치면 이미지/컨테이너까지 다 날려서 업무가 멈출 수 있습니다. BuildKit 캐시만 목표로 단계적으로 접근하세요.
3-1. BuildKit 캐시만 정리(prune)
# 현재 builder의 BuildKit 캐시 정리
docker buildx prune
# 사용되지 않는 캐시 전부 제거(주의)
docker buildx prune --all
# 24시간 이상 된 것만 제거(안전장치)
docker buildx prune --filter until=24h
- “느려졌다” 수준이면 보통
--filter until=...로 시작하는 게 안전합니다. - 캐시가 깨진 느낌(에러/불안정)이면
--all로 한 번 깨끗이 밀고 재구축이 더 빠를 때도 많습니다.
3-2. builder 자체를 새로 만들기(메타데이터 꼬임 대응)
builder 메타데이터가 꼬인 경우, prune만으로는 해결이 안 될 수 있습니다.
# 새 builder 생성
docker buildx create --name freshbuilder --use
# 부트스트랩(드라이버에 따라 필요)
docker buildx inspect --bootstrap
# 빌드 수행
docker buildx build --progress=plain -t myapp:dev .
기존 builder를 제거하고 싶다면:
docker buildx rm oldbuilder
3-3. Docker 데몬 레벨 정리(system prune)는 마지막 카드
# 매우 강력: 중지된 컨테이너/네트워크/빌드 캐시 등 정리
docker system prune
# 이미지까지 대량 삭제(주의)
docker system prune -a
이 단계는 “로컬 개발 머신”에서 특히 위험합니다. 팀 공용 서버/빌드 머신에서는 정책적으로 정리되기도 하니, 캐시가 자주 날아가는 원인이 될 수 있습니다.
4) 캐시가 계속 깨지는/안 타는 진짜 원인 7가지
캐시를 복구했는데도 다시 느려진다면, 대부분은 Dockerfile/빌드 컨텍스트가 캐시를 깨고 있습니다.
4-1. COPY . .가 너무 이른 위치에 있음
가장 흔한 패턴입니다. 소스 한 줄만 바뀌어도 이후 레이어가 전부 무효화됩니다.
나쁜 예
FROM node:20
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
좋은 예(의존성 레이어 분리)
FROM node:20
WORKDIR /app
# 의존성 파일만 먼저 복사
COPY package.json package-lock.json ./
RUN npm ci
# 그 다음 소스 복사
COPY . .
RUN npm run build
4-2. .dockerignore가 부실해서 컨텍스트가 계속 바뀜
빌드 컨텍스트에 node_modules, dist, .git가 포함되면 파일 변경이 캐시 키를 흔듭니다.
node_modules
.git
dist
.next
coverage
*.log
4-3. apt-get update가 매번 달라지는 네트워크 의존 단계
패키지 인덱스는 시간이 지나면 바뀌므로 캐시가 있어도 재현성이 떨어집니다.
- 가능한 한 패키지 설치를 한 레이어에 묶고
- BuildKit cache mount로 다운로드 캐시를 살립니다.
# syntax=docker/dockerfile:1.6
FROM ubuntu:22.04
RUN \
apt-get update && apt-get install -y curl ca-certificates && rm -rf /var/lib/apt/lists/*
4-4. ARG/ENV가 자주 바뀌어 캐시 키를 무효화
예: ARG BUILD_DATE, ARG GIT_SHA를 너무 이른 단계에 넣으면 이후 레이어가 전부 깨집니다.
ARG GIT_SHA
LABEL org.opencontainers.image.revision=$GIT_SHA
이런 메타 정보는 가능한 한 마지막에 두세요.
4-5. 멀티스테이지에서 “캐시 공유”가 안 되는 빌드 방식
docker build와 docker buildx build를 섞거나, CI에서 매번 다른 builder를 쓰면 캐시가 끊깁니다. 빌드 도구를 통일하고, 원격 캐시를 붙이세요.
4-6. 병렬 빌드/동시 실행으로 캐시 경합
동일 머신에서 여러 빌드가 동시에 돌아가면 캐시가 급격히 커지고 GC가 자주 발생해 체감상 “캐시가 깨지는” 현상이 생깁니다.
- 빌드 동시성 제한
- 캐시 크기/보관 정책 조정
4-7. 디스크 부족으로 인한 GC(자동 정리)
빌드 머신 디스크가 부족하면 Docker/BuildKit이 캐시를 공격적으로 정리합니다. 이 경우는 “복구”가 아니라 “환경 개선”이 필요합니다.
이런 운영 이슈는 쿠버네티스에서 장애를 진단하는 방식과 비슷하게, 징후를 빠르게 좁혀나가야 합니다. 컨테이너가 계속 재시작되는 상황의 진단 흐름은 Kubernetes CrashLoopBackOff 10가지 원인과 15분 진단도 참고할 만합니다.
5) BuildKit 캐시를 “복구”가 아니라 “지속”시키기: 원격 캐시
로컬에서는 캐시가 남아도, CI는 runner가 매번 새로 뜨면 캐시가 매번 0부터 시작합니다. BuildKit의 강점은 **원격 캐시(export/import)**입니다.
5-1. Registry 캐시(가장 범용)
docker buildx build \
--cache-to=type=registry,ref=ghcr.io/myorg/myapp:buildcache,mode=max \
--cache-from=type=registry,ref=ghcr.io/myorg/myapp:buildcache \
-t ghcr.io/myorg/myapp:latest \
--push \
.
mode=max는 캐시를 더 풍부하게 저장하지만 용량이 커질 수 있습니다.--push를 해야 registry에 캐시도 함께 올라갑니다(환경에 따라 다름).
5-2. GitHub Actions 캐시(type=gha)
docker buildx build \
--cache-to=type=gha,mode=max \
--cache-from=type=gha \
-t myapp:ci \
.
GHA 캐시는 워크플로우/브랜치 정책 영향을 받으니, 캐시 키 전략을 함께 설계해야 합니다. 전반적인 CI 캐시 설계는 GitHub Actions로 Nx 모놀리포 CI/CD 최적화에서 말하는 “캐시가 깨지는 조건”과 유사합니다.
6) “캐시가 깨졌다”를 재현 가능하게 만드는 디버깅 체크리스트
아래 순서로 보면 원인 파악이 빨라집니다.
- 빌드 도구 통일:
docker buildx build로 통일하고--progress=plain으로 확인 - builder 확인:
docker buildx ls에서 동일 builder 사용 여부 - 컨텍스트 크기 확인:
.dockerignore적용 후Sending build context크기가 줄었는지 - 캐시 무효화 지점 찾기: 로그에서 처음으로
CACHED가 끊기는 스텝이 어디인지 - 네트워크 의존 단계 개선: apt/pip/npm 다운로드는 cache mount 사용
- 캐시 정리/재생성:
buildx prune→ builder 재생성 - 원격 캐시 도입: CI에서 캐시가 매번 0이면 복구가 아니라 “저장소”가 필요
7) 실전 예제: Node.js 프로젝트 Dockerfile을 캐시 친화적으로 리팩터링
아래는 BuildKit 기능을 적극 활용한 예시입니다.
# syntax=docker/dockerfile:1.6
FROM node:20-slim AS deps
WORKDIR /app
# 의존성 설치 캐시 최적화
COPY package.json package-lock.json ./
RUN \
npm ci
FROM node:20-slim AS build
WORKDIR /app
COPY /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:20-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY /app/dist ./dist
COPY package.json ./
CMD ["node", "dist/server.js"]
핵심:
package*.json만 먼저 복사해 의존성 레이어를 고정--mount=type=cache로 npm 다운로드 캐시를 유지- 빌드 산출물만 런타임 이미지로 복사해 이미지 크기/변경 범위를 최소화
결론
Docker 빌드가 느려졌을 때 “BuildKit 캐시가 깨졌다”는 말은 보통 다음 중 하나입니다.
- 캐시 저장소가 GC/정리로 사라졌다 → 원격 캐시 도입/디스크 정책 개선
- Dockerfile/컨텍스트가 캐시 키를 계속 무효화한다 →
COPY순서,.dockerignore,ARG위치 재설계 - builder 메타데이터가 꼬였다 →
buildx prune및 builder 재생성
복구의 핵심은 (1) 어디서 캐시가 끊기는지 로그로 찾고, (2) 캐시를 정리/재구성하며, (3) 캐시가 지속되도록 원격 캐시를 붙이는 것입니다. 이 3단계를 갖추면 “갑자기 느려지는 Docker 빌드”는 대부분 재발을 막을 수 있습니다.