Published on

Docker 빌드 10배 빠르게 - BuildKit 캐시+SBOM

Authors

서버/클라이언트 성능 최적화만큼이나, CI에서 반복되는 Docker 빌드 시간을 줄이는 건 체감 효율이 큽니다. 특히 모노레포, 멀티스테이지 빌드, 패키지 매니저 캐시가 얽히면 빌드가 매번 0부터 시작되는 일이 잦고, 그 결과 배포 리드타임과 클라우드 비용이 함께 올라갑니다.

이 글은 **BuildKit 캐시(로컬+레지스트리+GitHub Actions 캐시)**를 조합해 빌드를 빠르게 만들고, 동시에 **SBOM(Software Bill of Materials)**을 생성해 아티팩트로 보관하거나 이미지에 첨부하는 흐름까지 한 번에 정리합니다.

참고로 이미지 최적화나 병목을 줄이는 접근은 웹 성능에서도 동일하게 중요합니다. 빌드/배포 파이프라인을 줄이면 프로덕트 개선 사이클도 빨라집니다. 관련해서는 Next.js LCP 4초→1초 - RSC·이미지·폰트 최적화도 같이 보면 “병목을 제거하는 사고방식”이 이어집니다.

왜 BuildKit이 체감상 10배까지 빨라지나

BuildKit은 단순히 “더 빠른 빌더”가 아니라, 캐시 키를 더 똑똑하게 계산하고, 레이어 실행을 병렬화하며, 외부 캐시 저장소로 캐시를 내보내고(import/export) 재사용할 수 있게 해줍니다.

체감 속도가 크게 달라지는 포인트는 주로 아래 3가지입니다.

  1. 의존성 설치 단계 캐시 고정
    • package-lock.json/pnpm-lock.yaml/poetry.lock 등 “의존성 그래프”가 바뀌지 않으면 설치 단계가 재실행되지 않게 만들기
  2. 캐시 마운트(--mount=type=cache)로 패키지 다운로드 재사용
    • npm/pnpm/pip/apt 다운로드 캐시를 레이어로 굳히지 않고, 빌드 캐시로 재사용
  3. 레지스트리 캐시(--cache-to/--cache-from)로 CI 간 재사용
    • CI 러너가 매번 새로 뜨더라도 캐시를 레지스트리에서 가져와 빌드 시간 단축

준비: BuildKit 활성화와 버전 체크

로컬에서 Docker Desktop을 쓰는 경우 대부분 BuildKit이 기본 활성화지만, CI나 리눅스 서버에서는 명시하는 편이 안전합니다.

# 1) BuildKit 활성화(일회성)
DOCKER_BUILDKIT=1 docker build .

# 2) buildx 확인
docker buildx version

docker buildx ls

CI에서는 보통 docker/setup-buildx-action 같은 액션을 사용하거나, 자체 러너에서는 buildx builder를 만들어 둡니다.

Dockerfile: 캐시가 “먹는” 구조로 다시 짜기

캐시 최적화의 핵심은 간단합니다.

  • 자주 바뀌는 파일(소스 코드)은 최대한 뒤에서 COPY
  • 의존성 설치는 lockfile 기준으로만 캐시가 깨지게 만들기
  • 다운로드/컴파일 캐시는 --mount=type=cache로 재사용

아래는 Node.js(예: Next.js) 기준 예시입니다.

# syntax=docker/dockerfile:1.7

FROM node:20-bookworm-slim AS deps
WORKDIR /app

# 의존성 그래프만 먼저 복사
COPY package.json package-lock.json ./

# npm 캐시를 BuildKit 캐시로 재사용
RUN --mount=type=cache,target=/root/.npm \
    npm ci

FROM node:20-bookworm-slim AS build
WORKDIR /app

# deps 레이어 재사용
COPY --from=deps /app/node_modules ./node_modules

# 이제 소스 복사(자주 바뀌는 영역)
COPY . .

RUN npm run build

FROM node:20-bookworm-slim AS runner
WORKDIR /app
ENV NODE_ENV=production

# 런타임에 필요한 것만
COPY --from=build /app/.next ./.next
COPY --from=build /app/public ./public
COPY --from=build /app/package.json ./package.json
COPY --from=build /app/node_modules ./node_modules

EXPOSE 3000
CMD ["node", "node_modules/next/dist/bin/next", "start", "-p", "3000"]

