도커 고급 강좌 #1 BuildKit과 buildx — 빌더의 정체

8 분 소요

중급 시리즈는 BuildKit을 “기본으로 켜져 있는 무언가” 정도로 다뤘습니다. 고급 시리즈는 그 안을 한 번 들여다봅니다. 이번 글은 빌더 자체의 구조, 그리고 BuildKit 위에 얹힌 도구 buildx의 쓰임을 정리합니다.

도커 고급 강좌 시리즈에서 이번 글의 위치:

  • #1 BuildKit과 buildx ← 이번 글
  • #2 멀티 아키텍처 이미지 (linux/amd64 + arm64)
  • #3 이미지 보안 — non-root, distroless, scan(Trivy)
  • #4 SBOM과 서명(cosign)
  • #5 리소스 제한과 cgroups
  • #6 프로덕션 운영 — restart 정책, healthcheck, graceful shutdown

두 빌더의 짧은 역사 #

도커 빌드 엔진이 두 세대로 갈렸다는 건 중급 #2에서 짚었습니다. 한 단계만 더 풀어봅니다.

Legacy builderdocker build가 원래 가지고 있던 빌더입니다. 단순한 모델로, Dockerfile을 위에서 아래로 한 줄씩 실행하며 각 줄마다 임시 컨테이너를 만들었다 commit 하는 방식입니다. 직관적이지만 한계가 분명했습니다. 병렬 처리가 없고, 캐시 모델이 단순하고, 이미지에 비밀이 포함될 위험이 컸습니다.

BuildKit은 도커 회사 외부에서 시작해(Moby 프로젝트), 이후 도커에 통합된 차세대 빌더입니다. 핵심 아이디어 두 개:

  1. Dockerfile을 그래프(LLB)로 컴파일 — 의존성 없는 노드는 병렬로 실행
  2. frontend / backend 분리 — Dockerfile 외의 형식도 입력으로 받을 수 있는 확장 가능한 구조
BuildKit의 빌드 흐름
   Dockerfile        bake.hcl       다른 frontend
       │               │                │
       ▼               ▼                ▼
   ┌──────────────────────────────────────────┐
   │   Frontend (Dockerfile parser, etc.)      │
   └────────────────┬─────────────────────────┘
                    │ produces
   ┌──────────────────────────────────────────┐
   │   LLB — Low-Level Build graph             │
   │   (의존성 그래프, 캐시 키, 병렬 가능)        │
   └────────────────┬─────────────────────────┘
                    │ executed by
   ┌──────────────────────────────────────────┐
   │   BuildKit backend                        │
   │   (snapshotter, cache, executor)          │
   └────────────────┬─────────────────────────┘
                    │ outputs
        이미지 / tar / 로컬 디렉터리 / 등

LLB는 사용자가 직접 만질 일은 거의 없지만, “이 줄과 저 줄이 정말 동시에 실행되네” 같은 BuildKit의 동작을 이해할 때 머리에 둘 만한 모델입니다.

# syntax= 한 줄의 정체 #

중급 #2에서 짚은 이 줄.

frontend 명시
# syntax=docker/dockerfile:1.7
FROM ...

이게 frontend 지정입니다. Dockerfile parser 자체를 어떤 버전을 쓸지를 가리킵니다. BuildKit은 이 한 줄을 보고 그 frontend를 OCI 이미지로 받아와 컴파일에 씁니다. 새 Dockerfile 기능(RUN --mount, COPY --link, --exclude 등)이 추가될 때마다 frontend 버전이 올라갑니다.

신뢰할 수 있는 핀:

  • docker/dockerfile:1 — 1.x의 latest. 자주 쓰는 기본
  • docker/dockerfile:1.7 — 1.7 시리즈로 핀
  • docker/dockerfile:1.7.0 — 정확히 한 버전으로

운영 / 재현이 중요한 환경에선 마이너 버전까지 핀하는 편이 안전합니다.

buildx — BuildKit 위의 CLI #

docker builddocker buildx build의 차이를 정리하면:

docker builddocker buildx build
빌더데몬 내장 (BuildKit 또는 legacy)외부 builder 인스턴스 (BuildKit)
멀티 플랫폼안 됨--platform linux/amd64,linux/arm64가능
캐시 임포트/익스포트제한적type=registry, type=gha 등 자유
--output이미지로만tar, oci, local, registry 등 다양
병렬 빌더단일여러 인스턴스 가능

요약하면 buildx는 BuildKit의 풀 기능에 접근하는 CLI 입니다. 최근 도커는 두 명령을 거의 동등하게 묶어두려는 추세고, docker build가 내부적으로 buildx를 부르는 형태로 굴러가고 있습니다. 새 코드는 그냥 buildx로 적는 편이 깔끔합니다.

Builder 인스턴스 — 빌드를 어디서 도느냐 #

buildx의 핵심 개념은 builder 인스턴스 입니다. 빌드를 실제로 도는 BuildKit 데몬을 어디에 둘지 정할 수 있습니다.

