Theme:

Docker 이미지를 빌드할 때마다 몇 분씩 걸린다면, 실제로 변경된 부분은 소스 코드 한 줄인데 전체를 다시 빌드하고 있는 건 아닐까요?

레이어 캐시의 기본 원리

Dockerfile의 각 명령어(FROM, RUN, COPY 등)는 하나의 레이어를 만듭니다. Docker는 빌드할 때 각 레이어의 입력이 이전 빌드와 동일하면 캐시된 결과를 재사용합니다.

PLAINTEXT
명령어 1: FROM node:20-alpine     → 캐시 히트 ✓
명령어 2: COPY package.json ./    → 캐시 히트 ✓ (파일 내용 동일)
명령어 3: RUN npm ci              → 캐시 히트 ✓ (이전 레이어 동일)
명령어 4: COPY . .                → 캐시 미스 ✗ (소스 코드 변경됨)
명령어 5: RUN npm run build       → 캐시 미스 ✗ (이전 레이어 변경)

핵심 규칙: 캐시 미스가 발생하면, 그 이후의 모든 레이어도 캐시를 사용할 수 없습니다.

캐시가 무효화되는 조건

RUN 명령어

  • 명령어 문자열이 변경되면 캐시 미스
  • 이전 레이어가 변경되면 캐시 미스
  • 명령어 실행 결과가 달라지더라도 문자열이 같으면 캐시 히트 (주의!)
DOCKERFILE
# 이 명령어는 실행할 때마다 다른 패키지 버전을 설치할 수 있지만
# 문자열이 동일하면 캐시를 재사용합니다
RUN apt-get update && apt-get install -y curl

COPY/ADD 명령어

  • 파일의 내용 체크섬을 비교합니다 (수정 시간이 아님)
  • 파일 내용이 하나라도 바뀌면 캐시 미스
  • 퍼미션 변경도 캐시 미스를 유발

명령어 순서 최적화

가장 기본적이면서도 효과가 큰 최적화는 변경 빈도가 낮은 명령어를 위에 배치하는 것입니다.

나쁜 예

DOCKERFILE
FROM node:20-alpine
WORKDIR /app
COPY . .                    # 소스 코드 변경 → 여기서 캐시 미스
RUN npm ci                  # 매번 다시 설치 (캐시 못 씀)
RUN npm run build           # 매번 다시 빌드

소스 코드 한 줄만 수정해도 npm ci부터 다시 실행됩니다.

좋은 예

DOCKERFILE
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./   # 의존성 파일은 잘 안 바뀜
RUN npm ci                               # 캐시 재사용!
COPY . .                                 # 소스 코드 (여기서만 캐시 미스)
RUN npm run build                        # 이것만 다시 실행

의존성 파일이 변경되지 않았다면 npm ci 레이어가 캐시에서 재사용됩니다. 소스 코드만 바뀌었을 때 빌드 시간이 분 단위에서 초 단위로 줄어들 수 있습니다.

언어별 캐시 최적화 패턴

DOCKERFILE
# Java (Gradle)
COPY build.gradle.kts settings.gradle.kts gradlew ./
COPY gradle/ gradle/
RUN ./gradlew dependencies --no-daemon
COPY src/ src/
RUN ./gradlew bootJar --no-daemon

# Python
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

# Go
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o /app

BuildKit 캐시 마운트

Docker BuildKit은 --mount=type=cache라는 강력한 기능을 제공합니다. 패키지 매니저의 다운로드 캐시를 빌드 간에 유지할 수 있습니다.

활성화 방법

BASH
# 방법 1: 환경변수
export DOCKER_BUILDKIT=1
docker build .

# 방법 2: docker buildx (권장)
docker buildx build .

npm 캐시 마운트

DOCKERFILE
# syntax=docker/dockerfile:1
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

/root/.npm 디렉토리가 빌드 간에 유지되므로, 패키지를 다시 다운로드하지 않아도 됩니다.

다양한 패키지 매니저에 적용

DOCKERFILE
# apt
RUN --mount=type=cache,target=/var/cache/apt \
    --mount=type=cache,target=/var/lib/apt \
    apt-get update && apt-get install -y curl git

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

# Maven
RUN --mount=type=cache,target=/root/.m2/repository \
    mvn package -DskipTests

# Gradle
RUN --mount=type=cache,target=/root/.gradle \
    ./gradlew build --no-daemon

# Go
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    go build -o /app

캐시 마운트 vs 레이어 캐시

특성레이어 캐시캐시 마운트
동작명령어 입력이 같으면 전체 재사용캐시 디렉토리만 유지
무효화이전 레이어 변경 시 연쇄 무효화무효화 없이 항상 사용 가능
이미지 포함레이어에 포함됨이미지에 포함되지 않음
활용명령어 순서 최적화패키지 매니저 캐시

