Published on

Docker 빌드가 느릴 때 - BuildKit 캐시 깨짐 해결

Authors

Docker 빌드가 어느 날부터 갑자기 5분, 10분씩 늘어지는 경우가 있습니다. 특히 DOCKER_BUILDKIT=1 를 켜놨는데도 매번 의존성 설치가 다시 돌고, 레이어가 전혀 재사용되지 않는다면 “BuildKit 캐시가 깨졌다” 라고 보는 게 가장 빠릅니다.

문제는 캐시가 깨지는 원인이 생각보다 다양하다는 점입니다. Dockerfile 한 줄 수정이 아니라, 빌드 컨텍스트, .dockerignore, CI 워커의 스토리지, 멀티스테이지 설계, ARG/ENV 위치, 심지어 타임스탬프가 섞인 파일 하나 때문에 전체가 무효화되기도 합니다.

이 글에서는 (1) 캐시가 실제로 깨졌는지 확인하는 방법, (2) BuildKit 캐시 키가 바뀌는 대표 패턴, (3) 로컬/CI에서 캐시를 “지속”시키는 설정, (4) 자주 쓰는 언어별(특히 Node/Python/Go) 개선 예시를 코드로 정리합니다.

관련해서 캐시 무효화 원인을 더 폭넓게 정리한 글은 Docker 빌드 캐시가 무효화되는 원인 7가지 도 같이 참고하면 좋습니다. CI에서 레이어 캐시 복구 관점은 Jenkins 빌드가 갑자기 느릴 때 Docker 레이어 캐시 복구 도 연결됩니다.

1) 먼저: 정말 BuildKit 캐시가 안 먹는지 확인

빌드 로그에서 캐시 히트 여부 보기

BuildKit은 캐시가 맞으면 CACHED 로 표시됩니다.

DOCKER_BUILDKIT=1 docker build --progress=plain -t myapp:dev .

로그에서 각 스텝이 CACHED 가 아니라 매번 RUN ... 을 실행하고 있다면 캐시 키가 바뀌고 있는 겁니다.

빌더/캐시 저장소 상태 점검

로컬에서 Docker Desktop 업데이트 이후, 또는 빌더 인스턴스가 바뀌면서 캐시가 사라지는 경우가 있습니다.

docker buildx ls
docker buildx inspect --bootstrap

빌더가 docker 드라이버인지, docker-container 인지에 따라 캐시 저장 위치/지속성이 달라집니다. CI에서는 특히 docker-container 빌더를 새로 만들면 내부 캐시가 초기화됩니다.

2) BuildKit 캐시가 깨지는 “실전” 원인들

캐시가 깨지는 원인은 결국 “해당 스텝의 입력이 바뀌었기 때문”입니다. 입력에는 소스 코드뿐 아니라, 빌드 컨텍스트에 포함된 파일, ARG 값, 베이스 이미지 digest, 네트워크로 내려받는 아티팩트 등이 포함됩니다.

2.1 빌드 컨텍스트가 불필요하게 커짐 (가장 흔함)

예를 들어 COPY . . 를 하는데, 실제로는 빌드에 필요 없는 파일(로그, 테스트 결과, .git, 로컬 캐시)이 컨텍스트에 포함되면, 파일 하나만 바뀌어도 COPY 레이어가 매번 달라지고 이후 레이어까지 연쇄적으로 무효화됩니다.

해결은 .dockerignore 를 “정교하게” 만드는 겁니다.

.git
node_modules
__pycache__
.pytest_cache
.dist
build
coverage
*.log
.env
.DS_Store

핵심은 COPY 를 최대한 “입력 범위가 작은 단위”로 쪼개는 것과 .dockerignore 로 컨텍스트 변동을 줄이는 것입니다.

2.2 ARG/ENV 위치가 캐시를 계속 깨뜨림

다음 패턴은 매우 흔합니다.

ARG BUILD_SHA
ENV BUILD_SHA=$BUILD_SHA
RUN npm ci

BUILD_SHA 가 커밋마다 바뀌면 ENV 가 바뀌고, 그 아래 모든 레이어가 무효화됩니다. BUILD_SHA 는 보통 런타임에서만 필요하거나, 최종 이미지에만 박아도 됩니다.

개선 예:

RUN npm ci
ARG BUILD_SHA
ENV BUILD_SHA=$BUILD_SHA

즉, 변동성이 큰 ARG 는 가능한 한 아래로 내리세요.

2.3 의존성 설치 전 COPY . . 를 해버림

Node/Python/Go 모두 동일한 실수입니다. 소스가 조금만 바뀌어도 의존성 설치 레이어가 매번 다시 실행됩니다.

나쁜 예(자주 보임):

COPY . .
RUN npm ci

좋은 예(의존성 파일만 먼저 복사):

COPY package.json package-lock.json ./
RUN npm ci
COPY . .

Python이라면:

COPY pyproject.toml poetry.lock ./
RUN poetry install --no-root
COPY . .

2.4 베이스 이미지가 계속 바뀜 (latest 태그)

FROM node:latest 같은 태그는 빌드 때마다 다른 digest를 가리킬 수 있어 캐시가 무력화됩니다.

가능하면 버전 태그를 고정하거나 digest pinning을 고려하세요.

FROM node:20.11.1-bookworm-slim

2.5 CI에서 “캐시 저장소” 자체가 매번 초기화

GitHub Actions, GitLab CI, Jenkins ephemeral agent 등에서 워커가 매번 새로 뜨면 로컬 레이어 캐시는 존재하지 않습니다. 이 경우 BuildKit의 cache-to / cache-from 를 써서 원격 캐시를 저장/복원해야 합니다.

