症狀
跑 make viz(前景 docker run -it … node services/viz.js)後按 Ctrl-C,取消不了。之後再跑一次 make viz 就撞 port 17090——原來上一個容器一直沒停,還佔著。
兩個症狀其實是同一個根因。
原因
容器裡直接 node xxx.js,node 就是 PID 1。實測(node:24.21.0-alpine):/proc/1/comm 是 node 本身(顯示 MainThread);送 SIGINT 後 2 秒仍在跑;docker stop 送 SIGTERM 也一樣沒反應,最後花了 10 秒才結束。
為什麼 PID 1 會這樣,官方文件講得很清楚:
- Linux 的 kill(2):能送給 PID 1(init)的訊號,只有 init 自己明確裝了 handler 的那些,目的是避免系統被意外弄掛。容器有自己的 PID namespace,pid_namespaces(7) 對 namespace 裡的 init 也是同樣規則(從祖先 namespace 送來的 SIGKILL/SIGSTOP 例外,會強制送達)
- Docker 的 docker run 文件直接寫:在容器裡當 PID 1 的程序會被 Linux 特別對待,預設動作的訊號一律忽略,所以除非程式自己寫了處理,否則不會因 SIGINT 或 SIGTERM 結束
- Node.js 官方 Docker image 的 Best Practices:Node.js 不是設計來當 PID 1 的,建議用
--init包一層 - 那 10 秒是
docker stop的預設寬限:docker stop 文件寫 Linux 容器預設 10 秒(Windows 容器 30 秒),時間到就改送 SIGKILL。我實測的 10 秒剛好對上
修法
兩層一起做:
# 第一層:讓 docker-init 當 PID 1,負責把訊號轉給 node
NODE := docker run --rm --init -v "$(CURDIR)":/app -w /app $(NODE_IMG)# compose 同理
x-node: &node
init: true// 第二層:服務自己處理結束訊號(少了 --init 也停得掉)
export function closeOnSignal(server, name) {
for (const signal of ['SIGINT', 'SIGTERM']) {
process.once(signal, () => {
server.close(() => process.exit(0));
server.closeAllConnections?.(); // 瀏覽器的 keep-alive 連線不會自己斷,不關的話 server.close 會一直等
});
}
}實測結果:加 --init 後 PID 1 是 docker-init,SIGINT 後立即結束;只加第二層(不加 --init),SIGINT 後 1 秒內結束——第二層單獨也有效。
另外把會「留著不走」的前景指令改成背景執行+明確的停止指令(make viz/make viz-stop),並在已經在跑時直接告訴使用者網址,而不是再起一個去撞 port。
為什麼這樣解
--init讓一個很小的 init 程式當 PID 1,它會把訊號轉給 node,node 就照一般程序的預設行為結束。Docker 預設用的docker-init背後是 tini(見 docker run 文件的 Specify an init process 段)- 只靠
--init不夠保險:哪天有人改了 Makefile、或換成別的啟動方式就又壞了。服務自己處理訊號,才是跟啟動方式無關的修法——這也是 graceful shutdown(收到結束訊號先把手上的事收尾再退出)本來就該做的 closeAllConnections()那行是實作時多踩的一步:瀏覽器開著頁面時會保持 keep-alive 連線,只呼叫server.close()會等那些連線自己斷,等於又停不掉
怎麼預防再次發生
- 新建任何跑 node 的容器(Dockerfile、compose、Makefile 的 docker run):預設加
--init/init: true,服務本身處理 SIGTERM - 驗證方法:起容器後
docker exec <c> cat /proc/1/comm看 PID 1 是誰;time docker stop <c>超過 1–2 秒就是訊號沒被處理 - 症狀辨識:「Ctrl-C 沒反應」「docker stop 很慢」「再跑一次就撞 port」「容器越堆越多」——這幾個常常是同一件事:訊號沒送到真正在跑的那個程序,殘骸就留下來
相關主題
- 啟動點與 Graceful Shutdown:應用層怎麼處理 SIGTERM/SIGINT
- 同一族的另一個坑:宿主機的 timeout 送訊號給
docker exec的用戶端,容器裡的程序收不到,殘骸一組一組堆起來