容器化技術 ch13 除錯圖鑑:容器出事時的系統化排查
下一章→
CH 13 維運技能

除錯圖鑑:容器出事時的系統化排查

先分類症狀Exit Code 對照表建置失敗/起不來/連不到/跑一跑掛掉工具箱與求助前的資訊蒐集

前面十二章建立的原理,在這一章變成可以立刻用的排查能力。

容器出問題時最大的陷阱是亂猜——重啟看看、刪掉重建看看、加個 --privileged 看看。這樣就算矇對了,下次還是不會。

這一章給你一套流程:先用症狀分類,再照該類別的順序查下去。每個步驟都會回扣到前面某一章的原理,因為除錯的本質就是驗證你對系統的理解哪裡出錯了。

不同類型的問題,排查路徑完全不同。花十秒鐘分類,勝過亂試十分鐘。

問題發生在哪個階段?
│
├─ ① docker build 就失敗          → 建置類(本章第 2 張卡)
│
├─ ② build 成功,但容器起不來      → 啟動類(第 3 張卡)
│     docker ps 看不到/狀態是 Exited 或 Restarting
│
├─ ③ 容器 Up,但連不到/功能不通    → 網路類(第 4 張卡,ch07 延伸)
│
├─ ④ 一開始正常,跑一陣子掛掉      → 執行期類(第 5 張卡)
│
└─ ⑤ 能跑但很慢                   → 效能類(第 5 張卡)

三個萬用起手式(不管哪一類都先做)

docker compose ps -a       # ① 現在到底有哪些容器、什麼狀態(-a 才看得到已結束的)
docker compose logs --tail 100 <服務>   # ② 它自己怎麼說
docker inspect <容器>      # ③ 它實際被設定成什麼樣(不是你以為的那樣)
🚨 最常見的除錯錯誤:只看應用的錯誤訊息,不看它相依的服務。
「app 說連不到資料庫」→ 十次有八次真正的原因是資料庫容器根本沒起來(密碼太弱被拒、記憶體不足、EULA 沒接受)。先確認相依服務的狀態,再查應用本身。
💡 除錯心法:每一步都在驗證一個假設,而不是隨機嘗試。「我假設容器在跑」→ ps 驗證。「我假設它們在同一個網路」→ network inspect 驗證。當某個假設被推翻,你就找到問題了。

症狀 A:COPY failed: file not found

ERROR: failed to compute cache key: "/target/app.jar" not found
  • 原因 1:檔案真的不存在——你忘了先 mvn package。
  • 原因 2:檔案被 .dockerignore 排除了(很常見!你排除了 target/,卻又想 COPY target/app.jar)。
  • 原因 3:路徑在 build context 之外(../ 拿不到,ch08)。
  • 驗證:
    docker build --progress=plain . 2>&1 | head -50   # 看完整輸出
    ls -la target/                                     # 檔案真的在嗎

症狀 B:apt-get install 噴 404

  • 原因:RUN apt-get update 被寫成獨立一層並被快取住,套件索引過期(ch08)。
  • 解法:update 和 install 串在同一個 RUN;或臨時 docker build --no-cache。

症狀 C:建置超級慢、或 Sending build context 好幾 GB

  • 原因:沒有 .dockerignore,target/、node_modules/、.git/ 全被送進去(ch08)。
  • 順便檢查:Maven 依賴是不是每次都重下?→ 用 COPY pom.xml 先裝依賴 + BuildKit cache mount(ch08、ch09)。

症狀 D:exec format error(在建置或執行時)

  • 原因:架構不符——在 M 系列 Mac 建的 arm64 映像丟到 x86 機器(ch04)。
  • 解法:docker build --platform linux/amd64 ... 或用 buildx 建多架構。

症狀 E:我改了 Dockerfile,但好像沒生效

  • 原因:快取命中了你以為會重跑的層。記住 RUN 只比對「指令字串」不檢查實際結果(ch08)。
  • 驗證:docker build --no-cache . 看看是不是快取問題;docker history 看每層的產生指令。

Exit code 是容器留給你的第一手線索,它會告訴你「是誰殺的、怎麼死的」。

