Published on

Docker BuildKit 캐시 안먹힘 원인·해결 7가지

Authors

서론

Docker BuildKit을 켰는데도 CACHED가 거의 안 뜨고 매번 RUN 단계가 다시 실행된다면, 문제는 “BuildKit이 느리다”가 아니라 캐시 키가 계속 바뀌는 조건을 스스로 만들고 있는 경우가 대부분입니다. 특히 CI(GitHub Actions, Jenkins)에서는 러너가 매번 새로 뜨거나, 레지스트리/로컬 캐시를 제대로 내보내지 않아 “캐시가 존재하지 않는 것처럼” 보이기도 합니다.

이 글에서는 BuildKit 캐시가 안 먹는 대표 원인 7가지를 증상 → 원인 → 해결 형태로 정리하고, 현장에서 바로 적용 가능한 Dockerfile/CI 예제를 함께 제공합니다.

CI에서 인증/권한 이슈로 캐시 푸시 자체가 실패하는 경우도 많습니다. AWS/ECR 연동에서 403/AccessDenied가 섞여 보이면 GitHub Actions OIDC로 AWS 배포 403 해결 가이드도 같이 확인해보세요.


1) 빌드 컨텍스트가 매번 바뀜 (COPY . .의 함정)

증상

  • 소스 변경이 거의 없는데도 COPY . . 이후 단계가 전부 재빌드됨
  • RUN npm ci, RUN pip install 같은 “비싼 단계”가 계속 다시 실행됨

원인

BuildKit은 COPY/ADD 입력(파일 목록/내용/메타데이터)에 따라 캐시 키를 만듭니다. 그런데 빌드 컨텍스트에 아래가 포함되면 변경이 잦습니다.

  • node_modules/, .venv/, dist/, build/
  • .git/, *.log, 테스트 산출물
  • CI에서 생성되는 임시 파일

해결

  1. .dockerignore를 제대로 작성합니다.
  2. 의존성 설치 단계 앞에서는 의존성 정의 파일만 먼저 COPY합니다.
# syntax=docker/dockerfile:1.7
FROM node:20-alpine AS build
WORKDIR /app

# 1) 의존성 정의만 먼저 복사
COPY package.json package-lock.json ./

# 2) 캐시가 먹는 설치 단계
RUN --mount=type=cache,target=/root/.npm \
    npm ci

# 3) 소스는 나중에 복사
COPY . .
RUN npm run build

권장 .dockerignore 예시:

.git
node_modules
.vscode
*.log
dist
build
__pycache__
.venv
.env

2) ARG/ENV가 캐시를 무효화함 (특히 커밋 SHA, 빌드 시간)

증상

  • ARG GIT_SHA를 넣은 뒤부터 모든 레이어가 매번 재빌드
  • “버전 정보 주입”을 추가했더니 캐시가 사라짐

원인

ARG해당 ARG를 참조하는 지점부터 캐시 키에 영향을 줍니다. 아래처럼 빌드 초반에 ARG를 선언하고 여러 단계에서 사용하면, 커밋 SHA가 바뀔 때마다 캐시가 광범위하게 깨집니다.

해결

  • 변경이 잦은 ARG최대한 뒤로 미루고, 실제로 필요한 최소 단계에서만 사용합니다.
  • “버전 라벨”은 최종 이미지 메타데이터에만 적용해도 됩니다.
FROM alpine:3.19
WORKDIR /app

# (의존성 설치 등 캐시를 최대한 살리고)
COPY app ./app

# 변경 잦은 값은 마지막에
ARG GIT_SHA
LABEL org.opencontainers.image.revision=$GIT_SHA

CMD ["/app/server"]

3) 비결정적(Non-deterministic) 빌드: apt-get upgrade, apk update, 시간/랜덤

증상

  • 같은 커밋인데도 빌드 결과가 달라지거나 캐시가 일정치 않음
  • RUN apt-get update && apt-get install ... 단계가 자주 다시 실행되는 느낌

원인

패키지 저장소는 시간이 지나면 인덱스/패키지 버전이 바뀝니다. 또한 pip install이 최신 버전을 끌어오거나, 빌드 중에 날짜를 파일에 기록하면 캐시 재사용이 어려워집니다.

해결

  • 가능하면 베이스 이미지 태그를 고정(또는 digest pinning)
  • 패키지 버전 핀/락파일 사용
  • 불필요한 upgrade 지양
FROM ubuntu:22.04

