前言
這篇是我在 containerize Go 專案時踩到的一個坑,主要是 prefork
模式無法啟動,最後才發現是因為我直接用 CMD ["./build/app"]
,導致這個程式變成 PID 1。後來試了 sh -c
跟 --pid=host
之後才搞定,順便把這整個過程紀錄一下。
問題起因
一開始我用最常見的寫法:1
CMD ["./build/app"]
這樣在容器裡 build/app
會變成 PID 1,而 Linux 下 PID 1 是非常特別的存在,有一些額外的行為限制。結果導致我使用的 Go prefork(應該是類似 fiber 那種)完全無法正常跑。
解法一:用 shell 包一層
後來試著用這種方式:1
CMD ["sh", "-c", "./build/app"]
這樣的好處是:
- 容器裡 PID 1 是
sh
build/app
變成子程序,可以自由 fork- Go 的 prefork 終於正常執行了
解法二:加上 –pid=host
我也試了下面這個參數:1
docker run --pid=host my-image
這是直接把容器的 PID namespace 切換成 host 的,讓容器的程式不會再被限制只能是自己的 PID 空間。這樣 CMD ["./build/app"]
也可以成功啟動 prefork。
那 PID 1 到底哪裡有問題?
Linux 的 PID 1 有幾個特性:
- Signal 處理特別:PID 1 預設會忽略 SIGTERM / SIGINT,必須自己寫 handler
- Zombie process 不會自動回收:如果有 fork 出子程序,要自己
wait()
,不然會累積成 zombie - 某些 runtime(像 Go)根本沒考慮 PID 1 情境:導致 prefork 無法用
為什麼 –pid=host 也能解?
這是因為 Go 的 prefork 能否正常運作,關鍵在於程式能否 fork 子程序並共用 socket,而不單純是 PID 1 的問題。
當你加上 --pid=host
,雖然容器內的 Go 程式還是 PID 1,但在 host 上看起來不是,因此 prefork 可以正常啟動。
不過這樣做會讓容器看到 host 上所有的 process,帶來安全性疑慮。雖然預設下(非 privileged container)你沒有權限 kill 其他 process,但如果加上 --privileged
或額外 capability,就有可能影響 host 上的服務,因此不建議這樣做。
小結
寫法 | Prefork OK | 安全性 |
---|---|---|
CMD ["./build/app"] | ❌ | ✅ |
CMD ["sh", "-c", "./build/app"] | ✅ | ✅ |
CMD ["./build/app"] + --pid=host | ✅ | ❌ |
簡而言之,Go 程式在 Docker 裡當 PID 1 會讓 prefork 失效,建議用 shell 包一層或用 tini 等 init 解決。