前面七章都在講「怎麼用別人做好的映像」。從這一章開始換邊站——你要自己做映像了。
Dockerfile 就是那份製作說明書。它的語法簡單到十分鐘就能看完,但寫得好和寫得爛,差別是 180 MB vs 900 MB、8 秒 vs 4 分鐘。這一章先把每個指令講清楚,然後深入真正決定建置速度的關鍵:layer cache 什麼時候會失效。
理解 cache 之後你會發現,Dockerfile 的指令順序不是隨便排的—— 它是一個關於「什麼東西多久變一次」的設計問題。
幾乎每個人第一天都打過這行指令,卻很少人知道最後那個點的意義:
docker build -t myapp:1.0 .
# ↑ 這個點COPY 只能從這包東西裡拿檔案——拿不到情境以外的任何檔案。Sending build context to Docker daemon 1.2GB?」target/、node_modules/、.git/——全部被打包送過去了,即使 Dockerfile 根本沒 COPY 它們。COPY ../common/lib.jar 會失敗?」.. 在情境外面。這是刻意的安全設計——否則一份 Dockerfile 就能偷走你整台機器的檔案。放在情境根目錄,語法跟 .gitignore 幾乎一樣。它同時解決速度與安全兩個問題:
# === 建置產物與相依(最佔空間)===
target/
build/
node_modules/
*.jar
# === 版控與 IDE ===
.git/
.gitignore
.idea/
.vscode/
# === 秘密!絕對不要進映像 ===
.env
*.pem
*.key
secrets/
# === 其他雜物 ===
README.md
docs/
Dockerfile
docker-compose*.yml.git/ 這條特別重要:如果你用 COPY . . 而沒有排除 .git,整個版控歷史都會進到映像裡——包括你三個月前不小心 commit 又刪掉的那個 API key。ch03 說過,映像的層是永久紀錄,刪不掉。docker build -f Dockerfile.prod -t myapp . 可以指定用哪份 Dockerfile,情境仍然是那個點。Dockerfile 的位置和 build context 是兩件獨立的事。FROM eclipse-temurin:17-jre-jammy
FROM maven:3.9-eclipse-temurin-17 AS builder # 具名階段,多階段建置用(ch10)ARG 除外)。基底選擇決定了映像 80% 的大小與漏洞數量(ch10 主題)。WORKDIR /app # 不存在會自動建立;後續指令的相對路徑都以此為基準RUN cd /app——每個 RUN 是獨立的一層、獨立的 shell,cd 的效果不會延續到下一個指令。COPY target/app.jar /app/app.jar
COPY --chown=appuser:appuser target/app.jar /app/app.jar # 順便設擁有者ADD 多了兩個「貼心」功能:自動解壓 tar、可以從 URL 下載。但這兩個行為都很隱晦:URL 下載不會驗證、不會用快取、還會讓映像多一層;自動解壓則常常出乎意料。COPY,除非你就是要自動解壓本地 tar。要抓網路檔案請用 RUN curl(可以驗證雜湊、可以順手清理)。RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*apt-get install 在一層、rm 在另一層,那些套件檔案永遠留在前一層裡,映像照樣變大。「產生垃圾」和「清掉垃圾」必須在同一層完成。RUN apt-get update 單獨一層。這層被快取住之後可能好幾個月都不重跑,於是套件索引老舊,某天 install 突然噴 404 Not Found——因為索引裡的版本已經從鏡像站下架了。update 和 install 一定要在同一個 RUN。ARG JAR_FILE=target/*.jar # 只在 build 時有效,容器裡看不到
ENV SPRING_PROFILES_ACTIVE=prod # 會留在映像裡,容器執行時有效
# 建置時覆寫 ARG
docker build --build-arg JAR_FILE=target/myapp.jar .ENV 會永久留在映像設定裡,任何人 docker inspect 或 docker history 都看得到;ARG 雖然不留在最終環境變數,但會記錄在建置歷史裡。密碼請用執行時注入或 BuildKit secret(ch10)。EXPOSE 8080:文件宣告,不會開埠(ch07 講過)。USER appuser:之後的指令與容器主程式都用這個身分執行。安全必備(ch10)。VOLUME /data:宣告該路徑應被持久化;沒指定時 Docker 會自動建匿名 volume(ch06 說過,會累積垃圾)。自己的應用映像通常不需要寫這個,交給使用者用 -v 決定。LABEL:加 metadata(維護者、版本、原始碼位置),方便管理與稽核。HEALTHCHECK:健康檢查(ch05 講過,Java 記得設 --start-period)。這兩個都在說「容器啟動時執行什麼」,區別讓無數人困惑。用一個類比就通了:
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
CMD ["--spring.profiles.active=prod"]
# 直接執行 → java -jar /app/app.jar --spring.profiles.active=prod
docker run myapp
# 覆寫 CMD(換水果)→ java -jar /app/app.jar --spring.profiles.active=dev
docker run myapp --spring.profiles.active=devCMD ["java", "-jar", "app.jar"]。使用者在 docker run 後面接指令會整個取代它——所以 docker run myapp bash 可以進去除錯,很方便。一般應用建議用這個。/bin/sh -c,你的 Java 就不是 PID 1,SIGTERM 收不到,優雅關機失效。#!/bin/sh
set -e
# 這裡可以做啟動前的準備(等待相依服務、產生設定檔…)
exec java -jar /app/app.jar "$@" # ← exec 是關鍵!exec 這個關鍵字非常重要:它讓 java 取代掉這個 shell 而不是當它的子行程——於是 java 繼承 PID 1,訊號才收得到。少了 exec,你就掉回 ch05 那個陷阱。"$@" 把 CMD 傳來的參數接過去。Docker build 的每一層都會被快取。理解快取何時失效,是把建置從四分鐘壓到八秒的唯一途徑。
Docker 逐層檢查「這一層我以前建過嗎」,判斷依據是:
RUN、ENV、WORKDIR 等:比對指令字串本身。一字不差就命中——注意它不會去檢查指令的實際結果(所以 RUN apt-get update 才會拿到過期的索引)。COPY、ADD:比對被複製檔案的內容雜湊。檔案內容變了就失效(只改動時間戳不算)。# ❌ 爛寫法:改一行程式碼,Maven 依賴全部重新下載(4 分鐘)
FROM maven:3.9-eclipse-temurin-17
WORKDIR /app
COPY . . ← 原始碼一改,這層就失效
RUN mvn package ← 於是這層也失效,重新下載 200 個 jar# ✅ 好寫法:只有 pom.xml 改變時才重新下載依賴
FROM maven:3.9-eclipse-temurin-17
WORKDIR /app
COPY pom.xml . ← 很少變動
RUN mvn dependency:go-offline -B ← 依賴被快取住了!
COPY src ./src ← 常常變動,但放在後面
RUN mvn package -o -DskipTests ← 只有這層要重跑(約 20 秒)差別:改一行 Java 程式碼,前者要 4 分鐘(重下 200 個 jar),後者只要 20 秒。這個技巧在每個語言都成立——Node 是先 COPY package.json 再 npm ci,Python 是先 COPY requirements.txt 再 pip install。共同原則:先複製「依賴清單」裝好依賴,再複製原始碼。
COPY . . 沒有 .dockerignore:改個 README 或 .git 一動就整包失效。ARG BUILD_TIME 放太前面:每次建置值都不同 → 從那層開始全滅。這類每次都變的東西一定放最後。docker buildx build --cache-from/--cache-to 把快取存到 registry。docker build . # 命中時輸出會顯示 CACHED
docker build --no-cache . # 強制全部重建(懷疑快取有問題時用)
docker history myapp:1.0 # 看每層多大、由哪個指令產生從 Docker 23 起,docker build 預設就是用 BuildKit 引擎(舊版可用 DOCKER_BUILDKIT=1 開啟)。它帶來四個實用能力:
# syntax=docker/dockerfile:1
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN --mount=type=cache,target=/root/.m2 \
mvn package -DskipTests/root/.m2(Maven 本地倉庫)被掛成一塊跨建置持久化的快取——即使前面的層失效了,已下載的 jar 依然在。/root/.npm、Go 用 /root/.cache/go-build,同樣道理。RUN --mount=type=secret,id=maven_settings,target=/root/.m2/settings.xml \
mvn package
# 建置時提供
docker build --secret id=maven_settings,src=$HOME/.m2/settings.xml .docker buildx build --platform linux/amd64,linux/arm64 \
-t myregistry.io/myapp:1.0 --push .# syntax=docker/dockerfile:1 是啟用這些新語法的宣告,建議所有新 Dockerfile 都加上——它會自動使用最新的 Dockerfile 語法解析器,不用升級 Docker 就能用新功能。| 寫法 | docker run myapp 的結果 | docker run myapp bash 的結果 | 適用 |
|---|---|---|---|
| 只有 CMD | 執行 CMD | CMD 被整個取代 → 進 bash | 一般應用(方便除錯) |
| 只有 ENTRYPOINT | 執行 ENTRYPOINT | bash 變成 ENTRYPOINT 的參數 | 把映像做成指令工具 |
| 兩者都有 | ENTRYPOINT + CMD 當參數 | bash 取代 CMD 成為參數 | 主體固定、參數可換(彈性最好) |
| 都沒有 | 繼承基底映像的設定 | 執行 bash | 基底映像已定義好時 |
| 對照組 | 差別 | 該選哪個 |
|---|---|---|
| COPY vs ADD | ADD 多了自動解壓 tar 與 URL 下載,但行為隱晦、不驗證、不快取 | 一律用 COPY;抓網路檔案用 RUN curl |
| ENV vs ARG | ENV 留在映像、執行時有效;ARG 只在建置期有效 | 執行期設定用 ENV,建置參數用 ARG;兩者都不能放密碼 |
| exec form vs shell form | shell form 被包進 /bin/sh -c,你的程式不是 PID 1 | 主程式一律 JSON 陣列(雙引號!) |
| RUN cd vs WORKDIR | 每個 RUN 是獨立 shell,cd 的效果不會延續 | 一律用 WORKDIR |
| EXPOSE vs -p | EXPOSE 只是文件宣告,不會開埠 | 要連得進去必須執行時加 -p |
| 指令類型 | 快取比對依據 | 什麼會讓它失效 | 對策 |
|---|---|---|---|
| RUN / ENV / WORKDIR | 指令字串本身 | 字串改了一個字元 | 注意它不檢查實際結果(apt-get update 陷阱) |
| COPY / ADD | 被複製檔案的內容雜湊 | 檔案內容變動(改時間戳不算) | 先 COPY 依賴清單再 COPY 原始碼 |
| 所有後續層 | 前一層是否命中 | 前面任一層失效 → 後面全部失效 | 變動頻率低的放前面(第一設計原則) |
點擊卡片翻面查看答案,共 11 張。