Docker 레이어 캐싱 전략과 실전 팁

Docker 레이어 캐시 동작 원리, COPY 순서 최적화, .dockerignore, BuildKit 캐시 전략, GitHub Actions·레지스트리 캐시까지 빌드 속도를 최대한 높이는 방법을 설명합니다.

· 7 min read · PALDYN Team

지난 글에서 BuildKit의 --mount=type=cache로 패키지 다운로드를 캐싱하는 방법을 다뤘다. 이번에는 Docker 레이어 캐시 전반을 체계적으로 정리한다. 레이어 순서 설계부터 CI 환경에서 캐시를 공유하는 방법까지 커버한다.

레이어 캐시 동작 원리

Docker는 이미지를 빌드할 때 각 RUN, COPY, ADD 명령마다 레이어를 생성한다. 이전 빌드에서 동일한 명령과 동일한 파일이 있으면 캐시 히트가 발생해 그 레이어를 재사용한다. 한 레이어의 캐시가 깨지면 이후의 모든 레이어 캐시도 깨진다.

# 캐시 히트 확인
$ docker build .
Step 3/7 : COPY package*.json ./
 ---> Using cache 캐시 히트
Step 4/7 : RUN npm ci
 ---> Using cache 캐시 히트
Step 5/7 : COPY . .
 ---> abc123 캐시 미스 (소스 변경됨)
Step 6/7 : RUN npm run build
 ---> 새로 실행 레이어 깨지면 여기도 새로 실행

이 원리를 이해하면 레이어 순서를 최적화하는 방법이 자명해진다.

핵심 원칙: 변경 빈도 낮은 것을 먼저

레이어 순서 최적화

자주 변하는 파일일수록 Dockerfile 아래에 배치한다. 소스 코드(COPY . .)는 항상 마지막 근처에 와야 하고, 의존성 설치(npm ci, pip install)는 의존성 명세 파일만 먼저 복사한 뒤 실행한다.

# 나쁜 예: 소스 먼저 복사
FROM node:20-slim
WORKDIR /app
COPY . .          # 소스 변경 시 여기서 캐시 깨짐
RUN npm ci        # 매번 재실행
RUN npm run build

# 좋은 예: 의존성 명세만 먼저
FROM node:20-slim
WORKDIR /app
COPY package.json package-lock.json ./  # 자주 안 바뀜
RUN npm ci                              # 위가 안 바뀌면 캐시 유지
COPY . .                                # 소스는 여기
RUN npm run build

.dockerignore로 불필요한 캐시 무효화 방지

.dockerignore가 없으면 COPY . .node_modules, .git, *.log 등 빌드에 불필요한 파일도 컨텍스트에 포함된다. 이 파일들이 바뀌면 캐시가 불필요하게 깨진다.

# .dockerignore
node_modules
.git
*.log
.DS_Store
dist
.env
.env.*
coverage
.nyc_output
# 빌드 컨텍스트 크기 확인
docker build --no-cache -t test . 2>&1 | grep "Sending build context"
# Sending build context to Docker daemon  2.048kB  ← .dockerignore 적용 후
# Sending build context to Docker daemon  145.2MB  ← 미적용 시

ARG 위치 주의

ARG는 선언 위치부터 캐시를 무효화한다. 자주 바뀌는 ARG(버전, 날짜 등)를 상단에 두면 모든 레이어 캐시가 깨진다.

# 나쁜 예: ARG가 상단
ARG GIT_COMMIT=unknown    # 매 빌드마다 달라짐
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci                # GIT_COMMIT이 바뀌면 여기 캐시도 깨짐

# 좋은 예: ARG를 실제 사용 직전으로 내림
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci                # ARG 영향 없음, 캐시 유지
COPY . .
ARG GIT_COMMIT=unknown    # 여기서 선언
ENV GIT_COMMIT=${GIT_COMMIT}

멀티 스테이지 빌드에서 캐시 활용

멀티 스테이지 빌드는 스테이지별로 독립적인 캐시를 가진다. builder 스테이지의 캐시는 런타임 스테이지 변경에 영향 받지 않는다.

# syntax=docker/dockerfile:1
FROM node:20-slim AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:20-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# 런타임 스테이지가 바뀌어도 deps, builder 캐시는 유지
FROM node:20-slim AS runtime
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/server.js"]

CI 환경 캐시 전략

CI 환경별 레이어 캐시 전략

GitHub Actions: type=gha

name: Build
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/build-push-action@v6
        with:
          context: .
          push: false
          cache-from: type=gha
          cache-to: type=gha,mode=max

mode=max는 모든 레이어를 캐시에 저장해 최대 히트율을 보장하지만 저장 공간을 더 사용한다. mode=min은 최종 이미지 레이어만 저장한다.

레지스트리 캐시

# 레지스트리에 캐시 저장
docker buildx build \
  --cache-from type=registry,ref=ghcr.io/myorg/myapp:cache \
  --cache-to type=registry,ref=ghcr.io/myorg/myapp:cache,mode=max \
  --push --tag ghcr.io/myorg/myapp:latest .

캐시 디버깅

# 캐시 히트/미스 상세 로그
BUILDKIT_PROGRESS=plain docker build .

# 캐시 없이 빌드 (전체 시간 측정)
time docker build --no-cache .

# 캐시 있는 빌드 시간 (최적화 후)
time docker build .

# 빌드 캐시 사용량 확인
docker buildx du --verbose

# 빌드 캐시 정리
docker buildx prune --filter type=exec.cachemount

주의: 캐시 무효화가 필요한 경우

레이어 캐시는 파일 내용 변화만 감지한다. 외부 API 응답이나 시간에 의존하는 RUN 명령은 파일이 바뀌지 않아도 결과가 달라질 수 있다.

# 이 레이어는 apt 저장소가 바뀌어도 캐시 히트됨 (위험할 수 있음)
RUN apt-get update && apt-get install -y curl

# 강제 무효화: ARG로 날짜 주입
ARG CACHE_DATE=2026-01-01
RUN apt-get update && apt-get install -y curl

보안 업데이트를 반드시 포함해야 하는 경우 주기적으로 --no-cache로 빌드하거나 CACHE_DATE ARG를 업데이트한다.


지난 글: BuildKit 캐시 마운트: RUN —mount=type=cache 완전 정복

다음 글: Docker 이미지에서 시크릿 유출 방지하기


읽어주셔서 감사합니다. 😊