Docker Image 優化與運作原理

面試被問到、實作也踩過的 image 優化問題,從 Image layer 的底層原理討論到優化技巧。

不管是面試時,或在 DevOps 實作上都會遇到 Image 優化的問題,尤其我過去接觸的 Application 以 Java 為主,相較於部分其他程式語言和框架,Java Application 的 Image Build 通常需要較長的時間。

所以想從 image 底層邏輯介紹到 Image 優化技巧,讓讀者不管是否用 docker 製作,都可以掌握到基礎原則,能更快地做出更小仍兼顧需求的 image。

Image 定義

Image 是一份不可變(Immutable)的應用程式執行環境模板,包含執行一個應用程式所需要的檔案,例如:

  • Application 程式碼
  • Application dependencies
  • runtime
  • 系統工具/函式庫
  • 設定檔

從 Image 建立並實際執行的 instance 叫做 Container。

Image Build with Dockerfile

Image Build 可以想像成:在一個隔離、可重現的建置環境中,準備:

  • Base Image:建置環境的基礎
  • Build Context:建置材料
  • Build Instruction:建置步驟

讓 Builder 建置並封裝成一個可以儲存、傳輸並啟動成 Container 的 Image。

Dockerfile 就是在使用 Docker 做 build image 時描述建置步驟的檔案,包含:

  • FROM:使用什麼作為建置的基礎
  • COPY / ADD:將檔案加入 Image filesystem
  • RUN:執行建置指令
  • WORKDIR:設定工作目錄
  • ENTRYPOINT / CMD:定義 Container 啟動時的預設行為

以 Java Maven 為例:

Base Image
└── Maven + JDK
Build Context
├── pom.xml
└── src/
Dockerfile
├── FROM
├── COPY
├── RUN mvn package
└── ...
↓
Docker Build
↓
Image
└── application.jar

為什麼要優化 image Build

可以把 Image Build 想成一個大量生產料理的廚房。

如果廚師:

  • 缺乏有效率地製作料理的方式
  • 將不必要的工具、食材放在廚房
  • 最終料理成品上未去除不必要的元素

那麼單次料理可能只慢一點,但當每天要做幾千份料理時,浪費就會被放大。Image Build 也是同樣的概念。

隨著:

  • Build 次數
  • Repository 數量
  • CI/CD pipeline
  • Deployment 頻率

增加,原本只是幾十秒或幾百 MB 的浪費,都會累積成不可忽略的時間與成本。

因此以下優化 image Build 從 Build 時間長短 以及 Build Image 大小 兩個方向去做討論。

Build 時間長短

Build 時間過長會直接增加 CI pipeline 的執行時間。

Image Build 越慢:

  • CI Runner 執行時間越長
  • 消耗更多 CPU / Memory
  • Pipeline feedback 越慢
  • 後續 Test / Deploy 也會被延後

尤其在頻繁 commit、PR 或 deployment 的情況下,影響會被放大。

CI Runner 可以簡單理解成「執行 CI 流程的機器」。

Image 大小

Image Build 完成後,Image 通常會被 push 到 Container Registry。

Deploy 時,再把 image 從 registry 拉到目標機器上去啟動 container。Image 越大,通常代表:

  • Pull 時間越長
  • 需要傳輸更多資料
  • 消耗更多網路頻寬
  • 新 instance / Pod 啟動可能更慢

這對有快速 Scale out 需求(ex. 使用者流量尖峰時多開幾個 instance)的服務尤為重要,因為 image 大小直接影響新 instance 能多快準備好應付流量。

另外通常用託管的 registry 是照儲存空間計費,隨著每次版本更新、hotfix,長期累積大量 image versions,會增加 Registry storage 使用量。

Image Registry 通常搭配適當的 lifecycle policy 定期清理不再需要的舊 images。

以 GitHub Actions 為例: GitHub Actions 提供的免費使用額度如下表(2026.8)

Plan Artifact storage Minutes (per month) Cache storage (per repository) Custom image storage
GitHub Free 500 MB 2,000 10 GB Not applicable

當免費的額度用完,就只能升級方案或者建立 self-hosted runners。


Image 原理——layer

要優化 Image Build,就需要先知道 Image 的核心原理。

Image 本質是由多個 immutable layers 由下往上疊加的結果。Docker Build 過程中,會根據建置步驟產生或重用這些 layers,最後形成 Image 的 filesystem。

Image layers 背後使用了 Content-Addressable Storage(CAS) 的概念。

簡單來說,Docker 會使用 content hash 來識別 layer content。Layer 本身是 immutable 的,因此相同的 layer content 可以被不同的 Image 共用,而不需要重複儲存。

因此,如果同一台機器上有多個 Image 都使用相同的 base layer,例如 FROM openjdk:17-jdk-slim,這些 Image 不需要各自儲存一份完全相同的 layer data。

有些人可能聽說 Docker 是使用 OverlayFS,該部分屬於 container runtime 的議題(union filesystem),由於篇幅有限在此不討論。

