目錄
Docker 建置快取與映像檔瘦身技巧
同一個 Dockerfile,寫法不同,建置時間可能從不到 1 秒變成好幾分鐘,映像檔大小也可能差很多。這一節用實驗說明快取的規則和瘦身技巧。
快取怎麼運作
docker build 依序處理每個指令。對每一行,Docker 會檢查:這個指令和它之前的所有層,是否和上次建置時一樣?
- 一樣:直接使用快取,輸出顯示
CACHED。 - 不一樣:重新執行這一行,而且之後的每一行都不再使用快取。
| 指令 | 判斷依據 |
|---|---|
RUN | 指令字串是否相同(不會檢查網路上的套件是否更新) |
COPY / ADD | 被複製的檔案內容是否相同 |
| 其他 | 指令和參數是否相同 |
實驗:指令順序的影響
用一個使用 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.js | package.json、package-lock.json | npm ci |
| Python | requirements.txt 或 pyproject.toml | pip install |
| Go | go.mod、go.sum | go mod download |
| Java (Maven) | pom.xml | mvn dependency:go-offline |
| PHP | composer.json、composer.lock | composer 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.14 | 1.63 GB |
python:3.14-slim | 180 MB |
python:3.14-alpine | 82.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日