LY Corporation Tech Blog

支持 LY Corporation 和 LY Corporation Group (LINE Plus, LINE Taiwan and LINE Vietnam) 服務,宣傳技術和開發文化。

Docker Bake 食譜公開:一次烤出多種 Image

如果你是一個工程師你肯定用過 Docker,應該很容易遇到一個狀況:

一開始只是:

docker build -t my-app .

後來變成:

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --build-arg NODE_VERSION=22 \
  --secret id=npmrc,src=$HOME/.npmrc \
  --cache-from type=gha \
  --cache-to type=gha,mode=max \
  --target production \
  --tag ghcr.io/acme/web:${GIT_SHA} \
  --tag ghcr.io/acme/web:latest \
  --push \
  .

然後當你以為這樣已經夠長了,某天因為需求大增,又需要多做一個不同版本的 image 出來。

例如:

  • 一張 production image:真正部署到 server、VM、Kubernetes 裡跑服務。
  • 一張 uploader image:只負責把 build 出來的靜態檔案上傳到 CDN。

這時候你很可能又複製一份差不多的 build command,改一下 --target--tag然後祈禱 CI 不要哪天被自己搞爆

這篇要聊的 Docker Bake,就是拿來處理這種情境的。

它不是要取代 Dockerfile,也不是重學一套全新的 build 系統。而是:

Dockerfile 負責定義「怎麼做菜」,Docker Bake 負責定義「今天要出哪些菜」。

也就是說,Dockerfile 還是你的料理步驟;Bake 則是把 build target、tag、platform、secret、cache、push 等設定收斂成一份可以被版本控管的「食譜」。


這東西是什麼時候出的?

如果要講「正式被端上桌」的時間點,Docker Bake 是在 Docker Desktop 4.38 時宣布 **General Availability。

Docker 官方在 2025-02-05 發布 Docker Desktop 4.38 的文章裡,明確提到 Bake 進入 GA。官方也把 Bake 描述成一個可以簡化並加速 Docker builds 的 orchestration tool。[1]

不過 Bake 不是 2025 年才突然冒出來的新玩具。它前面其實已經在 Docker Buildx 裡以實驗功能存在一段時間,只是到了 Docker Desktop 4.38 才正式 GA。


為什麼需要 Bake?

假設我們有一個前端專案。

build 完之後,通常會有兩種東西:

  1. 服務本體要用的東西

    • 例如 Node server。
    • 例如 SSR bundle。
    • 例如 Next.js standalone output。
    • 例如 Nuxt / Nitro server output。
  2. 要上傳到 CDN 的靜態資源

    • 例如 JS。
    • 例如 CSS。
    • 例如圖片、字型、manifest。
    • 例如 dist/client.next/static.output/public 這種資料夾。

最直覺的做法,可能是把所有東西塞在同一張 production image 裡。

但我自己會不太喜歡這樣,因為上傳 CDN 這件事通常需要額外的工具或權限:

  • AWS CLI
  • Cloudflare R2 / Wrangler
  • GCP gsutil
  • Azure CLI
  • CDN purge 工具
  • 各種 access token 或 secret

這些東西如果全部塞進 production image,會有幾個問題:

  • production image 變肥。
  • production runtime 多了它其實不需要的工具。
  • 上傳 CDN 的權限跟正式服務綁太近。
  • CI 裡很難清楚切開「部署服務」跟「上傳靜態檔」。
  • 可能會把上傳需要的機密金鑰不小心暴露在 production image

比較乾淨的方式是:

同一份 source code
  -> 同一份 build output
    -> production image:只放服務要跑的東西
    -> uploader image:只放上傳 CDN 需要的靜態檔和工具

這就是這篇的主線情境。

我們要用 Docker Bake 做到:

共用備料:Base、Deps、Builder 不重複
調味集中:環境變數、Tag、Secret 集中管理
分開出餐:Production 與 Uploader 各拿各的

Dockerfile 是料理步驟,Bake 是出餐菜單

Dockerfile 在管什麼?

Dockerfile 管的是「怎麼 build」。

例如:

  • 從哪個 base image 開始。
  • 怎麼安裝 dependencies。
  • 怎麼跑 build。
  • 最後要 copy 哪些檔案。
  • container 啟動時要跑什麼 command。

Docker Bake 在管什麼?

Docker Bake 管的是「要 build 哪些 target,以及每個 target 要用什麼設定」。