所以,當清理 Docker Desktop 的 image 時,直覺可能以為刪掉某個 Image 就會釋放它底層佔用的空間。但實際上因為 Layer 是共用的,只要該 layer 仍被其他 Image 引用或 Container 使用,就不能被回收。


Image 優化方式

1. 僅將必要的檔案放入 Build Context(搭配 .dockerignore)

最直覺的 Image Build 優化方式之一,是控制 Build Context 的大小,只把建置需要的檔案提供給 Builder。在 Docker 中,可以搭配 .dockerignore 排除不需要的檔案。

.dockerignore 的概念類似 .gitignore,用它來排除不需要進入 Build Context 的檔案。 以 Java / Maven 為例:

target/
.git/
.idea/
*.iml
.DS_Store

其中:

  • target/:Maven 編譯產生的 class、JAR 等 build artifacts
  • .git/:Git repository metadata

即使 Dockerfile 使用 COPY . .,它們也不會被複製進 Image。

2. 利用 Layer Cache

Docker Build 會根據 Dockerfile 的建置順序處理各個 build step,並根據 instruction、相關檔案內容與其他 build inputs 判斷是否可以重用既有的 cache。

如果某個 build step 的 cache 無法重用,該步驟需要重新執行;後續依賴該步驟結果的 build steps 通常也需要重新執行。

因此 Dockerfile 的指令順序非常重要。 一般來說,會將不常變動和執行成本高的步驟放在前面;將經常變動和執行成本低的步驟放在後面。

以 Java / Maven 為例:

COPY pom.xml /home/app
RUN mvn -f /home/app/pom.xml dependency:go-offline
COPY src /home/app/src
RUN mvn -f /home/app/pom.xml clean package -Dmaven.test.skip=true

刻意去做分開複製很重要,原因是在開發上,dependencies(pom.xml)通常比 source code 更不常變動。

反過來,如果選擇 COPY .,那麼被 COPY . 納入的檔案內容發生變化時,可能使該 COPY step 的 cache 失效(被稱為 cache invalidation 機制)。

3. 使用 Cache Mount

Cache Mount 是由使用者下指令達成,跟 Layer Cache 這種 Docker BuildKit 自動機制不同。

Cache mount 會讓指定路徑對應到 BuildKit 管理的 cache storage。每當該步驟必須重新執行時,可以重用過去產生的 cache data。

Cache mount 常用於 Dependencies 的下載,這樣即使因為 source code 修改,要重新執行這步驟時,通常只需下載尚未存在於 local dependency cache 中的套件,而不必每次從頭下載全部 dependencies。

RUN --mount=type=cache,target=/root/.m2 \
mvn package
BuildKit cache
│
│ cache mount
▼
/root/.m2
│
▼
Maven

4. Multi-stage build:將 Build 跟 Runtime 分開

Multi-stage build 讓你將 Build 過程拆分成多個 stages。每個 FROM 都會開始一個新的 stage。預設情況下,Docker 會將最後一個 stage 作為最終 Image。 前面的 build stages 不會自動進入最終 Image,只有透過 COPY --from 等方式明確帶入的內容才會被加入。

這非常適合將 Build Environment 與 Runtime Environment 分離。

當我們在 Build 時用到的工具,在 Runtime 時並不會用到,這時候就適合用 multi-stage build。

以 Java / Maven 為例,Java 在 build 時需要 Maven、JDK(Java Development Kit)來編譯出 jar 檔;但在 runtime,只需要用 JRE 來執行這個 jar 檔。

JDK 提供 Java 開發與編譯所需的工具,例如 javac;Runtime 則只需要執行 application 所需的 Java runtime。因此 Build stage 可以使用 JDK,而 Runtime stage 可以選擇較精簡的 Java runtime image。

FROM maven:... AS build
# Build application
FROM eclipse-temurin:17-jre AS runtime
COPY --from=build /home/app/target/app.jar /app.jar
# Run application

透過 COPY --from=build 這一步,把 jar 檔案複製到另一個乾淨的 runtime image 裡執行,讓 build 階段裝的整套工具鏈(Maven、JDK 的編譯器、下載下來的依賴 cache)都留在 build 階段的 image 裡,不會出現在最終拿去 deploy 的 image。

5. 合併 RUN 指令

Image layer 是累加的,因此在安裝套件時,通常會將安裝與清理動作透過 && 連結放在同一個 RUN:

RUN apt-get update \
&& apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*

而不是:

RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

這幾個指令要寫在同一個 RUN 裡,而不是拆成三個 RUN。原因回到前面提過的「層是累加的」:如果 apt-get install 是一層、rm -rf 是另一層,那麼 apt-get install 裝進去的東西(下載的 deb 套件、apt 的中繼資料)已經算進那一層的大小裡了;下一個 layer 雖然看不到這些檔案,但前一個 layer 已經包含它們,因此前一個 layer 所儲存的資料仍然存在。

合併成同一個 RUN,這些暫存檔案在同一層被建立、也在同一層被清掉,才不會被算進最終大小。