RUN apt-get update \
 && apt-get install -y --no-install-recommends \
      ca-certificates curl \
 && rm -rf /var/lib/apt/lists/*

추가로, Python은 requirements.txt/poetry.lock 기반으로 설치를 고정하고, BuildKit 캐시 마운트를 활용하세요.

FROM python:3.12-slim
WORKDIR /app

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

COPY . .
CMD ["python", "main.py"]

4) 멀티스테이지에서 “캐시가 이어지지 않는” 구조

증상

  • 빌더 스테이지는 캐시가 먹는 것 같은데 최종 스테이지가 매번 다시 빌드
  • COPY --from=build가 매번 변경으로 인식됨

원인

멀티스테이지 자체가 문제라기보다, 빌더 스테이지 산출물이 매번 달라지는 경우입니다.

  • 빌드 산출물에 timestamp가 포함
  • npm run build가 해시/메타를 바꿔치기
  • go build-ldflags "-X main.version=$(date)" 같은 값 주입

해결

  • 산출물 생성 과정에서 시간/랜덤을 제거
  • 버전 주입은 런타임 환경변수로 옮기거나, 최종 단계 라벨로 제한

Go 예시(비결정적 ldflags 제거):

FROM golang:1.22 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
    go mod download
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build \
    CGO_ENABLED=0 go build -o /out/app ./cmd/app

FROM gcr.io/distroless/base-debian12
COPY --from=build /out/app /app
ENTRYPOINT ["/app"]

5) CI에서 캐시를 “저장/복원”하지 않음 (로컬 캐시 착시)

증상

  • 로컬에서는 캐시가 잘 먹는데 CI에서는 매번 풀빌드
  • GitHub Actions에서 docker build는 빨랐는데 buildx로 바꾸니 느려짐

원인

호스티드 러너는 보통 매 실행마다 깨끗합니다. 즉, BuildKit 캐시는 로컬 디스크에 남지 않습니다. 따라서 캐시를 레지스트리/gha/로컬 아카이브로 내보내고(import) 다시 가져와야 합니다.

해결 (GitHub Actions + buildx)

cache-from/cache-to를 설정합니다.

name: build
on: [push]

jobs:
  docker:
    runs-on: ubuntu-latest
    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/org/app:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

레지스트리 캐시를 쓰는 패턴도 흔합니다(여러 러너/툴에서 공유 시 유리).

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

6) --no-cache/--pull/베이스 이미지 변경으로 캐시가 깨짐

증상

  • 팀원이 “보안 업데이트”를 위해 --pull을 넣은 뒤부터 캐시 효율이 급락
  • CI 스크립트에 --no-cache가 숨어 있음

원인

  • --no-cache: 말 그대로 캐시를 무시
  • --pull: 베이스 이미지가 업데이트되면 해당 이후 레이어 캐시가 무효화
  • 베이스 이미지 태그가 floating(latest, alpine:3)이면 예기치 않게 바뀜

해결

  • 정기 보안 패치 목적이면 --pull을 유지하되, 기대치(캐시 히트율 하락)를 합의
  • 태그를 고정하거나 digest pinning
# 태그 대신 digest로 고정(예시)
FROM alpine@sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

또한 CI 스크립트 안정성을 위해 set -euo pipefail로 플래그가 의도치 않게 주입되는 상황을 줄이는 것도 도움이 됩니다: bash set -euo pipefail로 스크립트 폭발 막기


7) BuildKit 캐시 마운트/시크릿 사용법 오류 (--mount=type=cache|secret)

증상

  • --mount=type=cache를 넣었는데도 의존성 설치가 매번 처음부터
  • 비밀키를 파일로 COPY했다가 캐시가 깨지거나(혹은 보안 사고)

원인

  • 캐시 마운트는 “레이어 결과”가 아니라 빌드 중 임시 디렉터리를 재사용하는 기능입니다. 대상 경로가 툴이 실제로 사용하는 캐시 경로와 다르면 효과가 없습니다.
  • 시크릿을 COPY하면 Dockerfile 입력이 바뀌거나, 이미지 레이어에 남아 캐시/보안 모두에 악영향

해결

  • 패키지 매니저별 캐시 경로를 정확히 지정
  • 시크릿은 --mount=type=secret으로 주입하고 레이어에 남기지 않기

npm/pip 외에 apt 캐시도 가능:

# syntax=docker/dockerfile:1.7
FROM ubuntu:22.04

RUN --mount=type=cache,target=/var/cache/apt \
    --mount=type=cache,target=/var/lib/apt \
    apt-get update && apt-get install -y --no-install-recommends curl

프라이빗 레포 접근(예: pip index token)을 시크릿으로:

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

COPY requirements.txt ./

RUN --mount=type=secret,id=pip_token \
    --mount=type=cache,target=/root/.cache/pip \
    PIP_EXTRA_INDEX_URL="https://__token__:$((cat /run/secrets/pip_token))/simple" \
    pip install -r requirements.txt

빌드 명령:

docker buildx build \
  --secret id=pip_token,src=./pip_token.txt \
  -t myapp:dev .

빠른 진단 체크리스트 (재현 없이도 5분 컷)

  1. BuildKit 활성화 확인: docker buildx version, 또는 빌드 로그에 # syntax=/buildkit 관련 출력 확인
  2. 캐시가 존재하는가: CI라면 cache-to/cache-from 설정이 있는지
  3. 컨텍스트 폭발 여부: .dockerignore 미비, COPY . . 위치가 너무 이른지
  4. ARG/ENV 위치: 커밋 SHA/빌드시간 주입이 앞단에 있는지
  5. 비결정적 작업: upgrade, floating tag, 최신 버전 설치, timestamp 생성
  6. 멀티스테이지 산출물 안정성: 빌드 산출물이 매번 달라지는지
  7. cache/secret mount 경로 정확성: 실제 툴 캐시 경로와 일치하는지

결론

BuildKit 캐시가 “안 먹는” 상황은 대부분 캐시 시스템의 문제가 아니라 캐시 키를 바꾸는 입력(컨텍스트/ARG/베이스/비결정성) 또는 CI에서 캐시를 보존하지 않는 설정에서 발생합니다.

가장 효과가 큰 순서로 정리하면:

  • .dockerignore + 의존성 정의 파일 분리 COPY
  • 변경 잦은 ARG를 뒤로 미루기
  • cache-to/cache-from로 CI 캐시 내보내기/가져오기
  • 비결정적 빌드 제거(버전 핀/태그 고정)

위 7가지를 순서대로 적용하면, 체감 빌드 시간이 “분 단위 → 초 단위”로 내려가는 경우가 많습니다.