자주 하는 실수 3가지

  1. COPY . .를 너무 앞에 둬서 의존성 설치 캐시가 매번 깨짐
  2. npm install을 사용해 lockfile이 바뀌거나 재현성이 떨어짐(가능하면 npm ci)
  3. 패키지 매니저 캐시 디렉터리를 레이어로 남겨 이미지가 커짐(캐시 마운트로 해결)

레지스트리 캐시: CI에서 진짜 효과가 나는 설정

로컬에서야 레이어 캐시가 남으니 빠른데, CI는 러너가 매번 초기화되는 경우가 많습니다. 이때는 레지스트리 캐시가 정답에 가깝습니다.

Buildx에서 다음을 쓰면 됩니다.

  • --cache-to type=registry로 캐시를 푸시
  • --cache-from type=registry로 캐시를 풀

GitHub Actions 예시(레지스트리 캐시 + 이미지 푸시)

name: build-and-push
on:
  push:
    branches: ["main"]

jobs:
  docker:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v4

      - uses: docker/setup-qemu-action@v3
      - uses: docker/setup-buildx-action@v3

      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push with registry cache
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: |
            ghcr.io/${{ github.repository }}:latest
            ghcr.io/${{ github.repository }}:${{ github.sha }}
          cache-from: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache
          cache-to: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache,mode=max

mode=max는 가능한 많은 중간 레이어를 캐시에 남겨 재사용 폭을 넓힙니다(캐시 저장소는 커질 수 있음).

ECR을 쓰는 경우 주의점

EKS에서 ImagePullBackOff가 나거나 ECR 인증이 꼬이면 “캐시를 잘 써도 배포가 멈추는” 일이 생깁니다. 이미지 풀/인증이 불안정하면 먼저 그 부분을 안정화하는 게 우선입니다. 필요하면 EKS Pod ImagePullBackOff - ECR 인증·IRSA 점검법을 참고하세요.

캐시 마운트 심화: apt/pip/pnpm 캐시까지 잡기

BuildKit의 RUN --mount=type=cache는 다운로드 캐시를 “레이어”가 아니라 “빌드 캐시”로 관리하게 해줍니다.

apt 캐시 예시

