目錄

Docker 建置快取與映像檔瘦身技巧

同一個 Dockerfile,寫法不同,建置時間可能從不到 1 秒變成好幾分鐘,映像檔大小也可能差很多。這一節用實驗說明快取的規則和瘦身技巧。

快取怎麼運作

docker build 依序處理每個指令。對每一行,Docker 會檢查:這個指令和它之前的所有層,是否和上次建置時一樣?

  • 一樣:直接使用快取,輸出顯示 CACHED。
  • 不一樣:重新執行這一行,而且之後的每一行都不再使用快取。
指令判斷依據
RUN指令字串是否相同(不會檢查網路上的套件是否更新)
COPY / ADD被複製的檔案內容是否相同
其他指令和參數是否相同
修改程式碼後重新建置:先安裝套件的順序讓 npm ci 使用快取,先複製原始碼的順序則讓 npm ci 重新執行
一層改變後,它下面的每一層都要重建:把不常變動的步驟放在前面

實驗:指令順序的影響

用一個使用 Express 的小專案(package.json 中有 express 套件),比較兩種 Dockerfile。

錯誤的順序(Dockerfile.bad):

FROM node:24-alpine
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]

正確的順序(Dockerfile.good):

FROM node:24-alpine
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
CMD ["node", "server.js"]

錯誤的順序:改一行程式碼

先建置一次,然後只修改 server.js 中的一個字,再建置:

$ docker build -f Dockerfile.bad -t cache-bad .

[+] Building 6.9s (9/9) FINISHED                                                     docker:default
 => [internal] load build definition from Dockerfile.bad                                       0.0s
 => => transferring dockerfile: 125B                                                           0.0s
 => [internal] load metadata for docker.io/library/node:24-alpine                              0.2s
 => [internal] load .dockerignore                                                              0.0s
 => => transferring context: 65B                                                               0.0s
 => [1/4] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => [internal] load build context                                                              0.0s
 => => transferring context: 280B                                                              0.0s
 => CACHED [2/4] WORKDIR /app                                                                  0.0s
 => [3/4] COPY . .                                                                             0.1s
 => [4/4] RUN npm install                                                                      5.4s
 => exporting to image                                                                         1.0s
 => => exporting layers                                                                        0.5s
 => => exporting manifest sha256:d27475f082a53018e303541bc1c0691ac25526e87761300e2aa8ec76d73e  0.0s
 => => exporting config sha256:19f2024df4329a5f2af23e50d8c18826f073d425e18133e9571c393999d913  0.0s
 => => exporting attestation manifest sha256:58b4730e9566f2ec7e8222cd41c14ea971d87272b6b0c190  0.0s
 => => exporting manifest list sha256:58b8fc79124f2fe21270801d5e40f09839c06ba5203304423125270  0.0s
 => => naming to docker.io/library/cache-bad:latest                                            0.0s
 => => unpacking to docker.io/library/cache-bad:latest                                         0.4s

[3/4] COPY . . 因為 server.js 改了而重新執行,它之後的 [4/4] RUN npm install 也跟著重跑,花了 5.4 秒重新下載、安裝所有套件。

正確的順序:改一行程式碼

第一次建置(npm install 需要執行):

$ docker build -f Dockerfile.good -t cache-good .

