Published on

Docker 빌드 Cache Miss? BuildKit 시크릿·캐시

Authors

서버/CI에서 Docker 이미지를 빌드할 때 “어제는 빨랐는데 오늘은 왜 전부 다시 빌드하지?” 같은 Cache Miss를 자주 겪습니다. 특히 의존성 설치 단계가 매번 다시 돌아가면 빌드 시간이 수 분에서 수십 분까지 늘어나고, CI 비용도 같이 상승합니다.

이 글은 Docker 레이어 캐시가 깨지는 대표 원인을 먼저 정리한 뒤, BuildKit의 --mount=type=secret(시크릿)과 --mount=type=cache(캐시 마운트) 를 이용해 “보안”과 “속도”를 동시에 잡는 방법을 실전 예제로 설명합니다.

참고: CI 캐시 관점의 점검 체크리스트는 GitHub Actions 캐시가 안 먹을 때 키·경로 9분 점검도 함께 보면 원인 분리가 더 빨라집니다.

Cache Miss가 발생하는 메커니즘(레이어 관점)

Docker 빌드는 Dockerfile의 각 명령이 레이어를 만들고, 각 레이어는 “명령 문자열 + 입력 파일 컨텍스트”가 같으면 재사용됩니다. 즉, 아래 조건 중 하나라도 바뀌면 해당 레이어부터 이후 레이어까지 연쇄적으로 캐시가 무효화됩니다.

1) COPY . .가 너무 이른 위치에 있음

가장 흔한 실수입니다. 소스 코드의 작은 변경도 의존성 설치 레이어에 영향을 주게 되어 npm ci/pip install이 매번 다시 실행됩니다.

나쁜 예(의존성 설치 전에 전체 복사):

FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build

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

FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

2) 빌드 컨텍스트에 불필요한 파일이 포함됨

.git, node_modules, 로그 파일, 로컬 캐시 등이 컨텍스트에 포함되면, 파일 타임스탬프/변경으로 인해 캐시가 자주 깨집니다.

.dockerignore는 “캐시 안정성”에도 직결됩니다.

# .dockerignore
.git
node_modules
npm-debug.log
.DS_Store
.env

3) 비결정적 명령(시간/네트워크)에 의존

예: apt-get update를 단독으로 실행하거나, 외부 URL에서 매번 다른 결과를 받는 경우입니다.

RUN apt-get update
RUN apt-get install -y curl

위처럼 분리하면 첫 번째 레이어가 자주 깨지고, 두 번째도 연쇄로 깨집니다. 보통은 하나의 RUN으로 묶고, 필요 시 버전을 고정합니다.

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

4) 시크릿을 ARG/ENV로 주입

토큰을 ARG로 넘겨 RUN에서 쓰면, 빌드 로그/히스토리에 남을 수 있고, 캐시 키에도 영향을 주기 쉽습니다. 무엇보다 보안상 위험합니다.

BuildKit 시크릿을 쓰면 이미지 레이어에 남기지 않고, 캐시를 깨지 않으면서 인증이 필요한 다운로드/설치를 수행할 수 있습니다.

BuildKit 활성화: 기본 전제

아래 예제는 BuildKit을 전제로 합니다.

  • 로컬: DOCKER_BUILDKIT=1 환경변수 또는 Docker Desktop 기본 설정
  • docker buildx 사용 권장
DOCKER_BUILDKIT=1 docker build -t myapp:local .

또는 buildx:

docker buildx build -t myapp:local .

BuildKit 시크릿: 토큰을 안전하게 쓰고 캐시도 지키기

대표 시나리오는 다음과 같습니다.

  • private npm registry
  • private PyPI
  • GitHub Packages
  • 사내 artifact repository

핵심: --mount=type=secret

Dockerfile 상단에 syntax directive를 추가하면 BuildKit 기능을 더 안정적으로 쓸 수 있습니다.

# syntax=docker/dockerfile:1.7
FROM node:20-alpine
WORKDIR /app

COPY package.json package-lock.json ./

RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
    npm ci

COPY . .
RUN npm run build

빌드할 때 시크릿 파일을 주입합니다.

docker buildx build \
  --secret id=npmrc,src=.npmrc \
  -t myapp:local \
  .
  • .npmrc에는 토큰이 들어있을 수 있지만, 이미지 레이어에 복사되지 않습니다.
  • RUN 단계에서만 마운트되고, 결과 레이어에는 남지 않습니다.

자주 하는 실수: 시크릿을 COPY로 넣었다가 삭제

COPY .npmrc /root/.npmrc
RUN npm ci
RUN rm -f /root/.npmrc

이 방식은 최종 파일을 지워도 이전 레이어에 흔적이 남을 수 있어 위험합니다. 또한 캐시 키가 .npmrc 변경에 민감해져서 Cache Miss를 유발하기도 합니다.

BuildKit 캐시 마운트: 의존성 다운로드를 “레이어 밖”에 저장

레이어 캐시는 “명령이 같을 때 레이어를 통째로 재사용”하는 방식이라, 의존성 설치 레이어가 깨지면 다운로드를 다시 하게 됩니다. BuildKit의 --mount=type=cache는 다운로드 캐시를 레이어 밖에 저장해, 레이어가 깨져도 네트워크 비용을 줄입니다.

Node.js: npm 캐시 마운트

# syntax=docker/dockerfile:1.7
FROM node:20-alpine
WORKDIR /app

COPY package.json package-lock.json ./

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

COPY . .
RUN npm run build
  • npm ci가 다시 실행되더라도, 패키지 tarball 다운로드가 크게 줄어듭니다.

Python: pip 캐시 마운트