docker ps -a                        # 看 STATUS 欄的 Exited (N)
docker inspect <容器> --format '{{.State.ExitCode}} {{.State.OOMKilled}} {{.State.Error}}'

Exit Code 對照(背起來很值得)

  • 0:正常結束。如果你預期它常駐卻拿到 0 → 主程式做完事就退出了(ch05:容器的命等於 PID 1 的命;例如 daemon 化、或 docker run ubuntu 沒給 -it)。
  • 1:應用程式自己的錯誤。去看 docker logs,答案在裡面(設定錯誤、找不到檔案、連不上相依服務…)。
  • 125:Docker 本身的錯誤——指令參數打錯、映像不存在。
  • 126:指令找到了但不能執行——沒有執行權限(腳本忘了 chmod +x),或腳本的換行是 Windows 的 CRLF(bad interpreter)。
  • 127:指令根本不存在——打錯字,或精簡映像裡沒有那個工具(distroless 沒有 bash!ch10)。
  • 137:被 SIGKILL 殺掉(128+9)。檢查 OOMKilled: true → 記憶體超過 cgroup 上限(ch02、ch09)。也可能是 docker stop 寬限期到了強制砍(ch05)。
  • 139:Segmentation fault(128+11)——原生程式碼崩潰,常見於架構不符或 JNI 函式庫問題。
  • 143:收到 SIGTERM 後正常結束(128+15)。這其實是好消息——代表優雅關機生效了(ch05)。

症狀:容器一直 Restarting(CrashLoop)

docker compose logs --tail 200 <服務>      # 抓崩潰前的最後訊息
docker compose ps -a                        # 看重啟次數
  • 重啟策略不是解方:如果是設定錯誤導致啟動即崩潰,重啟一萬次也一樣(ch05)。先看 logs 找真正的死因。
  • 如果日誌太快滾掉看不到,暫時把 restart 拿掉,或用:
    docker run --rm -it --entrypoint sh myapp:1.0
    # 手動進去,自己執行啟動指令,看完整錯誤
    這招非常有用——繞過 entrypoint 直接進容器,逐步驗證。

症狀:日誌完全是空的

  • 原因 1:應用把日誌寫進容器內的檔案,而不是 stdout(ch05)。
  • 原因 2:程式根本沒啟動就死了 → 檢查 exit code 125/126/127。
  • 原因 3:docker logs 抓的是 PID 1 的輸出,而你的程式是被 shell 包起來的子行程(shell form,ch05)。用 docker top 確認誰是 PID 1。

ch07 給過五步驟,這裡補上更完整的判斷邏輯。關鍵是先確定「是誰連不到誰」。

情境 A:你(宿主機)連不到容器

docker ps        # PORTS 欄有沒有 0.0.0.0:8080->8080/tcp?
  1. 沒有 PORTS 對應 → 忘了 -p/compose 沒寫 ports。注意 EXPOSE 不會開埠(ch07)。
  2. 有 PORTS 但連不上 → 容器內的服務可能監聽在 127.0.0.1 而不是 0.0.0.0:
    docker compose exec app ss -lntp    # 看監聽位址
  3. 埠寫反了 → -p 宿主機:容器,左外右內(ch07)。

情境 B:容器 A 連不到容器 B(最常見)

  1. 先確認 B 真的健康:docker compose ps 看是不是 healthy;logs 看它有沒有真的啟動完成。
  2. 位址寫錯——第一名的原因:寫成 localhost。應該用服務名稱(ch07)。
  3. 埠寫錯:容器間要用容器內部埠,不是 -p 映射後的外部埠(ch07)。
  4. 不在同一個網路(跨 compose 專案的頭號原因,ch11):
    docker network inspect myapp_default | grep -A3 Containers
  5. 名稱解析測試:
    docker compose exec app getent hosts sqlserver

情境 C:容器連不到外部網路

docker compose exec app ping -c 2 8.8.8.8        # 通不通
docker compose exec app getent hosts google.com   # DNS 解不解得開
  • IP 通但 DNS 不通 → DNS 設定問題(公司 VPN、自訂 DNS 常見)。
  • 都不通 → 網路模式是 none?宿主機防火牆擋住 docker0 的轉發?

沒有工具可用時的救星