현재 빌더 보기
docker buildx ls
# NAME/NODE          DRIVER/ENDPOINT             STATUS    PLATFORMS
# default *          docker                                
#  \_ default        \_ unix:///var/run/...      running   linux/amd64, linux/arm64

기본은 default라는 이름의 docker 드라이버입니다. 그러면 빌드는 도커 데몬 자체가 들고 있는 BuildKit 위에서 돕니다.

Driver 종류 #

Driver어디서 돔멀티 플랫폼
docker (기본)도커 데몬 내장 BuildKit호스트 아키만
docker-container별도 컨테이너로 띄운 BuildKit가능 (QEMU emulation)
kubernetesk8s 클러스터의 pod가능
remote원격 BuildKit 데몬환경에 따라

docker 드라이버의 가장 큰 한계는 멀티 플랫폼 빌드가 안 된다는 점 입니다. 그래서 docker-container 드라이버로 새 빌더를 만드는 게 거의 첫 단계입니다.

새 builder 만들기 #

docker-container 드라이버 빌더
docker buildx create --name multi --driver docker-container --use
docker buildx ls
# NAME/NODE          DRIVER/ENDPOINT             STATUS    PLATFORMS
# multi *            docker-container                      
#  \_ multi0         \_ unix:///var/run/...      inactive
# default            docker                                
#  \_ default        \_ unix:///var/run/...      running   linux/amd64, linux/arm64
  • --name multi — 빌더 이름
  • --driver docker-container — 별도 컨테이너 BuildKit
  • --use — 만들고 바로 활성 빌더로 지정

inactive 상태인 건 아직 처음 빌드를 안 돌렸다는 뜻입니다. 첫 빌드를 돌리면 BuildKit 컨테이너(moby/buildkit:...)가 알아서 뜹니다.

명시적으로 부트스트랩
docker buildx inspect multi --bootstrap
# 즉시 BuildKit 컨테이너를 띄움

빌더 전환 / 삭제:

빌더 다루기
docker buildx use default     # 기본 빌더로
docker buildx use multi       # multi 빌더로
docker buildx rm multi        # 삭제
docker buildx prune --all     # 모든 빌더의 캐시 정리

--output — 빌드 결과의 출력처 #

buildx의 표현력이 가장 잘 드러나는 지점입니다. 빌드 결과를 이미지로만 받지 않고 다양한 형태로 뽑을 수 있습니다.

자주 쓰는 형태들
# 1)도커 이미지로 (load) — 로컬 이미지 캐시에 들어감
docker buildx build --output type=docker -t myapp .
# 또는 (단축)
docker buildx build --load -t myapp .

# 2) 레지스트리로 직접 push
docker buildx build --output type=registry -t ghcr.io/me/myapp:1.0 .
# 또는 (단축)
docker buildx build --push -t ghcr.io/me/myapp:1.0 .

# 3) tar 파일로
docker buildx build --output type=tar,dest=myapp.tar .

# 4) OCI 이미지 layout으로 (디렉터리)
docker buildx build --output type=oci,dest=myapp-oci/ .

# 5) 빌드 결과의 파일시스템을 로컬로 추출 (이미지 안 만듦)
docker buildx build --output type=local,dest=./out --target builder .

type=local은 흥미로운 옵션입니다. 이미지를 만들지 않고 빌드 단계의 결과 파일만 추출합니다. CI가 빌드 산출물(예: 컴파일된 바이너리)을 도커와 무관하게 다음 단계로 넘기고 싶을 때 유용합니다.

Go 바이너리만 추출
docker buildx build --target builder --output type=local,dest=./bin .
ls bin/
# myapp

멀티 플랫폼 빌드 — 짧은 맛보기 #

docker-container 드라이버 위에서 한 명령으로 두 아키텍처를 동시에 빌드할 수 있습니다.

amd64 + arm64 동시
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t ghcr.io/me/myapp:1.0 \
  --push .

이때 만들어지는 건 두 아키텍처 이미지를 묶은 manifest list (OCI image index)입니다. 사용자는 자기 아키텍처에 맞는 이미지를 자동으로 받습니다. 깊이는 #2에서 본격적으로 다룹니다.

--load--platform의 충돌 한 가지: 도커 데몬의 로컬 이미지 캐시는 single-platform만 지원해서, 여러 플랫폼을 한 번에 --load 하는 건 안 됩니다. --push 하거나, 한 플랫폼만 골라 --load.

캐시 익스포트 — --cache-to/from 깊이 #

중급 #2에서 한 번 짚었던 외부 캐시인데, 옵션이 좀 더 있습니다.

레지스트리 캐시 — 옵션들
docker buildx build \
  --cache-to=type=registry,ref=ghcr.io/me/myapp:cache,mode=max,compression=zstd \
  --cache-from=type=registry,ref=ghcr.io/me/myapp:cache \
  --push -t ghcr.io/me/myapp:1.0 .
