Docker BuildKit 심화 — 내부 구조와 고급 기능

BuildKit의 LLB 솔버, 병렬 빌드, 캐시 마운트, 시크릿 마운트, SSH 포워딩, 멀티 플랫폼 빌드까지 실무에서 쓰이는 고급 기능을 심층 설명합니다.

· 7 min read · PALDYN Team

지난 글에서 시간대 불일치 문제를 해결했다. 이번에는 Docker 빌드 시스템의 핵심인 BuildKit을 깊이 들여다본다. 일반 docker build와 달리 BuildKit은 병렬 실행, 고급 캐시, 시크릿 안전 처리 등 실무에서 차별화되는 기능을 제공한다.

BuildKit이란

BuildKit은 Docker 18.09에 도입된 차세대 빌드 엔진이다. Docker 23부터는 기본 활성화되어 있다.

# BuildKit 활성화 여부 확인
docker info | grep -i buildkit

# 수동 활성화 (구버전)
DOCKER_BUILDKIT=1 docker build .

# daemon.json으로 영구 활성화
# /etc/docker/daemon.json
# { "features": { "buildkit": true } }

BuildKit 아키텍처

내부 구조: LLB

BuildKit의 핵심은 **LLB(Low-Level Build)**라는 중간 표현이다. Dockerfile 파서(Frontend)가 LLB 그래프를 생성하고, Solver가 의존성을 분석해 병렬로 실행한다.

기존 Docker 빌드는 각 레이어를 순차적으로 실행했다. BuildKit은 의존성이 없는 단계를 동시에 실행한다.

병렬 멀티 스테이지 빌드

BuildKit 병렬 멀티 스테이지 빌드

# syntax=docker/dockerfile:1

FROM node:20-alpine AS base
WORKDIR /app

FROM base AS deps
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci --only=production

FROM base AS test
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm test

FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:20-alpine AS production
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]

deps, test, builder 세 스테이지가 모두 base에 의존하지만 서로 독립적이므로 BuildKit이 병렬로 실행한다.

# 특정 스테이지만 빌드
docker build --target deps -t myapp:deps .
docker build --target test -t myapp:test .
docker build --target production -t myapp:prod .

캐시 마운트

# npm: 패키지 캐시를 빌드 간 유지
RUN --mount=type=cache,target=/root/.npm \
    npm ci

# pip: Python 패키지 캐시
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

# apt: 데비안 패키지 캐시
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    --mount=type=cache,target=/var/lib/apt,sharing=locked \
    apt-get update && apt-get install -y build-essential

# Go 모듈 캐시
RUN --mount=type=cache,target=/go/pkg/mod \
    go build ./...

캐시 마운트는 이미지 레이어에 포함되지 않으므로 이미지 크기에 영향을 주지 않는다.

시크릿 마운트 — 이미지에 흔적 없이

빌드 중 비밀번호나 API 키가 필요할 때 기존 방식(ARG, ENV)은 이미지 레이어에 값이 그대로 남는다.

# 잘못된 방법 — 이미지 레이어에 토큰이 남음
ARG NPM_TOKEN
RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc \
    && npm install \
    && rm -f .npmrc

BuildKit 시크릿 마운트는 빌드 중에만 파일로 마운트되고, 빌드 후에는 어떤 레이어에도 남지 않는다.

# 올바른 방법 — 이미지에 토큰 흔적 없음
# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
    npm install
# 빌드 시 시크릿 파일 전달
docker build \
  --secret id=npmrc,src=$HOME/.npmrc \
  -t myapp .

# 환경변수에서 직접
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" \
  | docker build --secret id=npmrc,src=/dev/stdin .

SSH 포워딩 — 비공개 Git 저장소 클론

# syntax=docker/dockerfile:1
FROM alpine

RUN apk add --no-cache openssh-client git

RUN --mount=type=ssh \
    git clone git@github.com:myorg/private-repo.git /app
# SSH 에이전트 시작
eval $(ssh-agent)
ssh-add ~/.ssh/id_ed25519

# SSH 에이전트 소켓을 빌드에 전달
docker build --ssh default -t myapp .

SSH 키가 이미지 어디에도 포함되지 않는다. 빌드 컨테이너에서 에이전트 포워딩으로만 사용된다.

빌드 결과 내보내기 (buildx)

# 로컬에 이미지로 저장 (기본)
docker build -t myapp .

# 타르볼로 내보내기
docker build -o type=tar,dest=myapp.tar .

# 디렉터리로 내보내기 (파일시스템만)
docker build --target builder -o type=local,dest=./output .

# OCI 이미지 타르볼
docker buildx build -o type=oci,dest=myapp-oci.tar .

멀티 플랫폼 빌드

# QEMU 에뮬레이터 설정
docker run --privileged --rm \
  tonistiigi/binfmt --install all

# buildx 빌더 생성
docker buildx create --name mybuilder --use

# amd64, arm64 동시 빌드
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t myregistry/myapp:latest \
  --push .

빌드 시간이 오래 걸리는 arm64는 크로스 컴파일을 활용하면 빠르다.

# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM golang:1.22 AS builder

ARG TARGETPLATFORM TARGETOS TARGETARCH

RUN GOOS=$TARGETOS GOARCH=$TARGETARCH \
    go build -o /app/server .

FROM --platform=$TARGETPLATFORM alpine
COPY --from=builder /app/server /app/server
CMD ["/app/server"]

$BUILDPLATFORM(빌드 호스트)에서 Go를 크로스 컴파일하고, 최종 이미지는 $TARGETPLATFORM으로 만든다. QEMU 에뮬레이션 없이 네이티브 속도로 컴파일된다.

빌드 인자와 캐시 효율

# syntax=docker/dockerfile:1.4

# 자주 바뀌는 ARG는 레이어 맨 뒤에 선언
FROM node:20-alpine

COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm npm ci

COPY . .

# 빌드 시 주입, 소스 코드에 삽입
ARG BUILD_VERSION=dev
RUN echo "BUILD_VERSION=$BUILD_VERSION" > .build-info
docker build --build-arg BUILD_VERSION=$(git rev-parse --short HEAD) -t myapp .

디버그: buildx bake

# bake 파일로 복잡한 빌드 정의
cat > docker-bake.hcl <<'EOF'
group "default" {
  targets = ["app", "test"]
}

target "app" {
  dockerfile = "Dockerfile"
  target = "production"
  tags = ["myapp:latest"]
}

target "test" {
  dockerfile = "Dockerfile"
  target = "test"
  tags = ["myapp:test"]
}
EOF

# 동시에 두 타겟 빌드
docker buildx bake

BuildKit은 Docker 빌드의 성능과 보안을 모두 개선한다. 캐시 마운트와 병렬 빌드로 속도를, 시크릿 마운트와 SSH 포워딩으로 보안을 챙길 수 있다.


지난 글: Docker 컨테이너 시간대(Timezone) 불일치 해결

다음 글: Docker 네임스페이스와 cgroups — 컨테이너 격리의 원리


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