如果拆成不同 layers,在第三層雖然看不到那些檔案,但前面的 layer 已經包含它們,因此這些資料仍然存在於 Image 的 layer data 中;如果在同一個 RUN 中,最終 layer 就不需要包含那些不必要的暫存檔案。

6. Base image 選擇:Full / Slim / Alpine / Distroless

Base image 就是建立 image 的基礎。若清楚 application 所需要的 runtime 與 dependencies,就可以選擇更精簡的 base image,更甚者自己打造一個,來減少最終 Image size。

當然還是要注意:若是從 public image registry 選擇,最好要有官方認證,安全性相較 image size 的優化更為重要。

Image tag 中常見的關鍵字,例如 slim、alpine、distroless,通常用來表示不同的 image variant 或 packaging approach。

Variant 簡單理解
Full / Default 功能較完整,通常包含較多常用套件與工具
Slim 在相同 base distribution 上移除許多不必要的套件
Alpine 基於 Alpine Linux,使用 musl libc,通常非常小
Distroless 只保留 application runtime 所需的最小 userspace

Slim vs Alpine

  • Slim 通常代表該 image 的精簡版本,但底層還是一般的 glibc-based Linux
  • Alpine 用 musl libc 取代 glibc,優勢是體積會更小。但 musl 與 glibc 並不是完全相容的替代品。某些依賴 native library、JNI 或其他原生擴充的 application,可能在 Alpine 上需要額外處理,甚至出現編譯或 runtime compatibility 問題。因此需要事前先實際測試。

glibc 和 musl 都是 Linux 上的 libc(C standard library)實作,提供程式跟 Linux 系統互動時常用的基礎 API。

Distroless

Distroless 是由 Google 團隊開發的一系列極簡 image,主要保留 application runtime 所需要的內容,不包含一般 Linux distribution 常見的:

  • Shell,例如 sh、bash
  • Package manager,例如 apt、apk
  • 常用 OS tools,例如 ls、ps
  • Debugging utilities,例如 curl、wget

因此通常可以比 Alpine-based image 更為精簡。

但代價是因為沒有 shell,因此沒辦法像 Debian / Alpine container 一樣使用 docker exec 進去下指令 debug,所以僅適合已經穩定、不太需要臨時進容器查東西的 production image。

Distroless 有提供 debug 版本。

Alpine 是拿相容性風險換取體積變小;Distroless 是拿 debug 方便性換取體積變小。

7. Dependencies 集中式來源

除了 Docker Build 本身的 cache,也可以從 dependency source 的角度進行優化。

可以將 application 使用的 dependencies 集中到內部的 Artifact Repository(例如 Nexus、Artifactory),而不是每次 build 都連線去外部的 public package repositories 做下載(Maven Central、npm registry 等)。

這樣的好處包含:

  • 內部網路通常比直接連外部 repository 更穩定
  • 降低對外部 repository 的依賴
  • 集中管理 dependency versions
  • 可以進行 access control
  • 檢查已知 vulnerabilities 與符合既定 security policies
  • Repository 本身也可以作為 proxy / cache,避免重複從外部下載相同 dependencies

缺點則是需要額外維護:

  • Artifact Repository infrastructure
  • Storage
  • Access control
  • Backup / lifecycle policy
  • Vulnerability scanning / governance 流程

怎麼量化與檢查

前面談的都是原理與優化方式,實際上還是需要透過工具量化,來驗證優化是否真的有效與成效如何。

  • docker history <image>:查看 Image 各 layer 的大小與對應指令,可以快速找出較大的 layer。
  • docker inspect <image>:查看更完整的 Image metadata,例如 configuration 與 layer 相關資訊。
  • dive:第三方工具,可以逐層檢視 filesystem 的變化,並分析可能浪費的空間,例如檔案在不同 layer 中被重複寫入。
  • Docker Scout:Docker 官方工具,可分析 Image 的 vulnerabilities、dependencies 等資訊。

結論

Image Build 優化並不是「能優化的地方全部都要做」。

每一項優化都需要投入成本,例如:

  • 尋找合適的 Base Image
  • 修改 Dockerfile
  • 測試相容性
  • 建立與維護 Cache
  • 建置與分析工具/流程
  • 安全性掃描

因此實務上通常會先從成本低、收益明顯的優化開始,例如:

  1. 使用 .dockerignore
  2. 合理安排 Layer Cache
  3. 使用 Multi-stage Build
  4. 選擇適當的 Base Image
  5. 再視實際 Build time、Image size 與 CI 成本,決定是否進一步導入 Cache Mount、Artifact Repository 等機制。

最重要的是:

先量化,再優化;優化後再量化。 持續迭代直到達到需求。

如果實測結果已經符合需求,就不一定需要為了追求更小的 Image 或更快的 Build,繼續增加 Image Build 流程的複雜度。


延伸 Topic

  • Docker Runtime 機制
  • Container Runtime: Docker vs Podman vs Buildah
  • OCI 標準

參考資料


← 返回文章列表