附錄
新手地雷圖鑑
26 個真的會讓人卡整個下午的坑,按「你看到的症狀」分類
整門課裡所有「⚠️ 陷阱」的集中收錄,但這裡的排序方式不同——
按照「你會先看到什麼症狀」分類,因為出事的時候你只知道症狀,還不知道原因。
用法:出事時掃過標題找到你的症狀,看原因,回對應章節看完整原理。
① 重建容器後資料歸零
- 原因:資料寫在容器的可寫層,
docker rm 時一併消失(ch03)。 - 解法:資料目錄掛 volume(ch06)。
② 把專案資料夾改名,資料全空
- 原因:Compose 專案名預設取自目錄名,volume 實際名稱是
<專案>_<volume>。改名=換了一個全新的空 volume(ch11)。 - 解法:在 compose 檔寫
name: myapp 固定專案名。舊資料還在,用 docker volume ls 找得回來。
③ 打了 docker compose down -v
- 原因:
-v 會刪除 volume。沒有確認提示、沒有回收桶(ch06)。 - 預防:把它當危險品;同理
docker system prune --volumes。
④ 用了匿名 volume
- 原因:
-v /var/lib/mysql(只寫容器內路徑)會建立一個亂碼名字的匿名 volume,下次啟動不會自動接回去(ch06)。 - 解法:一律用具名 volume。
docker volume ls -f dangling=true 檢查孤兒。
① Exited (0),但我要它常駐
- 原因:主程式不是前景執行——服務被 daemon 化,或執行的指令本來就會結束(
docker run ubuntu 沒給 -it)。容器的命=PID 1 的命(ch05)。 - 解法:
nginx -g 'daemon off;'、java -jar 直接跑;互動用 -it。
② Exited (137) 且 OOMKilled: true
- 原因:記憶體超過 cgroup 上限被核心砍掉(ch02)。Java 沒設
-XX:MaxRAMPercentage 是頭號成因(ch09)。
③ Exited (127)
- 原因:指令不存在——打錯字,或精簡映像裡根本沒有那個工具(distroless 沒有 bash,ch10)。
④ Exited (126)
- 原因:腳本沒有執行權限,或換行是 Windows 的 CRLF(Linux 會噴
bad interpreter)。Windows 開發者的常客。
⑤ 一直 Restarting,日誌滾太快看不到
- 解法:
docker run --rm -it --entrypoint sh <映像> 繞過 entrypoint 手動跑(ch13)。重啟策略救不了設定錯誤。
⑥ docker logs 完全空白
- 原因:日誌寫進容器內檔案而非 stdout(ch05);或程式根本沒啟動;或你的程式是 shell 的子行程而非 PID 1(
docker top 驗證)。
① 容器裡把資料庫寫成 localhost(第一名錯誤)
- 原因:每個容器有自己的 network namespace,
localhost = 這個容器自己(ch07)。 - 解法:用服務名稱:
jdbc:sqlserver://sqlserver:1433。
② 容器間用了映射後的外部埠
- 原因:
ports: ['14330:1433'] 時,容器間通訊完全不經過 port mapping,要用容器內部埠 1433(ch07)。
③ 第一次啟動失敗、重啟就好
- 原因:
depends_on 只等容器啟動,不等服務就緒(ch11)。 - 解法:healthcheck +
condition: service_healthy,並在應用層加連線重試。
④ 解析得到但連不上
- 原因:服務只監聽
127.0.0.1,容器內等於只有自己連得到(ch07)。 - 檢查:
docker compose exec app ss -lntp 看是不是 0.0.0.0。
⑤ 跨 compose 專案連不到
- 原因:不同專案有各自的預設網路,彼此隔離(ch11)。
- 解法:用
external 網路明確共用。
⑥ 忘了 -p,或以為 EXPOSE 會開埠
- 原因:
EXPOSE 只是文件宣告,真正建立轉接規則的是 -p(ch07)。
① COPY failed: file not found
- 原因:檔案不存在/被 .dockerignore 排除了/在 build context 之外(
../ 拿不到,ch08)。
② 改一行程式碼就要重建四分鐘
- 原因:
COPY . . 放在裝依賴之前,快取全滅(ch08)。 - 解法:先
COPY pom.xml 裝依賴,再 COPY src。加上 BuildKit cache mount 更好。
③ apt-get install 突然噴 404
- 原因:
apt-get update 單獨一層被快取住,套件索引過期(ch08)。Docker 只比對指令字串,不檢查實際結果。 - 解法:
update 與 install 串在同一個 RUN。
④ 明明 rm 掉了,映像還是很大/秘密還在
- 原因:層不可變,
rm 只是 whiteout 遮蔽,檔案還在下層(ch03)。 - 解法:產生垃圾與清理必須在同一個 RUN;秘密用 BuildKit secret mount 或執行時注入(ch10)。
⑤ Sending build context 1.2 GB
- 原因:沒有
.dockerignore,target/、node_modules/、.git/ 全被打包送出(ch08)。.git 裡藏著你歷史上 commit 過的所有秘密。
① docker stop 卡 10 秒,請求全斷線
- 原因:
CMD java -jar app.jar(shell form)讓 /bin/sh 變成 PID 1,SIGTERM 收不到也不轉發,優雅關機永遠不觸發(ch05)。 - 解法:改用 exec form
CMD ["java", "-jar", "app.jar"];用 entrypoint 腳本時記得 exec java ...。
② 應用毫無徵兆消失,連 stack trace 都沒有
- 原因:被核心 OOM Killer SIGKILL(不是 Java 的 OutOfMemoryError)。JVM 沒設
MaxRAMPercentage 時可能撐爆容器配額(ch09)。 - 解法:
-XX:MaxRAMPercentage=75.0,不要寫死 -Xmx(寫死的值會跟著映像跑到每個環境)。
③ 容器啟動途中就被判定不健康而重啟
- 原因:Spring Boot 冷啟動 30~90 秒,健康檢查沒設啟動寬限期(ch05)。
- 解法:Docker 用
--start-period=60s;K8s 用 startupProbe(ch14)。
④ 換 Alpine 之後效能怪怪的、或原生函式庫載不起來
- 原因:Alpine 用 musl libc 而非 glibc——多執行緒記憶體配置效能較差、JNI 相容性問題、DNS 行為不同。而且只省不到 15% 空間(ch10)。
⑤ 分層 JAR 之後噴 ClassNotFoundException
- 原因:啟動類別路徑寫錯。Spring Boot 3.2+ 是
org.springframework.boot.loader.launch.JarLauncher,3.1 以前少了 .launch(ch09)。
① .env 裡設了變數,容器裡卻讀不到
- 原因:
.env 是給 compose 檔本身做 ${VAR} 展開的,不會自動送進容器(ch11)。 - 解法:在
environment: 明確寫 KEY: ${VAR},或用 env_file:。
② 覆蓋檔改了 ports 卻變成兩個都在
- 原因:多檔合併時純量是覆蓋,清單是相加(ch11)。
- 解法:先
docker compose config 看合併結果。
③ M1 Mac 建的映像丟到伺服器噴 exec format error
- 原因:建出來是 arm64,伺服器是 x86(ch04)。Java 的「跨平台」救不了你——JVM 與原生函式庫都有架構之分。
- 解法:
--platform linux/amd64 或 buildx 建多架構。
④ 掛載後容器裡原本的檔案不見了
- 原因:掛載是蓋住不是合併(ch06)。經典案例是
node_modules/target 被遮掉。 - 解法:再疊一個 volume 把子路徑蓋回來。
⑤ 改了映像裡的初始檔案卻沒生效
- 原因:volume 已有內容,「從映像複製內容」只發生在 volume 為空的第一次(ch06)。
⑥ 把 .sql 掛到 SQL Server 的 initdb.d 卻沒反應
- 原因:SQL Server 官方映像沒有這個機制,那是 PostgreSQL/MySQL 才有的,掛上去會被安靜忽略(ch12)。
- 解法:一次性 init 容器跑 sqlcmd,或用 Flyway/Liquibase。
⑦ 宿主機上的檔案改不動、刪不掉
- 原因:容器以 root 執行,寫出的檔案擁有者是 root(Docker 預設沒開 user namespace,ch02/ch06)。
⑧ 同事在同網段連得到我的資料庫
- 原因:
-p 1433:1433 預設綁 0.0.0.0,而且 Docker 的 iptables 規則可能繞過你的防火牆(ch07)。 - 解法:
-p 127.0.0.1:1433:1433。
⑨ 磁碟突然滿了
- 原因:停止未刪的容器、懸空映像、孤兒 volume、建置快取、沒設輪替的日誌檔(ch05)。
症狀 → 章節速查
| 你看到的症狀 | 最可能原因 | 回去看 |
|---|
| 重建後資料不見 | 資料在可寫層,沒掛 volume | ch03/ch06 |
| Exited (0) 但應該常駐 | 主程式不是前景執行 | ch05 |
| Exited (137) / OOMKilled | 超過 cgroup 記憶體上限(Java 常見) | ch02/ch09 |
| docker stop 卡 10 秒 | shell form 讓 sh 當 PID 1,SIGTERM 收不到 | ch05 |
| 容器間 Connection refused | 位址寫成 localhost | ch07 |
| 第一次啟動失敗、重啟就好 | depends_on 不等服務就緒 | ch11 |
| 改一行程式碼重建四分鐘 | COPY . . 在裝依賴之前,快取全滅 | ch08 |
| exec format error | arm64 映像跑在 x86 上 | ch04 |
| .env 設了讀不到 | .env 只給 compose 檔展開,不進容器 | ch11 |
| 掛載後原本檔案消失 | 掛載是蓋住不是合併 | ch06 |
| initdb.d 腳本沒執行 | SQL Server 沒有這個機制 | ch12 |
| 映像裡刪掉的密碼還在 | 層不可變,rm 只是 whiteout 遮蔽 | ch03/ch10 |
01 / 5
以下哪一組症狀組合,指向同一個根本原因?
A 映像太大 + 網路連不上
B docker stop 卡 10 秒 + 優雅關機沒觸發 + docker logs 空白 —— 都可能來自「你的程式不是 PID 1」(shell form)
C 資料消失 + 建置變慢
D OOMKilled + DNS 解析失敗
解析
shell form 把你的程式包進 /bin/sh -c,於是 sh 成為 PID 1:SIGTERM 送給 sh 被忽略也不轉發(卡 10 秒、優雅關機失效),而 docker logs 抓的是 PID 1 的輸出(可能看不到子行程的日誌)。用 docker top 一驗就知道。
02 / 5
你改了映像裡資料庫的初始設定檔,重新 build 並啟動卻完全沒生效。原因是?
A build 失敗了
B volume 裡已有內容——「從映像複製內容到 volume」只發生在 volume 為空的第一次,之後純粹是覆蓋
C 設定檔語法錯誤
D 需要重啟 Docker 服務
解析
要重新初始化必須先 docker volume rm 掉那個 volume(會刪掉所有資料)。同一個機制也解釋了為什麼 PostgreSQL/MySQL 的 initdb.d 腳本只跑一次。
03 / 5
團隊有人在 M1 Mac 開發、有人在 Windows,CI 跑在 x86 Linux。最容易出事的是?
A 程式碼編碼問題
B M1 建的 arm64 映像推到 registry 後在 x86 上噴 exec format error;另外 Windows 的 CRLF 換行會讓容器內的腳本噴 bad interpreter(Exit 126)
C 時區設定不同
D Docker 版本不同
解析
兩個都是跨平台團隊的常客。解法分別是:建置時指定 --platform linux/amd64(或用 buildx 建多架構)、以及在 .gitattributes 設定腳本檔一律用 LF 換行。
04 / 5
「資料不見了」的四種死法中,哪一種其實資料還在、可以救回來?
A 執行了 docker compose down -v
B 把專案資料夾改名——volume 實際名稱是 <專案名>_<volume名>,改名只是掛到了新的空 volume,舊的還在(docker volume ls 找得到)
C 容器被 docker rm 刪除且資料在可寫層
D 執行了 docker system prune --volumes
解析
改目錄名造成的「資料消失」是最容易嚇到人卻最容易救的一種:舊 volume 完好無損,用 docker volume ls 找到它,把專案名改回去(或在 compose 檔寫 name: 固定住)即可。其他三種都是真的刪除了。
05 / 5
下列哪個做法無法防止秘密外洩到映像裡?
A 用 BuildKit 的 --mount=type=secret
B 先 COPY 秘密檔進去,用完後在後續的 RUN 用 rm 刪掉
C 在 .dockerignore 排除 .env 與 .git
D 執行時用環境變數或 Secret 注入
解析
層是不可變的,rm 只是在上層加 whiteout 標記遮住它,下層那個檔案原封不動——任何人 docker save 解開就能翻出來。這是 ch03 的核心結論之一,也是最多人誤以為安全的做法。
點擊卡片翻面查看答案,共 5 張。
ANSWER
① 沒掛 volume,資料在可寫層
② 專案資料夾改名 → 換了空 volume(舊的還在,可救!)
③ compose down -v
④ 匿名 volume 沒接回去
點擊翻回
QUESTION
容器起不來的 exit code 速記?
點擊翻面
ANSWER
0 主程式不是前景 | 1 應用錯誤看 logs
126 沒執行權限/CRLF | 127 指令不存在(distroless 沒 bash)
137 OOMKilled | 143 SIGTERM 正常結束(好消息)
點擊翻回
QUESTION
連不到服務的第一名原因?
點擊翻面
ANSWER
容器裡把位址寫成 localhost
localhost = 這個容器自己
要用服務名稱 + 容器內部埠
點擊翻回
ANSWER
① shell form → 優雅關機失效
② 沒設 MaxRAMPercentage → OOMKilled
③ 沒設啟動寬限期 → 啟動途中被判死
④ Alpine 的 musl libc 問題
⑤ 分層 JAR 的 JarLauncher 路徑(3.2+ 多了 .launch)
點擊翻回
QUESTION
三個「以為有效其實無效」的做法?
點擊翻面
ANSWER
① RUN rm 刪掉映像裡的秘密(層裡還在)
② .env 設變數以為會進容器(只給 compose 展開)
③ 掛 .sql 到 SQL Server 的 initdb.d(根本沒這機制)
點擊翻回