例如:

  • 要不要 build production
  • 要不要 build uploader
  • image tag 要怎麼命名。
  • 要不要 multi-platform。
  • secret 要怎麼傳。
  • cache 要怎麼用。
  • local build 要 --load,CI build 要 --push

Docker 官方 CLI 文件也把 docker buildx bake 定義成 high-level build command,而且指定的 targets 會在 build 過程中平行執行。[2]

所以 Bake 的定位可以這樣記:

Dockerfile:做菜步驟
Bake file:點菜單 / 出餐規則
Buildx / BuildKit:真正幫你開火做菜的人

共用備料:Base、Deps、Builder 不重複

先來看 Dockerfile。

下面這份 Dockerfile 用 Node 專案當例子,但概念不限框架。你可以把它套到 Next.js、Nuxt、Vite、Remix,或任何會產出 server bundle 和 static assets 的專案。

# syntax=docker/dockerfile:1.7
# 共同底料:大家都從同一個 Node base 開始
FROM node:22-alpine AS base
WORKDIR /app

# 共同依賴:只要 package lock 沒變,這層就可以重用
FROM base AS deps
COPY package.json package-lock.json ./

# 如果有 private npm registry,可以透過 BuildKit secret 掛進來
# 不要把 .npmrc COPY 進 image 裡
RUN --mount=type=cache,target=/root/.npm \
    --mount=type=secret,id=npmrc,target=/root/.npmrc,required=false \
    npm ci

# 共同 build:production 跟 uploader 都吃這份產物
FROM deps AS builder
COPY . .
RUN npm run build

# 第一份成品:真正要部署的 production image
FROM base AS production
ENV NODE_ENV=production

# 這邊假設 build 後會有 server output
# 你可以依照自己的框架改成 .next/standalone、.output/server、dist/server 等
COPY --from=builder /app/dist/server ./server
COPY --from=builder /app/package.json ./package.json

EXPOSE 3000
CMD ["node", "server/index.js"]

# 第二份成品:只負責上傳 CDN 的 uploader image
FROM alpine:3.20 AS uploader

RUN apk add --no-cache \
    bash \
    ca-certificates \
    aws-cli

WORKDIR /upload

# 這邊假設 build 後會有靜態 assets
# 你可以改成 dist/client、.output/public、.next/static 等
COPY --from=builder /app/dist/client ./public
COPY docker/upload-to-cdn.sh /usr/local/bin/upload-to-cdn

RUN chmod +x /usr/local/bin/upload-to-cdn

ENTRYPOINT ["upload-to-cdn"]

這裡的重點不是 Node,也不是 AWS CLI。

重點是這個結構:

base
  -> deps
    -> builder
      -> production
      -> uploader

basedepsbuilder 是共用備料。

production  uploader 是最後分開出餐。

Production image 只拿服務需要的東西:

COPY --from=builder /app/dist/server ./server

Uploader image 只拿 CDN 需要的靜態檔:

COPY --from=builder /app/dist/client ./public

這樣做的好處是:

  • 不用為了 production 跟 uploader 各 build 一次 app。
  • production image 不需要安裝 AWS CLI、wrangler 或其他上傳工具。
  • uploader image 不需要長期跑在正式環境。
  • 兩張 image 吃同一份 build output,版本比較不容易對不起來。

也就是說:

build 一次,分裝兩份,各拿各的。


小補充:Uploader image 裡的上傳腳本

剛剛 Dockerfile 裡有 copy 一個 docker/upload-to-cdn.sh

它可以長這樣:

#!/usr/bin/env bash
set -euo pipefail

: "${CDN_BUCKET:?CDN_BUCKET is required}"
: "${CDN_PREFIX:=assets}"
: "${CDN_ENDPOINT_URL:?CDN_ENDPOINT_URL is required}"

echo "Uploading static assets to CDN..."
echo "Bucket: ${CDN_BUCKET}"
echo "Prefix: ${CDN_PREFIX}"

aws s3 sync /upload/public "s3://${CDN_BUCKET}/${CDN_PREFIX}" \
  --endpoint-url "${CDN_ENDPOINT_URL}" \
  --delete \
  --cache-control "public,max-age=31536000,immutable"

echo "CDN upload finished."


這邊故意不把 AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY 寫進 image。

Uploader image 只負責包兩種東西:

要上傳的靜態檔。
上傳需要的工具和腳本。