3) BuildKit 캐시를 지속시키는 핵심: cache-to / cache-from

BuildKit은 레이어 캐시를 로컬에만 두지 않고, 레지스트리나 파일로 내보내고(import)할 수 있습니다. CI에서 가장 효과가 큰 방법입니다.

3.1 레지스트리 기반 캐시(가장 실용적)

다음은 buildx 로 레지스트리에 캐시를 저장하는 예시입니다.

docker buildx build \
  --builder mybuilder \
  --progress=plain \
  --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:sha-123 \
  --push \
  .

포인트:

  • mode=max 는 가능한 많은 캐시 메타데이터를 저장합니다(대부분의 CI에서 체감이 큼).
  • cache-from 을 반드시 같이 지정해야 다음 빌드에서 가져옵니다.

3.2 GitHub Actions에서 docker/build-push-action 로 적용

name: build
on:
  push:
    branches: [ main ]

jobs:
  docker:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4

      - uses: docker/setup-buildx-action@v3

      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ghcr.io/myorg/myapp:${{ github.sha }}
          cache-from: type=registry,ref=ghcr.io/myorg/myapp:buildcache
          cache-to: type=registry,ref=ghcr.io/myorg/myapp:buildcache,mode=max
          provenance: false

provenance 는 환경에 따라 메타데이터 생성이 추가 비용이 될 수 있어, 필요 없으면 끄는 편이 빌드 시간을 줄이는 데 도움이 됩니다(정책/보안 요구사항이 있으면 켜세요).

4) Dockerfile을 BuildKit 친화적으로 바꾸는 테크닉

4.1 RUN --mount=type=cache 로 패키지 매니저 캐시 살리기

BuildKit의 강점은 “레이어 캐시” 외에도 “마운트 캐시”를 제공한다는 점입니다. 의존성 설치가 잦은 프로젝트에서 체감이 큽니다.

Node (npm)

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

COPY package.json package-lock.json ./

RUN --mount=type=cache,target=/root/.npm \
    npm ci

COPY . .
RUN npm run build

Python (pip)

# syntax=docker/dockerfile:1.7
FROM python:3.12-slim AS build
WORKDIR /app

COPY requirements.txt ./

RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

COPY . .

--mount=type=cache 는 레이어를 더럽히지 않으면서 다운로드 캐시만 유지합니다. 단, CI에서 빌더가 매번 새로 생성되면 이 캐시는 빌더 내부에만 남으므로, 앞에서 설명한 원격 캐시 전략과 함께 쓰는 게 가장 좋습니다.

4.2 멀티스테이지에서 “변동 레이어”를 뒤로 미루기

빌드 산출물만 런타임 이미지로 복사하면 최종 이미지 레이어 변동이 줄고, 보안/용량도 좋아집니다.

# syntax=docker/dockerfile:1.7
FROM node:20-bookworm-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN npm run build

FROM nginx:1.27-alpine AS runtime
COPY --from=build /app/dist /usr/share/nginx/html

여기서도 COPY . . 는 최대한 뒤로, 그리고 빌드에 필요한 파일만 복사하도록 조정하면 캐시 안정성이 올라갑니다.

5) “캐시가 깨졌을 때” 빠르게 원인 찾는 체크리스트

5.1 어떤 스텝부터 캐시가 미스나는지 확인

--progress=plain 으로 빌드하고, 첫 번째로 CACHED 가 깨지는 스텝을 찾습니다. 대개 그 스텝의 입력이 원인입니다.

  • COPY . . 직후부터 깨진다: 컨텍스트/.dockerignore 문제 확률이 큼
  • RUN apt-get update 가 매번 돈다: 네트워크 의존 스텝을 묶는 방식/레이어 설계 문제
  • ARG 선언 이후부터 깨진다: ARG 값 변동 또는 위치 문제

5.2 컨텍스트에 “매번 바뀌는 파일”이 섞였는지 확인

대표적으로 다음이 범인입니다.

  • 빌드 산출물(dist, build)을 repo에 두고 같이 COPY
  • 테스트 리포트/커버리지 파일
  • 로컬에서 생성되는 .env 나 secret 파일

이런 파일은 .dockerignore 로 차단하거나, 애초에 빌드 컨텍스트 밖으로 빼세요.

5.3 CI 워커에서 캐시가 저장되는지 확인

  • 워커가 ephemeral이면 로컬 캐시는 기대하지 말 것
  • 원격 캐시(cache-to/cache-from)를 쓰는지 확인
  • 레지스트리 권한 문제로 캐시 push/pull 이 실패하면 매번 콜드 빌드가 됩니다

6) 실전 결론: 가장 효과 큰 처방 3가지

  1. .dockerignore 를 제대로 만들고 COPY 범위를 줄이기
  2. 의존성 설치 전에 의존성 파일만 먼저 COPY 하기
  3. CI에서는 BuildKit 원격 캐시를 반드시 구성하기(cache-to/cache-from)

이 3가지만 해도 “BuildKit을 켰는데도 느린 빌드”의 대부분은 해결됩니다. 그 다음 단계로 RUN --mount=type=cache 를 추가하면 네트워크 다운로드 비용까지 줄여서 체감이 더 커집니다.

빌드 캐시가 왜 무효화되는지 원인별로 더 깊게 보고 싶다면 Docker 빌드 캐시가 무효화되는 원인 7가지 를, CI에서 레이어 캐시를 복원하는 운영 팁은 Jenkins 빌드가 갑자기 느릴 때 Docker 레이어 캐시 복구 를 함께 보면 전체 그림이 더 빨리 잡힙니다.