# syntax=docker/dockerfile:1.7
FROM debian:bookworm-slim

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 \
      ca-certificates curl && \
    rm -rf /var/lib/apt/lists/*

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", "app.py"]

이 방식은 “이미지 크기”와 “빌드 속도”를 동시에 잡는 데 유리합니다.

SBOM을 왜 지금 같이 해야 하나

빌드가 빨라지면 배포 빈도가 올라가고, 배포 빈도가 올라가면 취약점 대응(패치)도 더 자주 하게 됩니다. 그런데 공급망 보안 요구사항이 있는 조직에서는 “어떤 라이브러리가 어디에 들어갔는지”를 증명해야 하는 순간이 옵니다.

SBOM은 그때 필요한 의존성 명세서입니다.

  • 취약점 스캐너(예: Trivy, Grype)가 SBOM 기반으로 더 정확히 분석 가능
  • 감사/컴플라이언스 대응에 유리
  • 특정 CVE가 터졌을 때 영향 범위를 빠르게 좁힘

핵심은 “SBOM 생성이 느려서 파이프라인을 망치면 안 된다”는 점인데, BuildKit 캐시로 빌드를 줄여두면 SBOM 단계가 추가돼도 전체 시간 증가를 상쇄하기 쉽습니다.

SBOM 생성 방법 2가지(실무에서 많이 씀)

방법 A: Trivy로 이미지 SBOM 생성

이미지를 빌드/푸시한 뒤 SBOM을 생성해 아티팩트로 업로드하는 방식입니다.

# 이미지 빌드
DOCKER_BUILDKIT=1 docker build -t myapp:local .

# SBOM 생성(CycloneDX)
trivy image --format cyclonedx --output sbom.cdx.json myapp:local

# 또는 SPDX(JSON)
trivy image --format spdx-json --output sbom.spdx.json myapp:local

장점: 적용이 단순하고 언어 스택이 섞여 있어도 잘 동작합니다.

방법 B: BuildKit로 SBOM을 “이미지에 첨부(attestation)”

Buildx는 provenance/SBOM 같은 attestation을 이미지에 붙일 수 있습니다(레지스트리와 뷰어/검증 도구 지원 여부에 따라 활용도가 달라질 수 있음).

docker buildx build \
  --push \
  --tag ghcr.io/org/myapp:latest \
  --sbom=true \
  --provenance=true \
  .

이 방식의 이점은 “SBOM 파일을 따로 보관하지 않아도 이미지와 함께 이동”한다는 점입니다.

주의: 조직의 레지스트리/보안 도구가 attestation을 어떻게 소비하는지 먼저 확인하세요. 운영에서 중요한 건 “만드는 것”보다 “검증/조회가 가능한 것”입니다.

GitHub Actions: 빌드 캐시 + SBOM 아티팩트까지 한 번에

아래 예시는 다음을 포함합니다.

  • Buildx 레지스트리 캐시 사용
  • 이미지 푸시
  • Trivy로 SBOM 생성
  • SBOM을 workflow artifact로 업로드
name: build-push-sbom
on:
  push:
    branches: ["main"]

jobs:
  docker:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    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 }}

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

      - name: Install Trivy
        run: |
          curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b ./bin
          ./bin/trivy --version

      - name: Generate SBOM (CycloneDX)
        run: |
          ./bin/trivy image --format cyclonedx --output sbom.cdx.json ghcr.io/${{ github.repository }}:${{ github.sha }}

      - name: Upload SBOM artifact
        uses: actions/upload-artifact@v4
        with:
          name: sbom
          path: sbom.cdx.json

이 구성은 “빌드 속도”와 “보안 산출물”을 함께 올려서, 나중에 감사/사고 대응 시점에 시간을 절약합니다.

빌드 시간을 진짜로 줄이는 체크리스트

1) 캐시가 깨지는 지점을 로그로 확인

BuildKit은 단계별 캐시 히트 여부가 비교적 명확히 보입니다.

docker buildx build --progress=plain .

여기서 특정 RUN npm ci가 매번 실행된다면, 그 앞 단계에서 캐시 키를 깨는 COPY가 있는지부터 의심합니다.

2) 컨텍스트 크기 줄이기(.dockerignore)

컨텍스트가 커지면 업로드/해시 계산 비용이 커지고, 사소한 파일 변경으로 캐시가 깨질 수 있습니다.

# .dockerignore
node_modules
.next
.git
coverage
*.log
.env

3) 멀티스테이지에서 “복사 순서” 점검

  • deps 단계는 lockfile만
  • build 단계에서 소스 복사
  • runner 단계는 결과물만

이 3단 구조만 제대로 지켜도 대부분의 프로젝트에서 빌드 시간이 확 줄어듭니다.

운영 관점: 캐시와 SBOM을 같이 굴릴 때의 규칙

  • 캐시 이미지 태그(buildcache)는 애플리케이션 이미지와 분리
  • 캐시 저장소 용량/보존 정책을 정함(무한정 쌓이면 비용 이슈)
  • SBOM은 “생성”뿐 아니라 “보관/조회/연계(취약점 스캔)”까지 흐름을 설계
  • 릴리스마다 SBOM을 남기면, 특정 장애/취약점 발생 시점에 영향 범위를 빠르게 역추적 가능

배포 자동화(예: Argo CD) 환경이라면, 이미지 태그/서명/산출물(SBOM)까지 한 세트로 관리하는 것이 장기적으로 안정적입니다. 동기화가 꼬일 때의 진단 흐름은 Argo CD Sync 실패? RBAC·CRD·Drift 진단법처럼 “원인 범주화”가 도움이 됩니다.

결론: 빠른 빌드가 보안을 방해하지 않게 만들기

BuildKit 캐시는 Docker 빌드 시간을 줄이는 가장 직접적인 방법이고, 레지스트리 캐시까지 붙이면 CI에서도 효과가 확실합니다. 여기에 SBOM을 파이프라인에 포함하면 “속도”를 얻으면서도 “무엇을 배포했는지”를 증명할 수 있어, 운영과 보안 요구사항을 동시에 만족시키기 쉬워집니다.

정리하면 다음 순서로 적용하는 걸 권합니다.

  1. Dockerfile을 캐시 친화적으로 재구성(의존성 단계 분리)
  2. BuildKit 캐시 마운트로 다운로드 캐시 재사용
  3. CI에 레지스트리 캐시 적용
  4. Trivy 등으로 SBOM 생성 후 아티팩트 보관(또는 attestation)

이 4단계만 해도 “빌드 10배”는 과장이 아니라, 프로젝트/CI 조건에 따라 충분히 현실적인 목표가 됩니다.