# 精簡映像裡沒有 ping/curl/ss 時,開一個工具容器加入同一個網路
docker run --rm -it --network myapp_default nicolaka/netshoot bash
# 裡面有 ping / dig / curl / ss / tcpdump / nmap / iperf

# 或直接共用某個容器的網路 namespace,用它的視角看世界
docker run --rm -it --network container:myapp-app-1 nicolaka/netshoot bash
⭐ 第二個指令特別強大:--network container:<名稱> 讓工具容器和目標容器共用同一個 network namespace(ch02)——你看到的網路環境跟目標容器完全一樣,非常適合診斷 distroless 這種進不去的映像。

執行期崩潰:先確認是不是記憶體

docker inspect <容器> --format '{{.State.OOMKilled}} {{.State.ExitCode}}'
#  true 137  → 確定是 OOM

docker stats                # 即時看「已用 / 上限」,觀察是否持續攀升
  • Java 應用:先檢查有沒有設 -XX:MaxRAMPercentage(ch09)。沒設的話 JVM 可能用了不合理的預設。
  • 記憶體持續成長不下降 → 可能是真的記憶體洩漏。開啟 -XX:+HeapDumpOnOutOfMemoryError,事後 docker cp 撈出 dump 分析(ch09)。
  • 不是 OOM 但一直重啟 → 看 healthcheck 是不是一直失敗(docker inspect 的 .State.Health.Log 有最近幾次的檢查結果)。

磁碟滿了(很常見,症狀千奇百怪)

docker system df -v        # 誰在吃空間(容器/映像/volume/建置快取)
df -h                      # 宿主機整體
  • 四大兇手:停止但沒刪的容器、懸空映像、沒人用的 volume、建置快取(ch05)。
  • 還有一個常被忽略的:容器的日誌檔。沒設輪替的話會無限長大:
    # compose 裡設定
    logging:
      driver: json-file
      options:
        max-size: '10m'
        max-file: '3'

效能問題

  • 寫入很慢 → 資料寫在 overlay 可寫層而非 volume(ch03 的 Copy-on-Write 代價)。資料庫、大量檔案寫入一定要用 volume。
  • Windows/macOS 上檔案存取極慢 → bind mount 經過檔案共享層(ch06)。只掛必要目錄,或把相依套件目錄改用 volume。
  • CPU 被限制 → docker stats 看 CPU% 是不是一直貼在上限。cgroup 的 CPU 限制會讓 process 被強制暫停(throttling),對 Java 的 GC 影響特別明顯。
  • Java 啟動慢 → 冷啟動本來就慢,記得 healthcheck 設 start_period(ch05);真的在意就研究 CDS 或 GraalVM 原生映像。

間歇性問題(最難搞)

docker events                          # 即時看 Docker 層級發生了什麼
docker events --since 1h --filter 'container=myapp-app-1'
docker inspect <容器> --format '{{json .State.Health}}' | jq

docker events 會告訴你容器什麼時候 die、restart、被 OOM kill、健康狀態何時改變——對付「半夜自己重啟」這種問題非常有效。

inspect 的實用格式化(比翻整份 JSON 快很多)

# 結束狀態一次看完
docker inspect <容器> --format '{{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}}'

# 這個容器的 IP
docker inspect <容器> --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'

# 掛了哪些東西(「我改的檔案怎麼沒生效」第一站)
docker inspect <容器> --format '{{json .Mounts}}' | jq

# 實際生效的環境變數(驗證設定有沒有進去)
docker inspect <容器> --format '{{json .Config.Env}}' | jq

# 健康檢查的歷史紀錄
docker inspect <容器> --format '{{json .State.Health}}' | jq

其他常備工具

docker top <容器>              # 容器內的 process → 驗證誰是 PID 1(ch05)
docker diff <容器>             # 相對於映像,檔案系統改了什麼(ch03 可寫層)
docker cp <容器>:/path ./      # 撈檔案出來(heap dump、log)
docker stats --no-stream       # 資源用量快照
docker history <映像>          # 映像每層的來源與大小
docker compose config          # 合併後的最終設定(ch11)

繞過 entrypoint 直接進去看

# 容器啟動就死,來不及 exec 時的救命招
docker run --rm -it --entrypoint sh myapp:1.0

