Tips on Dockerfile
Dockerfile 看似簡單,幾行 FROM、RUN、COPY 就能把應用打包成 image,但寫得好不好,直接牽動 build 速度、image 大小、安全性與日後的可維護性。本文整理幾個實務中最常用、卻也最常被忽略的 Dockerfile 撰寫訣竅。
(一)善用 layer cache , 指令由 變動較少 排到 容易變化 (由上到下)
Docker build 是逐層(layer)快取的,只要某一層的指令或內容改變,該層之後的所有 layer 都會失效重建。因此在Dockerfile內容中,指令順序(由上到下)應該從「變動頻率低」排到「變動頻率高」。最典型的例子,是先複製相依清單、安裝套件,最後才複製原始碼:
# 先處理相依,讓 cache 命中率最大化
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
如此一來,只要原始碼變動、相依沒變,npm ci 這一層就能命中 cache,不必重跑。
(二)用 multi-stage build 縮小 image
把「編譯環境」與「執行環境」分開,只把最終產物帶進 runtime image,可大幅縮小體積,也降低受攻擊面(attack surface):
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN go build -o server .
FROM alpine:3.20
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]
runtime image 不再包含 compiler 與原始碼,從數百 MB 降到數十 MB 是常態。
(三)合併 RUN 並在同一層清理 cache
每一個 RUN 都是一層。把相關指令用 && 串接成一層,並在同一層清掉套件快取,避免多餘體積殘留在 image 裡:
RUN apk add --no-cache curl \
&& rm -rf /var/cache/apk/*
Debian/Ubuntu 則常見 apt-get install 之後接 rm -rf /var/lib/apt/lists/*。重點是「清理必須和安裝在同一層」,否則被刪掉的檔案仍會留在前一層,image 體積依舊臃腫肥大。
(四)指明特定版本,別依賴 latest
FROM alpine:latest 看似方便,卻讓 build 結果無法重現:今天和下週 pull 到的可能是不同版本。base image 與關鍵套件都應該釘住明確版本(pin version),讓 image 具備可重現性(reproducibility):
FROM node:20.11-alpine3.19
(五)別用 root 跑 container
預設情況下 container 內的 process 以 root 執行,一旦被突破,風險極高。建立專用使用者並切換過去,是最基本的 security hardening:
RUN adduser -D appuser
USER appuser
此外,搭配 .dockerignore 把 .git、node_modules、本機祕密檔案排除在 build context 之外,既加快 build,也避免敏感資料被打包進 image。
(六)辨別 ENTRYPOINT 與 CMD
兩者常被混用,但角色不同: ENTRYPOINT 定義「這個 container 就是要跑的執行檔」,不易被覆寫; CMD 提供「預設參數」,執行 docker run 時可從命令行覆蓋。實務上常見的組合,是用 ENTRYPOINT 固定程式、CMD 給預設參數:
ENTRYPOINT ["nginx"]
CMD ["-g", "daemon off;"]
這樣既保證跑的是 nginx 又保留調整參數的彈性。(兩者的 shell form 與 exec form 差異,留待專文再談。)
(七)加上 HEALTHCHECK 讓容器狀態可觀測
沒有 HEALTHCHECK 時,只要 process 還在,Docker 就認為 container 是健康的,即便服務其實早已卡死。加上健康檢查,orchestrator 才能據此重啟或替換異常的 container:
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost:8080/health || exit 1
這對 SRE 而言,是把「容器活著」升級成「服務真的可用」的關鍵一步。
小結
以上提到的共通精髓之處,是把 Dockerfile 當成一個「可重現、可維護、最小化」的產物來對待,而不只是「能 build 起來就好」。
最後,補充一個實務上經常遭遇的情境:在企業內網或 air-gapped 環境下 build image 時,經常卡在 proxy 設定,導致 apk、apt、npm 無法對外下載套件而 build 失敗。這是編寫 Dockerfile 時最常見的問題之一,其解決方式我在一篇熱門文章已有提到,可以參考:〈Docker 環境下的 Proxy 配置〉。
常見問題(FAQ)
為什麼調整 Dockerfile 的指令順序能加快 build?
multi-stage build 為什麼能縮小 image?
COPY --from 把最終產物帶進 runtime image。這樣 runtime image 不再包含 compiler、建置工具與原始碼,體積常從數百 MB 降到數十 MB,同時也降低攻擊面。ENTRYPOINT 和 CMD 有什麼差別?
ENTRYPOINT 定義 container 一定要執行的程式,不易被覆寫;CMD 提供預設參數,執行 docker run 時可從命令行覆蓋。實務上常用組合是 ENTRYPOINT 固定執行檔、CMD 給預設參數,例如 ENTRYPOINT ["nginx"] 搭配 CMD ["-g", "daemon off;"],既保證跑的是 nginx,又保留調整參數的彈性。
留言
張貼留言