CI 환경에서의 캐시 전략

CI 러너는 매번 깨끗한 환경에서 시작하므로 로컬 레이어 캐시가 없습니다. 별도의 캐시 전략이 필요합니다.

레지스트리 캐시

BASH
# 이전 빌드 이미지를 캐시 소스로 사용
docker buildx build \
    --cache-from type=registry,ref=myregistry.com/myapp:cache \
    --cache-to type=registry,ref=myregistry.com/myapp:cache,mode=max \
    -t myapp:latest .

GitHub Actions 예시

YAML
# .github/workflows/build.yml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build with cache
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ghcr.io/myorg/myapp:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

type=gha는 GitHub Actions의 캐시 스토리지를 활용합니다.

인라인 캐시

BASH
# 이미지에 캐시 메타데이터를 포함시켜 push
docker buildx build \
    --cache-to type=inline \
    --push \
    -t myregistry.com/myapp:latest .

# 다음 빌드에서 해당 이미지를 캐시로 사용
docker buildx build \
    --cache-from myregistry.com/myapp:latest \
    -t myregistry.com/myapp:latest .

로컬 캐시 (CI에서 디렉토리로 저장)

BASH
docker buildx build \
    --cache-from type=local,src=/tmp/.buildx-cache \
    --cache-to type=local,dest=/tmp/.buildx-cache-new,mode=max \
    -t myapp:latest .

캐시 무효화를 피하는 패턴들

1. .dockerignore로 불필요한 파일 제외

TEXT
# .dockerignore
.git
node_modules
dist
*.md
.env
.vscode

.git 디렉토리가 빌드 컨텍스트에 포함되면, 커밋할 때마다 COPY . .의 캐시가 무효화됩니다.

2. ARG 사용 시 위치 주의

DOCKERFILE
FROM node:20-alpine

# ARG를 아래쪽에 배치 — 값이 바뀌어도 윗 레이어에 영향 없음
COPY package*.json ./
RUN npm ci
COPY . .

ARG BUILD_VERSION=dev
RUN npm run build -- --env version=${BUILD_VERSION}

ARG가 사용된 레이어부터 캐시가 무효화되므로, 빈번히 변경되는 ARG는 최대한 아래에 배치합니다.

3. 파일 복사 세분화

DOCKERFILE
# 나쁜 예: 설정 파일 하나 바뀌면 전체 캐시 미스
COPY . .

# 좋은 예: 변경 빈도별로 나누어 복사
COPY tsconfig.json ./
COPY webpack.config.js ./
COPY package.json package-lock.json ./
RUN npm ci
COPY src/ ./src/
COPY public/ ./public/
RUN npm run build

4. 날짜/타임스탬프 포함 명령어 피하기

DOCKERFILE
# 나쁜 예: 빌드할 때마다 날짜가 달라져 캐시 미스
RUN echo "Built on $(date)" > /build-info.txt

# 좋은 예: 빌드의 마지막에 배치
COPY . .
RUN npm run build
RUN echo "Built on $(date)" > /build-info.txt

캐시 분석 및 디버깅

빌드 로그 확인

BASH
# 캐시 사용 여부 확인
docker build . 2>&1 | grep -E "CACHED|RUN|COPY"

# BuildKit 상세 로그
docker buildx build --progress=plain .

캐시가 사용되면 CACHED라고 표시됩니다.

PLAINTEXT
#5 [2/6] COPY package.json package-lock.json ./
#5 CACHED

#6 [3/6] RUN npm ci
#6 CACHED

#7 [4/6] COPY . .
#7 sha256:abc123... 2.5MB

#8 [5/6] RUN npm run build
#8 0.523 Building...

캐시 정리

BASH
# 빌드 캐시 확인
docker builder prune --dry-run

# 빌드 캐시 정리
docker builder prune

# 전체 정리 (주의: 사용하지 않는 이미지/컨테이너도 삭제)
docker system prune -a

정리

  • Docker는 각 명령어의 입력이 동일하면 캐시를 재사용하고, 캐시 미스가 발생하면 이후 모든 레이어를 다시 빌드합니다.
  • 변경 빈도가 낮은 명령어(의존성 설치)를 위에, 높은 명령어(소스 복사)를 아래에 배치하는 것이 기본 전략입니다.
  • BuildKit의 --mount=type=cache는 패키지 매니저 캐시를 빌드 간에 유지하여 다운로드 시간을 절약합니다.
  • CI 환경에서는 레지스트리 캐시(type=registry)나 GitHub Actions 캐시(type=gha)를 설정해야 캐시를 활용할 수 있습니다.
  • .dockerignore, ARG 배치, 파일 복사 세분화로 불필요한 캐시 무효화를 방지합니다.
댓글 로딩 중...