옵션의미
mode=min (기본)결과 레이어만 캐시
mode=max모든 중간 스테이지 캐시 (멀티스테이지 효과 큼)
compression=gzip|zstd|estargz캐시 레이어 압축. zstd가 빠르고 작음
image-manifest=trueOCI 표준 manifest 형식으로 (호환성↑)
oci-mediatypes=trueOCI media type 강제

멀티스테이지가 깊으면 mode=max가 큰 차이를 만듭니다. 빌더 / 의존성 / 테스트 스테이지의 캐시까지 모두 살아 있어, 작은 변경에도 거의 즉시 빌드가 끝납니다. 단점은 캐시 자체가 커진다는 점입니다.

docker buildx bake — 선언적 다단 빌드 #

여러 이미지를 한 번에 빌드해야 할 때(monorepo: web + worker + admin), 셸 스크립트로 buildx build를 여러 번 부르는 건 금방 지저분해집니다. bake가 이걸 선언적으로 풀어줍니다.

docker-bake.hcl
group "default" {
  targets = ["web", "worker", "admin"]
}

target "web" {
  context = "./web"
  tags    = ["ghcr.io/me/web:1.0", "ghcr.io/me/web:latest"]
  platforms = ["linux/amd64", "linux/arm64"]
  cache-from = ["type=registry,ref=ghcr.io/me/web:cache"]
  cache-to   = ["type=registry,ref=ghcr.io/me/web:cache,mode=max"]
}

target "worker" {
  context = "./worker"
  tags    = ["ghcr.io/me/worker:1.0"]
  platforms = ["linux/amd64", "linux/arm64"]
}

target "admin" {
  inherits = ["web"]
  context  = "./admin"
  tags     = ["ghcr.io/me/admin:1.0"]
}
실행
docker buildx bake --push
# 세 이미지를 동시에, 멀티 플랫폼으로, 캐시까지 묶어서

bake의 강점:

  • 병렬 빌드web, worker, admin이 서로 의존 없으면 동시 빌드
  • **inherits**로 공통 설정 상속
  • 변수 / 함수 — HCL의 variable, function으로 환경별 분기
  • JSON / HCL / compose 파일 — 셋 다 입력 가능 (compose.yaml의 build: 정의도 직접 bake의 입력)
compose.yaml을 bake 입력으로
docker buildx bake -f compose.yaml --push

같은 정의를 두 번 적지 않고 — compose가 정의한 빌드를 그대로 CI에서 사용하는 패턴입니다.

bake의 흔한 패턴 — 환경별 분기 #

dev / prod 분기
variable "TAG" { default = "dev" }
variable "REGISTRY" { default = "ghcr.io/me" }

target "_common" {
  platforms = ["linux/amd64", "linux/arm64"]
}

target "web" {
  inherits = ["_common"]
  context  = "./web"
  tags     = ["${REGISTRY}/web:${TAG}"]
}
환경 주입
docker buildx bake web                              # dev 태그
TAG=1.2.3 REGISTRY=ghcr.io/me docker buildx bake web --push   # 운영

CI의 빌드 스크립트가 매우 가벼워집니다.

기타 자주 쓰는 buildx 옵션 #

유용한 옵션들
--progress=plain         # 풀 로그 (BuildKit의 한 줄 요약 대신)
--progress=quiet         # 최소 출력
--no-cache-filter=test   # 특정 스테이지만 캐시 무시
--build-context other=./other-dir   # 추가 빌드 컨텍스트 명명
--allow=network.host     # 빌드 안에서 호스트 네트워크 사용 (보안 의식)

--build-context는 monorepo에서 다른 디렉터리를 컨텍스트로 가져올 때 유용합니다. Dockerfile에서 COPY --from=other ./shared/types /app/types처럼 받습니다.

정리 #

이번 글에서 잡은 그림:

  • BuildKit = LLB (그래프) + frontend (Dockerfile parser) + backend. # syntax=가 frontend 핀
  • **buildx**는 BuildKit의 풀 기능 CLI. 새 코드는 buildx로
  • builder 인스턴스가 핵심 개념 — docker 드라이버는 멀티플랫폼 안 됨, docker-container 드라이버로 새 빌더 만드는 게 거의 첫 단계
  • **--output**으로 결과를 이미지 / tar / OCI / 로컬 디렉터리 / 레지스트리로 자유롭게
  • 외부 캐시는 mode=max + compression=zstd가 멀티스테이지에 효과적
  • **docker buildx bake**로 monorepo / 다단 빌드를 선언적으로. compose.yaml 입력도 가능

다음 글(#2 멀티 아키텍처 이미지)에서는 --platform 한 옵션 뒤의 맥락 — manifest list, QEMU emulation, 네이티브 멀티 아키 빌더, 그리고 Apple Silicon에서 빌드한 이미지가 운영 서버에서 안 도는 흔한 사고를 어떻게 피할지 깊게 봅니다.

X