# 進去之後手動驗證
ls -la /app          # 檔案在不在、擁有者對不對
whoami               # 現在是什麼身分(ch10 的 USER)
env | sort           # 環境變數
java -version        # 執行環境
java -jar /app/app.jar   # 手動啟動,看完整錯誤

求助前先蒐集這些(也是給自己的檢查清單)

☐ 症狀分類:建置/啟動/連線/執行期/效能?
☐ docker compose ps -a 的輸出(各服務狀態)
☐ docker compose logs --tail 100(出問題的服務+它相依的服務)
☐ Exit code 與 OOMKilled 狀態
☐ docker compose config(實際生效的設定,非你以為的)
☐ Dockerfile 與 compose 檔本身
☐ 最近改了什麼(上次正常是什麼時候?中間動了哪些東西?)
☐ 環境資訊:docker version、作業系統、CPU 架構(M 系列 Mac?)
⭐ 最後一條最重要:「上次正常是什麼時候、中間改了什麼」——這個問題解決的案例比所有指令加起來還多。如果想不起來,git log 和 docker events 會幫你回憶。
Exit Code 完整對照表
Code意義常見原因先查哪裡
0正常結束主程式做完就退出(daemon 化、沒有 -it)ch05:容器的命 = PID 1 的命
1應用程式錯誤設定錯誤、找不到檔案、連不上相依服務docker logs(答案幾乎都在裡面)
125Docker 本身錯誤指令參數打錯、映像不存在你打的那行指令
126指令不可執行腳本沒 chmod +x、CRLF 換行檔案權限與換行格式
127指令不存在打錯字、精簡映像沒有那個工具(distroless 沒 bash)映像裡到底有什麼
137被 SIGKILL(128+9)OOM Killer、或 stop 寬限期到期強砍docker inspect 的 OOMKilled
139Segmentation fault(128+11)原生程式崩潰、架構不符、JNI 問題映像架構、原生函式庫
143收到 SIGTERM 正常結束(128+15)這是好消息——優雅關機生效了不用查,正常
症狀 → 原因 → 該下什麼指令
症狀最可能原因驗證指令
COPY failed: not found檔案不存在,或被 .dockerignore 排除ls -la target/;檢查 .dockerignore
apt-get 404update 單獨一層被快取,索引過期改成同一個 RUN;或 --no-cache
容器立刻 Exited (0)主程式不是常駐(daemon 化/缺 -it)docker top 看 PID 1 是什麼
一直 Restarting啟動即崩潰,重啟策略救不了logs --tail 200;--entrypoint sh 進去手動跑
docker logs 是空的日誌寫進檔案而非 stdout,或程式沒啟動就死docker top;檢查 exit code
容器間 Connection refused位址寫成 localhost,或服務沒真的就緒getent hosts &lt;服務名&gt;;ps 看 healthy
解析得到但連不上服務只監聽 127.0.0.1exec ... ss -lntp
跑一陣子被殺OOMKilled(Java 沒設 MaxRAMPercentage)inspect 的 OOMKilled;docker stats
磁碟突然滿了停止的容器/懸空映像/volume/建置快取/日誌沒輪替docker system df -v
寫入很慢資料寫在 overlay 可寫層(CoW 代價)inspect .Mounts 確認有沒有掛 volume
改了設定沒生效覆蓋檔合併結果不如預期/.env 沒送進容器docker compose config;inspect .Config.Env
半夜自己重啟healthcheck 失敗、OOM、宿主機重開docker events --since 12h

練習題 點選選項查看解析