真正的 credentials 應該在 CI runtime 才注入。

例如:

docker run --rm \
  -e AWS_ACCESS_KEY_ID \
  -e AWS_SECRET_ACCESS_KEY \
  -e AWS_DEFAULT_REGION=auto \
  -e CDN_BUCKET="my-cdn-bucket" \
  -e CDN_PREFIX="assets" \
  -e CDN_ENDPOINT_URL="https://example-account.r2.cloudflarestorage.com" \
  ghcr.io/acme/web-cdn-uploader:${TAG}

這件事很重要。

因為 production image 不該知道怎麼上傳 CDN,更不該帶著可以上傳 CDN 的權限一起上線。


調味集中:環境變數、Tag、Secret 集中管理

接著來看 docker-bake.hcl

這份檔案就是我們的食譜。

Docker Bake 的 bake file 可以用 HCL、JSON、YAML / Compose file 來寫;官方文件也提到 HCL 是比較常見的格式。[3]

這裡我們用 HCL。

variable "REGISTRY" {
  default = "ghcr.io/acme"
}

variable "IMAGE_NAME" {
  default = "web"
}

variable "TAG" {
  default = "dev"
}

variable "NODE_VERSION" {
  default = "22-alpine"
}

variable "HOME" {
  default = null
}

variable "PRODUCTION_PLATFORMS" {
  type    = list(string)
  default = ["linux/amd64", "linux/arm64"]
}

# 共用設定:context、Dockerfile、build args、secret 都放這裡
target "_common" {
  context    = "."
  dockerfile = "Dockerfile"

  args = {
    NODE_VERSION = NODE_VERSION
  }

  # 有 private npm package 的話,就集中在這裡設定
  # Dockerfile 裡用 --mount=type=secret,id=npmrc 來讀
  secret = [
    {
      type = "file"
      id   = "npmrc"
      src  = "${HOME}/.npmrc"
    }
  ]

  labels = {
    "org.opencontainers.image.source" = "https://github.com/acme/web"
  }
}

# CI cache 設定獨立出來,之後要共用比較方便
target "_cache" {
  cache-from = ["type=gha"]
  cache-to   = ["type=gha,mode=max"]
}

# 真正要部署的 image
target "production" {
  inherits = ["_common", "_cache"]

  target    = "production"
  platforms = PRODUCTION_PLATFORMS

  tags = [
    "${REGISTRY}/${IMAGE_NAME}:${TAG}",
    "${REGISTRY}/${IMAGE_NAME}:latest"
  ]
}

# 只負責上傳 CDN 的 image
target "uploader" {
  inherits = ["_common", "_cache"]

  target    = "uploader"
  platforms = ["linux/amd64"]

  tags = [
    "${REGISTRY}/${IMAGE_NAME}-cdn-uploader:${TAG}"
  ]
}

# 預設一次 build production + uploader
group "default" {
  targets = ["production", "uploader"]
}

# CI release 時也可以明確呼叫 release group
group "release" {
  targets = ["production", "uploader"]
}

這份設定做了幾件事。


1. Tag 不要散在 CI 裡

以前你可能會在 GitHub Actions、GitLab CI 或 shell script 裡寫:

--tag ghcr.io/acme/web:${GIT_SHA}
--tag ghcr.io/acme/web:latest
--tag ghcr.io/acme/web-cdn-uploader:${GIT_SHA}

現在集中在 Bake file:

tags = [
  "${REGISTRY}/${IMAGE_NAME}:${TAG}",
  "${REGISTRY}/${IMAGE_NAME}:latest"
]

CI 只要負責傳變數:

TAG="(gitrevparseshortHEAD(git rev-parse --short HEAD" docker buildx bake release --push

這樣 tag 規則就會跟著 repo 一起被 review、被版本控管。


2. Build args 集中

args = {
  NODE_VERSION = NODE_VERSION
}

Dockerfile 裡對應:

ARG NODE_VERSION=22-alpine
FROM node:${NODE_VERSION} AS base

以後要從 Node 22 換成 Node 24,不用去 CI 裡翻一堆 build command。

改這裡就好。


3. Secret 集中,但不要跟 runtime secret 搞混

Bake 裡可以設定 build-time secret:

secret = [
  {
    type = "file"
    id   = "npmrc"
    src  = "{HOME/.npmrc"
  }
]

Dockerfile 裡用:

RUN --mount=type=secret,id=npmrc,target=/root/.npmrc,required=false \
    npm ci

這種適合放什麼?

很適合放 build 時才需要的東西,例如:

  • private npm registry token
  • private package registry credentials
  • build 時需要讀的 cloud credentials

但它不適合拿來放 uploader image 執行時才需要的 CDN credentials。

簡單分法:

build image 的時候需要:Bake secret
run container 的時候需要:docker run -e / CI secret / workload identity

也就是:

npm install 需要的 token -> build-time secret
上傳 CDN 需要的 key -> runtime secret

不要把這兩件事混在一起。


4. inherits 讓你不要一直複製貼上

這段很關鍵:

target "production" {
  inherits = ["_common", "_cache"]
  target   = "production"
}

target "uploader" {
  inherits = ["_common", "_cache"]
  target   = "uploader"
}

production  uploader 都會繼承:

  • context
  • dockerfile
  • args
  • secret
  • labels
  • cache-from
  • cache-to

但它們又可以各自定義:

  • Dockerfile target
  • platforms
  • tags

Docker 官方文件也說,Bake target 可以透過 inherits 從其他 target 重用 attributes,而且可以一次繼承多個 target。[3:1]

我自己會覺得這是 Bake 最實用的地方之一。

因為它讓你可以把 build 設定拆成幾種小積木:

target "_common" {}
target "_cache" {}
target "_release" {}
target "_local" {}

然後再組成真正要 build 的 target。


分開出餐:Production 與 Uploader 各拿各的

現在我們有了 Dockerfile,也有了 docker-bake.hcl

實際使用時就簡單很多。


先看 Bake 展開後會做什麼

剛開始導入 Bake,我會很推薦先用:

docker buildx bake --print

或只看某個 target:

docker buildx bake production --print

--print 不會真的 build,它會把 Bake 設定展開成最後的 build options。Docker 官方 CLI 文件也有列出 --print 是用來 print options without building。[2:1]

這可以幫你檢查:

  • tag 對不對。
  • target 對不對。
  • platform 對不對。
  • secret 有沒有掛對。
  • group 裡面到底包含哪些 target。

我覺得這有點像在烤之前先看訂單,不然真的送進烤箱才發現 tag 打錯,會很煩。


本機只 build production

docker buildx bake production --load

--load 會把 build 出來的 image 載回本機 Docker image store,適合本機測試。


本機只 build uploader

docker buildx bake uploader --load

然後可以試跑 uploader:

 docker run --rm \
  -e CDN_BUCKET="my-cdn-bucket" \
  -e CDN_PREFIX="assets" \
  -e CDN_ENDPOINT_URL="https://example-account.r2.cloudflarestorage.com" \
  ghcr.io/acme/web-cdn-uploader:dev
 

當然,真的要上傳時還是要補上 cloud credentials。


預設一次 build 兩張

因為我們有寫:

group "default" {
  targets = ["production", "uploader"]
}

所以直接跑:

docker buildx bake

就會 build:

production
uploader

Docker Bake 的 group 可以讓你一次呼叫多個 targets。這很適合一個 repo 會產出多張 image 的情境。[3:2]


CI release:一次 build 並 push 兩張 image

CI 裡大概可以長這樣:

export TAG="$(git rev-parse --short HEAD)"
export REGISTRY="ghcr.io/acme"
export IMAGE_NAME="web"

docker buildx bake release --push

--push 會把 build 結果推到 registry。Docker 官方 CLI 文件裡也說 --push  --set=*.output=type=registry 的 shorthand。[2:2]

推完之後,你會得到兩張 image:

ghcr.io/acme/web:{TAG
ghcr.io/acme/web:latest
ghcr.io/acme/web-cdn-uploader:{TAG

接著 CI 可以再跑 uploader image:

docker run --rm \
  -e AWS_ACCESS_KEY_ID \
  -e AWS_SECRET_ACCESS_KEY \
  -e AWS_DEFAULT_REGION=auto \
  -e CDN_BUCKET="my-cdn-bucket" \
  -e CDN_PREFIX="assets" \
  -e CDN_ENDPOINT_URL="https://example-account.r2.cloudflarestorage.com" \
  ghcr.io/acme/web-cdn-uploader:${TAG}

這樣整條線會變得很清楚:

1. build production image
2. build uploader image
3. push images
4. run uploader image to upload static assets
5. deploy production image

而且 production image 跟 uploader image 是同一個 commit、同一份 build output 來的。


一個比較完整的 CI 情境

假設你用 GitHub Actions,概念上可以像這樣:

 name: release

on:
  push:
    branches:
      - main

jobs:
  build:
    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 images with Bake
        env:
          REGISTRY: ghcr.io/acme
          IMAGE_NAME: web
          TAG: ${{ github.sha }}
        run: docker buildx bake release --push

      - name: Upload static assets to CDN
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.CDN_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.CDN_SECRET_ACCESS_KEY }}
          AWS_DEFAULT_REGION: auto
          CDN_BUCKET: my-cdn-bucket
          CDN_PREFIX: assets
          CDN_ENDPOINT_URL: ${{ secrets.CDN_ENDPOINT_URL }}
          TAG: ${{ github.sha }}
        run: |
          docker run --rm \
            -e AWS_ACCESS_KEY_ID \
            -e AWS_SECRET_ACCESS_KEY \
            -e AWS_DEFAULT_REGION \
            -e CDN_BUCKET \
            -e CDN_PREFIX \
            -e CDN_ENDPOINT_URL \
            ghcr.io/acme/web-cdn-uploader:${TAG}
 

這樣做的好處是:

  • Build 規則在 docker-bake.hcl,不是散在 GitHub Actions 裡。
  • CI 只負責提供環境變數和 secret。
  • production image 跟 uploader image 的命名規則一致。
  • uploader 的權限只在 upload 那一步出現。

什麼時候你會需要 Docker Bake?

我不會說每個專案都要上 Bake。

如果你的專案真的只有一張 image,build command 也只有:

docker build -t my-app .

那你確實用不太到 bake

但只要你開始遇到下面幾種情境,Bake 就會變得很香。


情境 1:同一個 repo 會生出多張 image

例如:

web
worker
scheduler
migration
cdn-uploader

這種很適合 Bake。

因為你可以用 group 管理:

group "default" {
  targets = ["web", "worker", "scheduler"]
}

group "release" {
  targets = ["web", "worker", "scheduler", "cdn-uploader"]
}

情境 2:同一份 Dockerfile 有多個 target

例如:

deps
builder
test
lint
production
uploader

你不想每次都手打:

docker buildx build --target production ...
docker buildx build --target uploader ...
docker buildx build --target test ...

Bake 可以幫你把這些 target 變成固定菜單。


情境 3:build command 開始太長

只要你的 command 開始出現這些東西:

--platform
--build-arg
--secret
--cache-from
--cache-to
--target
--tag
--push

就可以考慮 Bake。

不是因為 Bake 比較潮,而是因為你應該把這些規則從 shell script 裡搬出來。


情境 4:local 跟 CI 想用同一套 build 規則

這點我覺得很重要。

很多專案最後會變成:

local build:工程師自己打一套 command
CI build:YAML 裡另一套 command
release build:某個 shell script 又一套 command

時間久了,三套 command 慢慢長得不一樣。

然後某天就會出現:

為什麼我 local build 可以,CI build 不行?

Bake 可以讓 build 規則集中在 repo 裡。

local 要跑:

docker buildx bake production --load

CI 要跑:

docker buildx bake release --push

用的是同一份 docker-bake.hcl


這篇的完整檔案整理

最後把三個檔案整理在一起。


Dockerfile

# syntax=docker/dockerfile:1.7

ARG NODE_VERSION=22-alpine

FROM node:${NODE_VERSION} AS base
WORKDIR /app
ENV CI=true

FROM base AS deps
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
    --mount=type=secret,id=npmrc,target=/root/.npmrc,required=false \
    npm ci

FROM deps AS builder
COPY . .
RUN npm run build

FROM base AS production
ENV NODE_ENV=production
COPY --from=builder /app/dist/server ./server
COPY --from=builder /app/package.json ./package.json
EXPOSE 3000
CMD ["node", "server/index.js"]

FROM alpine:3.20 AS uploader
RUN apk add --no-cache bash ca-certificates aws-cli
WORKDIR /upload
COPY --from=builder /app/dist/client ./public
COPY docker/upload-to-cdn.sh /usr/local/bin/upload-to-cdn
RUN chmod +x /usr/local/bin/upload-to-cdn
ENTRYPOINT ["upload-to-cdn"]

docker-bake.hcl

variable "REGISTRY" {
  default = "ghcr.io/acme"
}

