← Back to Blog
Coding中

Docker:Image Optimisation

從 Layer Caching、Multi-stage builds 到 Distroless,整理 Docker Image 又快又小又安全的優化方式,思考的像是資深工程師

2026.06.04·6 min read·1,375 words

在上一篇整理了容器化的底層原理(OCI、Namespaces、Cgroups)與 Docker 架構之後,這篇文章往實務面走:如何優化 docker Image 建得快、體積小、跑得安全。這些直接關係到 CI/CD 的效率、部署速度與儲存成本,更是成為資深工程師的必經之路。

Image 最佳化:又快又小又安全

Layer Caching(層級快取)

Dockerfile 裡的每一個 RUN、COPY、ADD 都會產生一個唯讀層(Layer)。Docker 在建置時,只要發現該層的指令與相關檔案沒有改變,就會直接使用快取(Cache),跳過重新執行。

但快取是鏈狀失效的:只要某一層失效,它之後的所有層都會跟著重建。

Mindset

變動越不頻繁的指令放越前面。先 COPY package.json 並執行 npm install,然後才 COPY . .(原始碼)。只要沒改 package.json,每次改 code 都不用重新下載幾百 MB 的套件。

FROM node:22-alpine
WORKDIR /app
 
# 套件清單很少變動 → 放前面,快取可以長期存活
COPY package.json pnpm-lock.yaml ./
RUN npm install
 
# 原始碼天天改 → 放後面,失效時只重建這層之後的部分
COPY . .
 
CMD ["node", "server.js"]

如果反過來先 COPY . . 再安裝套件,那麼每一次 commit 都會讓 npm install 重跑,CI/CD 自然越來越慢。

Multi-stage Builds(多階段建置)

Multi-stage 一直都是被強調可以省時間的方法,因為可以把編譯階段時間省下來,但大部分專案不一定有多個程式語言混合,所以這部分我自己還沒真的體會到。

Mindset

在 Dockerfile 裡寫多個 FROM。在第一個階段(Build stage)編譯程式碼,在第二個階段(Run stage)只把編譯好的二進制檔(Binary)複製過來。

# --- Build stage:包含完整的 Go toolchain(數百 MB)---
FROM golang:1.23 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server .
 
# --- Run stage:只帶走 Binary ---
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]

編譯環境留在第一階段,最終 Image 只剩下執行檔本身。

Base Image 選擇:Alpine vs. Distroless

特性AlpineDistroless(Google 推出)
體積約 5MB 的極簡 Linux更極端,只有應用程式的執行期依賴
Shell / 套件管理有 /bin/sh 與 apk連 Shell 和 apt/apk 都沒有
C 函式庫musl libc(非標準 glibc)基於 Debian 的 glibc
除錯難度可以 docker exec 進去下指令無法進入互動式 Shell(有 :debug tag 可用)
攻擊面小極小,大幅減少 CVE 漏洞
Alpine 的 musl libc 相容性

Alpine 底層使用 musl libc 而非標準的 glibc,有時候會遇到 C 語言編譯套件的相容性問題(Python 的 wheel、Node 的 native module 是常見災區)。

Distroless 裡面連 Shell(/bin/sh)和套件管理工具(apt/apk)都沒有。意味著駭客即使攻進 Container,也無法下指令作怪——攻擊面的縮小不只是體積問題,更是資安防線。

安全性:權限的最小化

預設情況下,Docker Container 裡面的 User 是 Root。

Non-root User

如果 Container 被駭客入侵,且發生容器逃逸(Container Breakout),駭客就會直接擁有 Host 機器的 Root 權限。解法是在 Dockerfile 建立並切換到一般使用者:

# Alpine 系列
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
# Debian/Ubuntu 系列
RUN addgroup --system app && adduser --system --group app
USER app

Principle of Least Privilege(最小權限原則)

Container 應該只擁有它絕對需要的權限。兩個常用手段:

  1. 移除所有 Linux Capabilities:

    docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE my-app
  2. 唯讀檔案:

    docker run --read-only --tmpfs /tmp my-app
Note

--read-only 搭配 --tmpfs 是常見組合:根檔案系統鎖死,但保留 /tmp 給應用程式寫暫存檔。

儲存與網路(Storage & Networking)

儲存:Bind Mount vs. Volume

特性Bind MountVolume
本質直接掛載 Host 上的絕對路徑(如 /home/user/app)由 Docker 完全管理(通常在 /var/lib/docker/volumes/)
可移植性高度依賴 Host 的目錄結構與權限,不可移植跨平台,生命週期獨立於 Container
效能一般(macOS/Windows 上有檔案系統轉換損耗)較佳
適合場景本地開發:Host 改 code,Container 馬上生效正式環境:資料庫持久化(Production Database)

情境題解析

面試情境題

「如果 CI/CD pipeline build image 越來越慢,而且最後產出的 image 高達 2GB,會從哪些方向下手優化?」

這題其實就是上面所有觀念的綜合應用,可以依「慢 → 層數 → 體積 → 根本」的順序展開:

  1. 首先檢查 Dockerfile

檢查 Dockerfile,將安裝 Dependencies 的步驟與複製原始碼的步驟分開,確保頻繁改動的 Code 不會破壞套件下載的 Cache。

  1. 接著檢查 RUN 指令

將多個 RUN apt-get update && apt-get install ... 合併為同一層,並在同一層內清除快取(如 rm -rf /var/lib/apt/lists/*),避免無用的中繼檔案殘留在最終 Image 中:

RUN apt-get update && \
    apt-get install -y --no-install-recommends curl && \
    rm -rf /var/lib/apt/lists/*
  1. 導入 Multi-stage build

把編譯環境(包含 SDK、編譯工具)留在第一階段,最終只將編譯後的執行檔放入乾淨的 Runtime 環境中——這通常能把幾 GB 的 Image 縮減到幾十 MB。

  1. 檢視 Base Image 的選擇

與其使用肥大的 ubuntu 或 node:latest,評估切換到 node:alpine 或 Distroless。這不僅能進一步縮小體積、加速部署,還能順便減少 CVE 漏洞,提升系統安全性。

總結

目標手段
快Layer Caching:少變動的層放前面、.dockerignore 縮小 build context
小Multi-stage builds、合併 RUN 並清快取、精簡 Base Image
安全Non-root User、--cap-drop=ALL、--read-only、Distroless 移除 Shell
last edited 2026.06.04·© 2026 tylerastro·built by hand