- Published on
Docker 빌드 Cache Miss? BuildKit 시크릿·캐시
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버/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 \
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 \
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 \
pip install -r requirements.txt
COPY . .
CMD ["python", "main.py"]
apt 캐시 마운트(주의점 포함)
apt는 캐시/리스트 디렉토리가 여러 개라 정교하게 잡아야 하고, 이미지 재현성도 고려해야 합니다.
# syntax=docker/dockerfile:1.7
FROM ubuntu:24.04
RUN \
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 \
npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY /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를 줄이는 우선순위
COPY순서를 재설계해서 의존성 레이어를 안정화.dockerignore로 빌드 컨텍스트 변동 최소화- BuildKit 시크릿으로 인증 정보가 레이어에 남지 않게 처리
- BuildKit 캐시 마운트로 다운로드/컴파일 캐시를 레이어 밖으로 분리
- CI에서는 원격 캐시(export/import)로 러너 간 캐시를 공유
위 5가지를 적용하면 “캐시가 안 먹는다”는 막연한 문제를 대부분 구조적으로 해결할 수 있고, 빌드 속도/보안/재현성을 동시에 끌어올릴 수 있습니다.