0 / 9
01 / 9
容器狀態顯示 Exited (0),但你預期它是常駐服務。這代表什麼?
A 容器啟動失敗
B 主程式正常執行完畢就退出了——容器的命等於 PID 1 的命,常見於服務被 daemon 化到背景,或執行的指令本來就會結束
C 記憶體不足
D 映像檔損毀
解析
Exit 0 是「正常結束」不是錯誤。容器裡的服務必須在前景執行(nginx -g 'daemon off;'、java -jar 直接跑),不能像傳統伺服器那樣丟到背景——啟動指令一結束,PID 1 就沒了,容器跟著死。用 docker top 可以確認 PID 1 到底是什麼。
02 / 9
Exit code 137 且 docker inspect 顯示 OOMKilled: true。第一個該檢查什麼?
A 網路設定
B 記憶體:容器的 cgroup 上限是多少、應用實際用了多少;Java 應用要特別確認有沒有設 -XX:MaxRAMPercentage
C 磁碟空間
D 映像版本
解析
137 = 128 + 9(SIGKILL),配合 OOMKilled: true 就確定是核心的 OOM Killer 動的手。Java 沒設 MaxRAMPercentage 時,JVM 可能用不合理的預設值撐爆容器配額。用 docker stats 觀察記憶體是否持續攀升可判斷是配額太小還是真的洩漏。
03 / 9
Exit code 127 代表什麼?
A 權限不足
B 指令根本不存在——可能是打錯字,或精簡映像裡沒有那個工具(例如 distroless 映像裡沒有 bash)
C 記憶體不足
D 網路逾時
解析
126 和 127 很容易混淆:126 是「找到了但不能執行」(沒有執行權限、CRLF 換行導致 bad interpreter),127 是「根本找不到這個指令」。在 distroless 映像上執行 docker exec ... bash 就會拿到 127,因為那是刻意的設計(ch10)。
04 / 9
容器一直 Restarting,但日誌滾得太快看不清楚。最有效的做法是?
A 加大重啟次數限制
B 用 docker run --rm -it --entrypoint sh myapp:1.0 繞過 entrypoint 進去,手動執行啟動指令看完整錯誤
C 改用 --privileged 執行
D 刪除映像重新建置
解析
這是最實用的救命招:繞過 entrypoint 直接得到一個 shell,然後逐步驗證——ls 看檔案在不在、whoami 看身分對不對、env 看環境變數、手動跑啟動指令看完整輸出。重啟策略本身不是解方:設定錯誤導致啟動即崩潰的話,重啟一萬次結果一樣。
05 / 9
app 說「連不到資料庫」,最常見的真正原因是?
A 網路設定錯誤
B 資料庫容器根本沒起來(密碼太弱被拒、記憶體不足、EULA 沒接受)——先確認相依服務狀態,再查應用本身
C 防火牆阻擋
D JDBC driver 版本不符
解析
最常見的除錯錯誤是只看應用的錯誤訊息,不看它相依的服務。連線失敗只是症狀。萬用起手式:docker compose ps -a 看誰活著、logs 看相依服務怎麼說。這一步能解決大半的案例。
06 / 9
映像是 distroless 沒有任何工具,要怎麼診斷它的網路問題?
A 只能改用有 shell 的映像重新部署
B 用 docker run --rm -it --network container:<目標容器> nicolaka/netshoot bash,讓工具容器與目標容器共用同一個 network namespace
C 從宿主機直接 ping 容器 IP
D 查看 Docker daemon 的日誌
解析
--network container:<名稱> 讓工具容器進入目標容器的 network namespace(ch02),看到的網路環境完全一樣:同樣的網卡、IP、路由、DNS。這是診斷精簡映像網路問題的標準手法,裡面有 ping/dig/curl/ss/tcpdump 一應俱全。
07 / 9
「我明明改了 compose 設定卻沒生效」,最該先執行的是?
A docker compose restart
B docker compose config 看變數展開與多檔合併後的最終設定;必要時再用 docker inspect --format '{{json .Config.Env}}' 確認容器內實際生效的環境變數
C docker system prune
D 重新安裝 Docker
解析
不要用猜的,直接看工具實際認定的內容。常見成因有兩個:一是覆蓋檔的合併規則(清單型別是相加不是取代,ch11);二是把變數寫在 .env 卻沒有在 environment 裡引用——.env 只用於 compose 檔的 ${VAR} 展開,不會自動送進容器。
08 / 9
服務半夜會自己重啟,白天查什麼都正常。哪個工具最適合追查?
A docker stats
B docker events --since 12h:它會顯示容器何時 die、restart、被 OOM kill、健康狀態何時改變,能還原事件的時間序
C docker history
D docker diff
解析
docker events 是處理間歇性問題的利器,因為它記錄的是 Docker 層級的事件時間序,可以看出「先健康檢查失敗、然後被 kill、然後重啟」這樣的因果鏈。搭配 inspect 的 .State.Health.Log 可以看到最近幾次健康檢查的實際輸出。
09 / 9
求助(或自己排查)前最該回答的問題是?
A Docker 版本是多少
B 「上次正常是什麼時候、中間改了什麼」——這個問題解決的案例比所有指令加起來還多
C 容器用了多少記憶體
D 映像有多大
解析
多數故障都有觸發原因,而觸發原因通常是「某個改動」。想不起來改了什麼時,git log 和 docker events 可以幫你回憶。當然,附上 ps -a、logs、exit code、compose config 這些客觀資料也同樣重要——但時間軸資訊常常是破案關鍵。