variable "IMAGE_NAME" {
  default = "web"
}

variable "TAG" {
  default = "dev"
}

variable "NODE_VERSION" {
  default = "22-alpine"
}

variable "HOME" {
  default = null
}

variable "PRODUCTION_PLATFORMS" {
  type    = list(string)
  default = ["linux/amd64", "linux/arm64"]
}

target "_common" {
  context    = "."
  dockerfile = "Dockerfile"

  args = {
    NODE_VERSION = NODE_VERSION
  }

  secret = [
    {
      type = "file"
      id   = "npmrc"
      src  = "${HOME}/.npmrc"
    }
  ]

  labels = {
    "org.opencontainers.image.source" = "https://github.com/acme/web"
  }
}

target "_cache" {
  cache-from = ["type=gha"]
  cache-to   = ["type=gha,mode=max"]
}

target "production" {
  inherits = ["_common", "_cache"]

  target    = "production"
  platforms = PRODUCTION_PLATFORMS

  tags = [
    "${REGISTRY}/${IMAGE_NAME}:${TAG}",
    "${REGISTRY}/${IMAGE_NAME}:latest"
  ]
}

target "uploader" {
  inherits = ["_common", "_cache"]

  target    = "uploader"
  platforms = ["linux/amd64"]

  tags = [
    "${REGISTRY}/${IMAGE_NAME}-cdn-uploader:${TAG}"
  ]
}

group "default" {
  targets = ["production", "uploader"]
}

group "release" {
  targets = ["production", "uploader"]
}

docker/upload-to-cdn.sh

#!/usr/bin/env bash
set -euo pipefail

: "${CDN_BUCKET:?CDN_BUCKET is required}"
: "${CDN_PREFIX:=assets}"
: "${CDN_ENDPOINT_URL:?CDN_ENDPOINT_URL is required}"

aws s3 sync /upload/public "s3://${CDN_BUCKET}/${CDN_PREFIX}" \
  --endpoint-url "${CDN_ENDPOINT_URL}" \
  --delete \
  --cache-control "public,max-age=31536000,immutable"

常用指令整理

看 Bake 會怎麼 build,但不真的 build

docker buildx bake --print

本機 build production image

docker buildx bake production --load

本機 build uploader image

docker buildx bake uploader --load

CI build 並 push production + uploader

TAG="(gitrevparseshortHEAD(git rev-parse --short HEAD" docker buildx bake release --push

跑 uploader,把 assets 上傳到 CDN

docker run --rm \
  -e AWS_ACCESS_KEY_ID \
  -e AWS_SECRET_ACCESS_KEY \
  -e AWS_DEFAULT_REGION=auto \
  -e CDN_BUCKET="my-cdn-bucket" \
  -e CDN_PREFIX="assets" \
  -e CDN_ENDPOINT_URL="https://example-account.r2.cloudflarestorage.com" \
  ghcr.io/acme/web-cdn-uploader:${TAG}

結語:不再手刻一堆 build 咒語了

Docker Bake 最吸引我的地方是可以更好管控 build 流程。

它解決的是那種每個專案長大後都很容易遇到的問題:

build command 一開始很短,後來越長越可怕。
一開始只有一張 image,後來 production、worker、uploader 全部長出來。
一開始大家都會 build,後來只剩 CI YAML 看得懂。

Bake 的價值就是把這些東西收回 repo 裡,變成一份可以被 review、可以被版本控管、可以 local 跟 CI 共用的食譜。

對「production image 跟 CDN uploader image 要分開」這種情境,它特別適合。

因為整件事會變得很直覺:

共用備料:base / deps / builder
調味集中:env / tag / secret / cache
分開出餐:production / uploader

不用每次都重新背一串 build 指令。

只要跟 Docker 說:

docker buildx bake

剩下就交給食譜。


參考資料


  1. Docker 官方部落格,〈Docker Desktop 4.38: New AI Agent, Multi-Node Kubernetes, and Bake in GA〉,2025-02-05。https://www.docker.com/blog/docker-desktop-4-38/ ↩︎

  2. Docker Docs,〈docker buildx bake〉。https://docs.docker.com/reference/cli/docker/buildx/bake/ ↩︎ ↩︎ ↩︎

  3. Docker Docs,〈Bake file reference〉。https://docs.docker.com/build/bake/reference/ ↩︎ ↩︎ ↩︎