Docker Container 錯誤排查指南
English Abstract: When a container exits with code 137 or 143, it's tempting to assume OOM or a clean shutdown — but both assumptions are often wrong. This post walks through reading docker inspect's ExitCode and OOMKilled fields correctly, decoding the 128+signal exit code convention, cross-checking kernel-level OOM killer logs with dmesg/journalctl, setting sane memory limits, and the classic shell-form vs exec-form CMD pitfall that silently breaks SIGTERM handling.
當 production 裡的某個 container 無預警停掉,docker ps 已經看不到它,docker ps -a 卻顯示 Exited (137),第一個念頭通常是「是不是被 OOM killer 幹掉了」。但 137 不一定代表記憶體不足,143 也不一定代表你自己下了 docker stop。這篇文章從 docker inspect 的欄位意義開始,拆解 Exit Code 與訊號的對應關係、怎麼用 dmesg/journalctl 找出真正的 OOM killer 記錄,到設定 memory limit 與讓應用程式正確處理 SIGTERM,建立一套完整的排查流程。
(一)用 docker inspect 看懂 ExitCode 與 OOMKilled
container 停掉的瞬間,docker ps -a 只能看到一個簡化過的狀態字串,真正有用的欄位要從 docker inspect 的 State 區塊才看得到。先執行:
docker inspect <container_id> --format '{{json .State}}' | python3 -m json.tool
輸出範例:
{
"Status": "exited",
"Running": false,
"Paused": false,
"Restarting": false,
"OOMKilled": true,
"Dead": false,
"Pid": 0,
"ExitCode": 137,
"Error": "",
"StartedAt": "2026-10-11T03:14:22.123456789Z",
"FinishedAt": "2026-10-11T03:18:47.987654321Z"
}
三個最關鍵的欄位:
- ExitCode:process 實際回傳的結束碼。0 代表正常結束,非 0 代表異常;128 以上的值通常代表是被訊號終止(128+N,N 是訊號編號,下一節詳細拆解)。
- OOMKilled:布林值,
true代表 Docker 從 kernel 收到「這個 container 自己的 cgroup」觸發了 OOM killer。這是 Docker daemon 事後附加的旗標,不是 process 自己回傳的碼。 - StartedAt / FinishedAt:算出存活時間,用來判斷是「啟動初期即失敗」還是「穩定運行一段時間後才被終止」,兩者的排查方向完全不同。
要特別注意:OOMKilled: false 不代表這次退出跟記憶體完全無關,只代表「不是這個 container 自己的 cgroup 觸發的 OOM」。host 整體記憶體不足時,kernel 也可能先殺掉別的 process 而連帶影響這個 container,這種情況要改查 host 層級的紀錄,第(三)節會示範。
參考:Docker Engine API - ContainerInspect
(二)Exit Code 137 vs 143:訊號編號怎麼對應
Linux 的慣例是:process 如果是被訊號終止,shell 回報的 exit code 會是 128 + 訊號編號。常見的幾個:
| Exit Code | 訊號編號 | 訊號名稱 | 常見情境 |
|---|---|---|---|
| 137 | 9 | SIGKILL | OOM killer、docker kill、grace period 逾時後被強制終止 |
| 143 | 15 | SIGTERM | docker stop、編排系統(K8s)發出的正常終止信號 |
| 139 | 11 | SIGSEGV | 應用程式本身的記憶體存取錯誤(segfault) |
| 134 | 6 | SIGABRT | 應用程式呼叫 abort(),常見於斷言失敗 |
重點是:137 只代表「process 被 SIGKILL 終止」,不代表一定是 OOM。docker stop 預設會先送 SIGTERM,等待 10 秒(可用 --time 調整)沒反應才補一記 SIGKILL;如果應用程式沒有正確處理 SIGTERM(第(五)節會示範常見的陷阱),同樣會在逾時後拿到 137。要確認是不是真的 OOM,必須同時交叉比對 OOMKilled 欄位:
docker inspect <container_id> --format 'ExitCode={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}}'
ExitCode=137 OOMKilled=true→ 確定是 OOMExitCode=137 OOMKilled=false→ 是被 SIGKILL,但原因另有其他(手動 docker kill、stop 逾時強制終止等),要往第(五)節的方向查
(三)用 dmesg/journalctl 揪出真正的 OOM killer 記錄
當 OOMKilled 是 true,或者懷疑是 host 層級記憶體不足但這個欄位卻是 false,都要回到 kernel 的紀錄去確認。OOM killer 被觸發時一定會在 kernel ring buffer 留下記錄:
dmesg -T | grep -i "killed process"
或在有 systemd-journald 的環境下:
journalctl -k --since "-1 hour" | grep -i oom
典型的輸出會包含被殺掉的 process 名稱、pid,以及觸發當下的記憶體用量統計,可以用這些資訊確認時間點跟 container 停掉的時間是否吻合。
如果是 cgroup v2,也可以直接看這個 container 對應 cgroup 的 OOM 事件計數器,不需要搜 log:
cat /sys/fs/cgroup/system.slice/docker-<container_id_full>.scope/memory.events
輸出裡的 oom_kill 欄位如果大於 0,代表這個 cgroup 確實觸發過 OOM killer(實際路徑依發行版與 cgroup driver 設定可能不同,用 systemctl status docker-<id>.scope 或 find /sys/fs/cgroup -name "docker-<id>*" 可以找到正確路徑)。
host 整體記憶體不足時,kernel OOM killer 選擇要殺的 process 是看全系統的 oom_score,不一定會選中目標 container 裡的 process——也可能殺到別的 container、甚至 containerd 的子程序,連帶讓你要查的 container 跟著停擺,但它自己的OOMKilled欄位仍是false。這種情況要搭配free -h跟docker stats看 host 整體記憶體壓力,不能只盯著單一 container 的旗標。
參考:Linux Kernel Docs - Control Group v2
(四)設定 memory limit,避免無預警 OOMKilled
確認問題根源是記憶體不足之後,下一步是給 container 設定合理的 memory limit,並且長期觀察用量曲線。docker run 範例:
docker run -d \
--name myapp \
--memory="512m" \
--memory-swap="512m" \
myapp:1.0
--memory-swap 設成跟 --memory 相同的值,代表完全不給 swap,記憶體用滿就直接觸發 OOM,而不是讓 process 掉進 swap thrashing 導致效能雪崩(這對延遲敏感的服務通常是比較好的選擇)。
用 Docker Compose 的話:
services:
myapp:
image: myapp:1.0
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256M
limits.memory 是硬上限,超過就會被 OOM killer 終止;reservations.memory 是排程時的軟性保留量,用在 Swarm 模式下決定排程位置,單機 docker-compose up 不會強制生效。
設好之後要持續驗證,不是設完就結束:
docker stats --no-stream myapp
看 MEM USAGE / LIMIT 的比例,如果長期貼近 100%,代表 limit 抓得太緊,一次流量洪峰或 GC 行為就可能瞬間觸發 OOM,建議抓在觀察到的尖峰用量之上留 20~30% 緩衝。
(五)讓應用程式正確處理 SIGTERM,避免被誤判成異常
docker stop 的流程是:先對 container 裡 PID 1 送 SIGTERM,等待 grace period(預設 10 秒,可用 docker stop --time <秒數> 或 Compose 的 stop_grace_period 調整),逾時還沒結束才補送 SIGKILL。這裡最常見的坑,是 Dockerfile 的 CMD/ENTRYPOINT 寫成 shell form,導致 SIGTERM 根本傳不到應用程式:
CMD myapp --config /etc/myapp.conf
這種寫法等同於 CMD ["/bin/sh", "-c", "myapp --config /etc/myapp.conf"],真正的 PID 1 是 /bin/sh,不是 myapp。docker stop 送出的 SIGTERM 會先被 shell 收到,而大多數 shell 預設不會把訊號轉發給它啟動的子程序,結果就是 shell 本身沒反應、應用程式也沒收到訊號,等 grace period 一到,Docker 只好對整個 process tree 補送 SIGKILL——這時候看到的 137,其實跟記憶體一點關係都沒有。
修正方式是改用 exec form:
CMD ["myapp", "--config", "/etc/myapp.conf"]
exec form 不會另外啟動 shell,myapp 直接成為 PID 1,可以正確收到 SIGTERM。接下來應用程式自己也要處理這個訊號,以 Node.js 為例:
process.on('SIGTERM', () => {
console.log('Received SIGTERM, shutting down gracefully...');
server.close(() => {
process.exit(0);
});
});
Python(搭配 signal 模組):
import signal
import sys
def handle_sigterm(signum, frame):
print("Received SIGTERM, shutting down gracefully...")
# 在這裡關閉連線池、完成進行中的請求
sys.exit(0)
signal.signal(signal.SIGTERM, handle_sigterm)
如果應用程式需要比預設 10 秒更長的時間完成收尾(例如要等進行中的請求跑完),記得同步調高 grace period:
docker stop --time 30 myapp
或在 Compose 裡:
services:
myapp:
stop_grace_period: 30s
改完之後,手動驗證一次:
docker stop myapp && docker inspect myapp --format 'ExitCode={{.State.ExitCode}}'
正常情況下應用程式會在收到 SIGTERM 後主動以預期的碼結束(常見是 0),而不是等到逾時被 SIGKILL 拿到 137。
結論
遇到 container 異常退出,照這個順序排查:
- 先用
docker inspect看ExitCode與OOMKilled,不要只看docker ps -a的簡化狀態 - 137 不等於 OOM,143 也不等於「一定是自己手動 stop」,一律交叉比對
OOMKilled欄位 - 懷疑 OOM 時,用
dmesg -T或journalctl -k確認 kernel 層級的紀錄,注意 host 層級 OOM 不一定會反映在單一 container 的旗標上 - 設定合理的 memory limit,用
docker stats持續觀察用量曲線,不要設完就不管 - 檢查
CMD/ENTRYPOINT是不是 exec form,應用程式是否正確處理 SIGTERM,避免正常的停機流程被誤判成異常
延伸閱讀:Tips on Dockerfile
常見問題 FAQ
docker inspect 顯示 OOMKilled: false,但 container 還是莫名其妙掛了,是什麼原因?
OOMKilled: false 只代表這個 container 自己的 cgroup 沒有觸發 OOM killer,不代表跟記憶體完全無關。host 整體記憶體不足時,kernel 的 OOM killer 可能是針對其他 process(甚至是 containerd/dockerd 本身的子 process)動手,連帶讓這個 container 被終止,這種情況 OOMKilled 欄位不會是 true。遇到這種狀況要改查 host 層級的 dmesg -T 或 journalctl -k,確認事發當下 kernel 是否有 OOM killer 的紀錄。
Exit Code 137 一定是記憶體不足(OOM)造成的嗎?
不一定。137 代表 process 是被訊號 9(SIGKILL)終止,OOM killer 會送 SIGKILL,但 docker kill(預設訊號也是 SIGKILL)、或 docker stop 在 grace period 內沒收到正常結束而被強制終止,同樣會得到 137。要確認是不是 OOM,必須同時看 docker inspect 的 OOMKilled 欄位,true 才是真正的記憶體問題。
設定了 --memory limit,為什麼 container 還是無預警被 kill?
常見原因是 limit 設得太接近平常的尖峰使用量,一次流量洪峰或 GC 行為就會瞬間超過上限。建議搭配 docker stats 長期觀察記憶體使用曲線,limit 抓在尖峰值之上留足夠的緩衝(建議 20~30% 以上),並視應用特性評估是否需要調整 --memory-swap 或改用 memory reservation 當作軟性門檻。
Exit Code 143 跟我自己呼叫 docker stop 有關係嗎,要怎麼分辨?
143 代表 process 收到 SIGTERM(訊號 15)後結束,這是 docker stop、Kubernetes Pod 終止、或編排系統滾動更新的正常流程。要分辨是不是自己手動操作,可以搭配 docker events 或平台的操作紀錄(CI/CD pipeline log、K8s event)比對時間點;如果找不到對應的人為操作,很可能是編排系統自動觸發的健康檢查失敗或滾動部署。
本文含聯盟連結。As an Amazon Associate I earn from qualifying purchases.
留言
張貼留言