前面十二章建立的原理,在這一章變成可以立刻用的排查能力。
容器出問題時最大的陷阱是亂猜——重啟看看、刪掉重建看看、加個 --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 <容器> # ③ 它實際被設定成什麼樣(不是你以為的那樣)ps 驗證。「我假設它們在同一個網路」→ network inspect 驗證。當某個假設被推翻,你就找到問題了。ERROR: failed to compute cache key: "/target/app.jar" not foundmvn package。.dockerignore 排除了(很常見!你排除了 target/,卻又想 COPY target/app.jar)。../ 拿不到,ch08)。docker build --progress=plain . 2>&1 | head -50 # 看完整輸出
ls -la target/ # 檔案真的在嗎RUN apt-get update 被寫成獨立一層並被快取住,套件索引過期(ch08)。update 和 install 串在同一個 RUN;或臨時 docker build --no-cache。.dockerignore,target/、node_modules/、.git/ 全被送進去(ch08)。COPY pom.xml 先裝依賴 + BuildKit cache mount(ch08、ch09)。docker build --platform linux/amd64 ... 或用 buildx 建多架構。docker build --no-cache . 看看是不是快取問題;docker history 看每層的產生指令。Exit code 是容器留給你的第一手線索,它會告訴你「是誰殺的、怎麼死的」。
docker ps -a # 看 STATUS 欄的 Exited (N)
docker inspect <容器> --format '{{.State.ExitCode}} {{.State.OOMKilled}} {{.State.Error}}'docker run ubuntu 沒給 -it)。docker logs,答案在裡面(設定錯誤、找不到檔案、連不上相依服務…)。chmod +x),或腳本的換行是 Windows 的 CRLF(bad interpreter)。OOMKilled: true → 記憶體超過 cgroup 上限(ch02、ch09)。也可能是 docker stop 寬限期到了強制砍(ch05)。docker compose logs --tail 200 <服務> # 抓崩潰前的最後訊息
docker compose ps -a # 看重啟次數restart 拿掉,或用:docker run --rm -it --entrypoint sh myapp:1.0
# 手動進去,自己執行啟動指令,看完整錯誤這招非常有用——繞過 entrypoint 直接進容器,逐步驗證。docker logs 抓的是 PID 1 的輸出,而你的程式是被 shell 包起來的子行程(shell form,ch05)。用 docker top 確認誰是 PID 1。ch07 給過五步驟,這裡補上更完整的判斷邏輯。關鍵是先確定「是誰連不到誰」。
docker ps # PORTS 欄有沒有 0.0.0.0:8080->8080/tcp?-p/compose 沒寫 ports。注意 EXPOSE 不會開埠(ch07)。127.0.0.1 而不是 0.0.0.0:docker compose exec app ss -lntp # 看監聽位址-p 宿主機:容器,左外右內(ch07)。docker compose ps 看是不是 healthy;logs 看它有沒有真的啟動完成。localhost。應該用服務名稱(ch07)。-p 映射後的外部埠(ch07)。docker network inspect myapp_default | grep -A3 Containersdocker compose exec app getent hosts sqlserverdocker compose exec app ping -c 2 8.8.8.8 # 通不通
docker compose exec app getent hosts google.com # 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 # 即時看「已用 / 上限」,觀察是否持續攀升-XX:MaxRAMPercentage(ch09)。沒設的話 JVM 可能用了不合理的預設。-XX:+HeapDumpOnOutOfMemoryError,事後 docker cp 撈出 dump 分析(ch09)。docker inspect 的 .State.Health.Log 有最近幾次的檢查結果)。docker system df -v # 誰在吃空間(容器/映像/volume/建置快取)
df -h # 宿主機整體# compose 裡設定
logging:
driver: json-file
options:
max-size: '10m'
max-file: '3'docker stats 看 CPU% 是不是一直貼在上限。cgroup 的 CPU 限制會讓 process 被強制暫停(throttling),對 Java 的 GC 影響特別明顯。start_period(ch05);真的在意就研究 CDS 或 GraalVM 原生映像。docker events # 即時看 Docker 層級發生了什麼
docker events --since 1h --filter 'container=myapp-app-1'
docker inspect <容器> --format '{{json .State.Health}}' | jqdocker events 會告訴你容器什麼時候 die、restart、被 OOM kill、健康狀態何時改變——對付「半夜自己重啟」這種問題非常有效。
# 結束狀態一次看完
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}}' | jqdocker top <容器> # 容器內的 process → 驗證誰是 PID 1(ch05)
docker diff <容器> # 相對於映像,檔案系統改了什麼(ch03 可寫層)
docker cp <容器>:/path ./ # 撈檔案出來(heap dump、log)
docker stats --no-stream # 資源用量快照
docker history <映像> # 映像每層的來源與大小
docker compose config # 合併後的最終設定(ch11)# 容器啟動就死,來不及 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(出問題的服務+它相依的服務)OOMKilled 狀態docker compose config(實際生效的設定,非你以為的)docker version、作業系統、CPU 架構(M 系列 Mac?)git log 和 docker events 會幫你回憶。| Code | 意義 | 常見原因 | 先查哪裡 |
|---|---|---|---|
| 0 | 正常結束 | 主程式做完就退出(daemon 化、沒有 -it) | ch05:容器的命 = PID 1 的命 |
| 1 | 應用程式錯誤 | 設定錯誤、找不到檔案、連不上相依服務 | docker logs(答案幾乎都在裡面) |
| 125 | Docker 本身錯誤 | 指令參數打錯、映像不存在 | 你打的那行指令 |
| 126 | 指令不可執行 | 腳本沒 chmod +x、CRLF 換行 | 檔案權限與換行格式 |
| 127 | 指令不存在 | 打錯字、精簡映像沒有那個工具(distroless 沒 bash) | 映像裡到底有什麼 |
| 137 | 被 SIGKILL(128+9) | OOM Killer、或 stop 寬限期到期強砍 | docker inspect 的 OOMKilled |
| 139 | Segmentation fault(128+11) | 原生程式崩潰、架構不符、JNI 問題 | 映像架構、原生函式庫 |
| 143 | 收到 SIGTERM 正常結束(128+15) | 這是好消息——優雅關機生效了 | 不用查,正常 |
| 症狀 | 最可能原因 | 驗證指令 |
|---|---|---|
| COPY failed: not found | 檔案不存在,或被 .dockerignore 排除 | ls -la target/;檢查 .dockerignore |
| apt-get 404 | update 單獨一層被快取,索引過期 | 改成同一個 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 <服務名>;ps 看 healthy |
| 解析得到但連不上 | 服務只監聽 127.0.0.1 | exec ... 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 |
點擊卡片翻面查看答案,共 12 張。