# syntax=docker/dockerfile:1.7
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"]

apt 캐시 마운트(주의점 포함)

apt는 캐시/리스트 디렉토리가 여러 개라 정교하게 잡아야 하고, 이미지 재현성도 고려해야 합니다.

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

RUN --mount=type=cache,target=/var/cache/apt \
    --mount=type=cache,target=/var/lib/apt/lists \
    apt-get update \
    && apt-get install -y --no-install-recommends ca-certificates curl \
    && rm -rf /var/lib/apt/lists/*
  • 캐시 마운트는 “속도”에 도움 되지만, 미러 상태에 따라 결과가 바뀔 수 있습니다. 운영 빌드에서는 버전 고정, 내부 미러 사용 등을 고려하세요.

시크릿 + 캐시를 함께 쓰는 실전 패턴(Private registry)

Private registry에서 패키지를 받는 경우, 시크릿으로 인증하고 캐시 마운트로 다운로드 비용을 줄이는 조합이 가장 효과적입니다.

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

COPY package.json package-lock.json ./

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

COPY . .
RUN npm run build

FROM node:20-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
CMD ["node", "dist/server.js"]

빌드 명령:

docker buildx build \
  --secret id=npmrc,src=.npmrc \
  -t myapp:prod \
  .

이 패턴의 장점:

  • 시크릿이 최종 이미지에 포함되지 않음
  • 의존성 다운로드가 캐시에 남아 재빌드가 빨라짐
  • 멀티 스테이지로 런타임 이미지를 가볍게 유지

캐시가 “먹는지” 확인하는 방법

1) 빌드 로그에서 캐시 히트 확인

BuildKit은 캐시 히트 시 CACHED 같은 표시가 나옵니다(환경에 따라 표현은 다를 수 있음).

2) 캐시가 깨지는 지점을 역추적

  • 깨지는 레이어 바로 위에 있는 COPY/RUN이 무엇인지 확인
  • “입력 파일이 바뀌었는지”를 확인
  • .dockerignore로 컨텍스트 변동을 줄였는지 확인

CI에서 캐시가 계속 비는 경우는 Dockerfile 문제뿐 아니라 “캐시 저장소/키/스코프” 이슈일 수 있습니다. 이때는 GitHub Actions 캐시가 안 먹을 때 키·경로 9분 점검의 체크리스트가 그대로 도움이 됩니다.

자주 겪는 함정 5가지

1) 락파일이 자주 바뀜

package-lock.json, poetry.lock 등이 불필요하게 변경되면 의존성 레이어가 계속 깨집니다. 팀에서 락파일 생성 규칙을 통일하세요.

2) 빌드 시점에 소스 버전을 주입하려고 ARG를 남발

ARG BUILD_SHA 같은 값이 매번 바뀌면 해당 레이어 이후가 전부 무효화됩니다. 꼭 필요한 위치(가장 아래 레이어)로 내리거나, 런타임 라벨로만 남기는 방식을 고려합니다.

ARG BUILD_SHA
LABEL org.opencontainers.image.revision=$BUILD_SHA

3) RUN curl ... | bash 패턴

외부 스크립트가 바뀌면 재현성이 무너지고 캐시도 불안정합니다. 가능하면 체크섬 검증, 버전 고정, 내부 아티팩트 사용을 권장합니다.

4) 시크릿을 환경변수로 노출

ENV NPM_TOKEN=...는 이미지 메타데이터/레이어에 흔적이 남기 쉽습니다. BuildKit 시크릿으로 바꾸세요.

5) 컨테이너 런타임 장애를 “빌드 캐시”로 오해

빌드는 빨라졌는데 배포 후 CrashLoopBackOff가 나면 빌드 캐시가 아니라 런타임 설정/리소스 문제일 가능성이 큽니다. 증상 분리는 K8s CrashLoopBackOff·OOMKilled 원인별 해결 가이드를 참고하세요.

CI에서 더 빠르게: 원격 캐시(export/import) 개념만 잡기

로컬 머신에서는 레이어 캐시가 남지만, CI는 매번 깨끗한 러너에서 시작하는 경우가 많습니다. 이때는 BuildKit의 캐시를 외부로 내보내고(import/export) 다음 빌드에서 다시 가져오는 전략이 필요합니다.

예를 들어 buildx--cache-to, --cache-from을 지원합니다(백엔드는 레지스트리, 로컬 디렉토리 등 선택).

docker buildx build \
  --cache-from type=registry,ref=registry.example.com/myapp:buildcache \
  --cache-to type=registry,ref=registry.example.com/myapp:buildcache,mode=max \
  -t registry.example.com/myapp:latest \
  --push \
  .
  • mode=max는 더 많은 중간 캐시를 저장해 히트율을 높입니다.
  • 조직/보안 정책상 캐시 레지스트리 운영이 어렵다면, CI 플랫폼 캐시와 조합하는 방식도 고려합니다.

정리: Cache Miss를 줄이는 우선순위

  1. COPY 순서를 재설계해서 의존성 레이어를 안정화
  2. .dockerignore로 빌드 컨텍스트 변동 최소화
  3. BuildKit 시크릿으로 인증 정보가 레이어에 남지 않게 처리
  4. BuildKit 캐시 마운트로 다운로드/컴파일 캐시를 레이어 밖으로 분리
  5. CI에서는 원격 캐시(export/import)로 러너 간 캐시를 공유

위 5가지를 적용하면 “캐시가 안 먹는다”는 막연한 문제를 대부분 구조적으로 해결할 수 있고, 빌드 속도/보안/재현성을 동시에 끌어올릴 수 있습니다.