點擊卡片翻面查看答案,共 12 張。

QUESTION
除錯的第一步?
點擊翻面
ANSWER
分類症狀:建置失敗/起不來/連不到/跑一陣子掛/很慢
不同類型排查路徑完全不同
花十秒分類,勝過亂試十分鐘
點擊翻回
QUESTION
三個萬用起手式?
點擊翻面
ANSWER
docker compose ps -a(誰活著,-a 才看得到已結束的)
docker compose logs --tail 100(它自己怎麼說)
docker inspect(它實際被設定成什麼樣)
點擊翻回
QUESTION
最常見的除錯錯誤?
點擊翻面
ANSWER
只看應用的錯誤訊息,不看它相依的服務
「app 連不到 DB」十次有八次是 DB 根本沒起來
→ 先確認相依服務狀態
點擊翻回
QUESTION
Exit code 0 / 1 / 125 代表?
點擊翻面
ANSWER
0:正常結束(預期常駐卻拿到 → 主程式不是前景常駐)
1:應用自己的錯誤 → 看 logs
125:Docker 本身錯誤(參數打錯、映像不存在)
點擊翻回
QUESTION
Exit code 126 / 127 的差別?
點擊翻面
ANSWER
126:找到了但不能執行(沒 chmod +x、CRLF 換行)
127:根本不存在(打錯字、distroless 沒有 bash)
點擊翻回
QUESTION
Exit code 137 / 139 / 143?
點擊翻面
ANSWER
137 = 128+9 SIGKILL → 檢查 OOMKilled
139 = 128+11 Segfault → 架構不符、JNI 問題
143 = 128+15 SIGTERM 正常結束 → 好消息,優雅關機生效
點擊翻回
QUESTION
容器一直 Restarting 怎麼查?
點擊翻面
ANSWER
logs --tail 200 抓崩潰前訊息
最有效:docker run --rm -it --entrypoint sh myapp:1.0
→ 繞過 entrypoint 進去手動執行,看完整錯誤
點擊翻回
QUESTION
docker logs 空白的三個原因?
點擊翻面
ANSWER
① 日誌寫進容器內檔案而非 stdout
② 程式沒啟動就死(查 exit code 125/126/127)
③ 程式是 shell 的子行程,PID 1 不是它(docker top 確認)
點擊翻回
QUESTION
distroless 映像怎麼診斷網路?
點擊翻面
ANSWER
docker run --rm -it --network container:<目標> nicolaka/netshoot bash
共用目標容器的 network namespace → 看到的網路環境完全一樣
(裡面有 ping/dig/curl/ss/tcpdump)
點擊翻回
QUESTION
「設定沒生效」查什麼?
點擊翻面
ANSWER
docker compose config(變數展開與多檔合併後的結果)
docker inspect --format '{{json .Config.Env}}'(容器內實際的環境變數)
常見成因:清單合併是相加、.env 沒被引用
點擊翻回
QUESTION
間歇性問題用什麼工具?
點擊翻面
ANSWER
docker events --since 12h
顯示 die / restart / OOM kill / 健康狀態變化的時間序
搭配 inspect 的 .State.Health.Log 看檢查輸出
點擊翻回
QUESTION
求助前最重要的一個問題?
點擊翻面
ANSWER
「上次正常是什麼時候、中間改了什麼」
這個問題解決的案例比所有指令加起來還多
(想不起來就查 git log 和 docker events)
點擊翻回