[+] Building 5.7s (10/10) FINISHED                                                   docker:default
 => [internal] load build definition from Dockerfile.good                                      0.0s
 => => transferring dockerfile: 147B                                                           0.0s
 => [internal] load metadata for docker.io/library/node:24-alpine                              0.1s
 => [internal] load .dockerignore                                                              0.0s
 => => transferring context: 65B                                                               0.0s
 => [1/5] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => [internal] load build context                                                              0.0s
 => => transferring context: 95B                                                               0.0s
 => CACHED [2/5] WORKDIR /app                                                                  0.0s
 => [3/5] COPY package.json ./                                                                 0.0s
 => [4/5] RUN npm install                                                                      4.4s
 => [5/5] COPY . .                                                                             0.1s
 => exporting to image                                                                         0.9s
 => => exporting layers                                                                        0.5s
 => => exporting manifest sha256:4b36d92a774b02cc1a188193e178a445b90f330b19f2dcc741d631e246e8  0.0s
 => => exporting config sha256:4a597aa667d9ba52eab36c4483f7507bbcae158b1b22af5ca6e4ac5b7020fa  0.0s
 => => exporting attestation manifest sha256:efea2ef65e2672718391b6419b9f5f2be50396163032a29e  0.0s
 => => exporting manifest list sha256:236918100fd2092d0388da5c03b32dfa3abaa2e62304abb3e3dd335  0.0s
 => => naming to docker.io/library/cache-good:latest                                           0.0s
 => => unpacking to docker.io/library/cache-good:latest                                        0.4s

修改 server.js 後再建置:

$ docker build -f Dockerfile.good -t cache-good .

[+] Building 0.4s (10/10) FINISHED                                                   docker:default
 => [internal] load build definition from Dockerfile.good                                      0.0s
 => => transferring dockerfile: 147B                                                           0.0s
 => [internal] load metadata for docker.io/library/node:24-alpine                              0.1s
 => [internal] load .dockerignore                                                              0.0s
 => => transferring context: 65B                                                               0.0s
 => [1/5] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => [internal] load build context                                                              0.0s
 => => transferring context: 280B                                                              0.0s
 => CACHED [2/5] WORKDIR /app                                                                  0.0s
 => CACHED [3/5] COPY package.json ./                                                          0.0s
 => CACHED [4/5] RUN npm install                                                               0.0s
 => [5/5] COPY . .                                                                             0.0s
 => exporting to image                                                                         0.1s
 => => exporting layers                                                                        0.0s
 => => exporting manifest sha256:6cd8c2671a295aab85724aefe72f8dc749f4d332224f5d534fff5e4e57fa  0.0s
 => => exporting config sha256:7df3d574ae91f98ea3634c4dcede63e64ce8018c902643c2db08c2b4dee4c3  0.0s
 => => exporting attestation manifest sha256:fc9ff7e57f4187c04b74446f5dd96e46ef9c7f0ea88372a6  0.0s
 => => exporting manifest list sha256:79e8a6d76ff2382f02bcadc183a44670959d1d0a30467a27b9d619e  0.0s
 => => naming to docker.io/library/cache-good:latest                                           0.0s
 => => unpacking to docker.io/library/cache-good:latest                                        0.0s

整個建置只花了 0.4 秒:COPY package.json、RUN npm install 都是 CACHED,只有最後的 COPY . . 重新執行。這個專案只有一個套件;真實專案動輒幾百個套件,npm install 可能要好幾分鐘,差距會更大。

同樣的道理適用於所有語言:

語言先複製再安裝
Node.jspackage.json、package-lock.jsonnpm ci
Pythonrequirements.txt 或 pyproject.tomlpip install
Gogo.mod、go.sumgo mod download
Java (Maven)pom.xmlmvn dependency:go-offline
PHPcomposer.json、composer.lockcomposer install

實驗:在同一層裡清理暫存檔

每一層都是疊加的。在後面的 RUN 裡刪除檔案,前面那一層的大小不會減少。比較兩種安裝 curl 的寫法:

Dockerfile.separate:

FROM debian:trixie-slim
RUN apt-get update
RUN apt-get install -y --no-install-recommends curl
RUN rm -rf /var/lib/apt/lists/*

Dockerfile.same:

FROM debian:trixie-slim
RUN apt-get update \
    && apt-get install -y --no-install-recommends curl \
    && rm -rf /var/lib/apt/lists/*

建置後比較大小:

$ docker images --filter reference=apt-separate --filter reference=apt-same --filter reference=debian:trixie-slim
IMAGE                 ID             DISK USAGE   CONTENT SIZE   EXTRA
apt-same:latest       5357e9775c1e        136MB         34.9MB
apt-separate:latest   ad6c0a37d9ea        174MB         51.7MB

內容完全相同(都裝了 curl、都刪掉了套件清單),apt-separate 卻大了 38 MB。用 docker history 看原因:

$ docker history apt-separate
IMAGE          CREATED          CREATED BY                                      SIZE      COMMENT
ad6c0a37d9ea   11 seconds ago   RUN /bin/sh -c rm -rf /var/lib/apt/lists/* #…   20.5kB    buildkit.dockerfile.v0
<missing>      11 seconds ago   RUN /bin/sh -c apt-get install -y --no-insta…   13.4MB    buildkit.dockerfile.v0
<missing>      16 seconds ago   RUN /bin/sh -c apt-get update # buildkit        21.6MB    buildkit.dockerfile.v0
<missing>      8 days ago       # debian.sh --arch 'amd64' out/ 'trixie' '@1…   87.7MB    debuerreotype 0.17

$ docker history apt-same
IMAGE          CREATED        CREATED BY                                      SIZE      COMMENT
5357e9775c1e   1 second ago   RUN /bin/sh -c apt-get update     && apt-get…   13.4MB    buildkit.dockerfile.v0
<missing>      8 days ago     # debian.sh --arch 'amd64' out/ 'trixie' '@1…   87.7MB    debuerreotype 0.17

apt-separate 的 apt-get update 那一層有 21.6 MB(下載的套件清單),後面的 rm -rf 只是在新的一層裡把它「標記為刪除」,原本那 21.6 MB 仍然留在映像檔裡。寫在同一個 RUN 裡,套件清單在這一層結束前就被刪掉,從來沒有被存進任何一層。

各語言常用的「不留暫存」寫法:

RUN pip install --no-cache-dir -r requirements.txt
RUN npm ci && npm cache clean --force
RUN apk add --no-cache curl

.dockerignore

建置開始時,Docker 會把整個建置內容(通常是目前資料夾)送給建置引擎。沒有排除 node_modules 時,Dockerfile 入門 中的範例要傳送 30 MB,排除後只有 159 B。.dockerignore 範例:

.git
node_modules
dist
*.log
.env
.env.*
Dockerfile*
.dockerignore

除了加快建置,也避免這些檔案的變動讓 COPY . . 的快取失效,並防止 .env、私鑰被打包進映像檔。

選擇精簡的基礎映像檔

映像檔 一節實測的 Python 3.14 映像檔大小:

基礎映像檔硬碟空間
python:3.141.63 GB
python:3.14-slim180 MB
python:3.14-alpine82.6 MB

多階段建置

編譯工具、開發用套件只在建置時需要,不應該留在最終的映像檔裡。多階段建置 是最有效的瘦身方式。

快取掛載:加速套件下載

BuildKit 支援快取掛載,讓套件管理工具的下載快取在多次建置之間保留。即使 RUN npm ci 這一層的快取失效,也不用從網路重新下載全部套件:

# syntax=docker/dockerfile:1
FROM node:24-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

快取掛載的內容不會進入映像檔,所以不會增加映像檔大小。

強制不使用快取

需要拿到套件的最新版本,或懷疑快取有問題時:

$ docker build --no-cache -f Dockerfile.good -t cache-good .

[+] Building 6.6s (10/10) FINISHED                                                   docker:default
 => [internal] load build definition from Dockerfile.good                                      0.0s
 => => transferring dockerfile: 147B                                                           0.0s
 => [internal] load metadata for docker.io/library/node:24-alpine                              0.3s
 => [internal] load .dockerignore                                                              0.0s
 => => transferring context: 65B                                                               0.0s
 => [1/5] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => [internal] load build context                                                              0.0s
 => => transferring context: 280B                                                              0.0s
 => CACHED [2/5] WORKDIR /app                                                                  0.0s
 => [3/5] COPY package.json ./                                                                 0.0s
 => [4/5] RUN npm install                                                                      5.1s
 => [5/5] COPY . .                                                                             0.1s
 => exporting to image                                                                         1.0s
 => => exporting layers                                                                        0.4s
 => => exporting manifest sha256:30688d5c22069d97e9db0bd32d5939e8e99fda0c6a0c4af1f32fb5baacc9  0.0s
 => => exporting config sha256:e8e75108925e8f145e7367b8e36350a8175c3776154f560fb47a9fb287fe21  0.0s
 => => exporting attestation manifest sha256:adbbb2fc6683c9a35c67bb9237cf3afcfd24c39dd9aa4b55  0.0s
 => => exporting manifest list sha256:6b92b5704d4fa7a3f11cfda5ffde00e6516bdbf358e23dbece712a2  0.0s
 => => naming to docker.io/library/cache-good:latest                                           0.0s
 => => unpacking to docker.io/library/cache-good:latest                                        0.5s

--no-cache 讓每一步都重新執行(沒有任何 CACHED)。加上 --pull 還會重新檢查基礎映像檔有沒有新版本。

查看與清除建置快取

建置快取會越積越多:

$ docker builder du
ID                           RECLAIMABLE   SIZE       LAST ACCESSED
mr6xd3ylof28pr8obxiet3fo8    true          403B       3 minutes ago
l34l5occgcgh8bscivg2iusrn    true          626B       3 minutes ago
...(中間省略)
Shared:		488.2MB
Private:	118.5MB
Reclaimable:	606.7MB
Total:		606.7MB
$ docker builder prune -f
ID						RECLAIMABLE	SIZE		LAST ACCESSED
xqb9jtg0kmq0mdzb0vrmod68j               	true 		20.67kB   	3 minutes ago
u1pbndrekyh1q91qbb4n9e3fy*              	true 	8.192kB   	5 minutes ago
...(中間省略)
Total:	118.5MB

docker builder prune 只刪除建置快取,不影響映像檔與容器。清理所有 Docker 資料的方法見 清理硬碟空間。

重點回顧

  • 某一層改變後,它之後的所有層都要重建。
  • 把不常變動的步驟(安裝套件)放在前面,常變動的(複製程式碼)放在後面。
  • 在同一個 RUN 裡下載、安裝並清理暫存檔,暫存檔才不會留在映像檔中。
  • RUN --mount=type=cache 讓套件下載在多次建置之間共用。

動手練習

練習 1:下面兩個 Dockerfile 片段,哪一個做出的映像檔比較小?為什麼?

# A
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# B
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
答案

B 比較小。A 的 rm 在新的一層執行,套件清單仍然存在於前面的層中;B 在同一層下載並刪除,清單沒有被保存下來。

練習 2:用 docker build --no-cache 和不加參數各建置一次同一個專案,比較 [+] Building 顯示的總時間。

提示

不加參數時大部分步驟顯示 CACHED,通常只要零點幾秒;--no-cache 每個步驟都重新執行,時間取決於套件安裝的速度。

常見問題

RUN apt-get update 被快取,裝到舊版套件怎麼辦?

快取只比對指令字串,不會檢查網路上的套件是否更新。把 apt-get update 和 apt-get install 寫在同一個 RUN 中,需要時用 docker build --no-cache 強制重建。

建置快取佔用很多硬碟空間?

用 docker builder du 查看、docker builder prune 清除,實測見 [清理硬碟空間](/zh-tw/docker/cleanup/)。

CI 中每次都從頭建置很慢?

使用外部快取,例如 GitHub Actions 的 cache-from: type=gha,見 [GitHub Actions 自動建置](/zh-tw/docker/ci-cd/)。

更新於 2026年10月14日