容器化技術 新手地雷圖鑑
附錄

新手地雷圖鑑

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)。
症狀 → 章節速查
你看到的症狀最可能原因回去看
重建後資料不見資料在可寫層,沒掛 volumech03/ch06
Exited (0) 但應該常駐主程式不是前景執行ch05
Exited (137) / OOMKilled超過 cgroup 記憶體上限(Java 常見)ch02/ch09
docker stop 卡 10 秒shell form 讓 sh 當 PID 1,SIGTERM 收不到ch05
容器間 Connection refused位址寫成 localhostch07
第一次啟動失敗、重啟就好depends_on 不等服務就緒ch11
改一行程式碼重建四分鐘COPY . . 在裝依賴之前,快取全滅ch08
exec format errorarm64 映像跑在 x86 上ch04
.env 設了讀不到.env 只給 compose 檔展開,不進容器ch11
掛載後原本檔案消失掛載是蓋住不是合併ch06
initdb.d 腳本沒執行SQL Server 沒有這個機制ch12
映像裡刪掉的密碼還在層不可變,rm 只是 whiteout 遮蔽ch03/ch10

練習題 點選選項查看解析

0 / 5
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 張。

QUESTION
資料消失的四種死法?
點擊翻面
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 = 這個容器自己
要用服務名稱 + 容器內部埠
點擊翻回
QUESTION
Java 五大地雷?
點擊翻面
ANSWER
① shell form → 優雅關機失效
② 沒設 MaxRAMPercentage → OOMKilled
③ 沒設啟動寬限期 → 啟動途中被判死
④ Alpine 的 musl libc 問題
⑤ 分層 JAR 的 JarLauncher 路徑(3.2+ 多了 .launch)
點擊翻回
QUESTION
三個「以為有效其實無效」的做法?
點擊翻面
ANSWER
① RUN rm 刪掉映像裡的秘密(層裡還在)
② .env 設變數以為會進容器(只給 compose 展開)
③ 掛 .sql 到 SQL Server 的 initdb.d(根本沒這機制)
點擊翻回