一個容易踩雷的事實#

很多開發者預設 /bin/sh 就是 bash。在 Ubuntu / Debian 上不是。

  • Ubuntu 預設 /bin/sh 是 dash 的 symlink
  • Alpine 預設 /bin/sh 是 busybox ash
  • CentOS / RHEL 上 /bin/sh 是 bash
  • macOS 上 /bin/sh 是 bash 的相容模式(最新版改用 zsh,但 sh 仍是 bash)
ls -l /bin/sh
# Ubuntu: /bin/sh -> dash
# Alpine: /bin/sh -> /bin/busybox
# Debian: /bin/sh -> dash

Ubuntu 從 6.10 起就把 /bin/sh 改為 dash,原因是 dash 啟動快、相依少,適合系統 boot script。

dash、bash、ash 的差異#

三者都實作 POSIX shell,但擴充功能差很多。

  • bash:GNU bash,功能最豐富,支援 array、process substitution、[[ ]]source
  • dash:Debian Almquist Shell,POSIX 嚴格實作,幾乎沒有擴充
  • busybox ash:BusyBox 內建的 Almquist 變種,輕量;功能介於兩者之間

寫 entrypoint script 最容易踩到的差異:

  • [[ ]] 與正則 =~:bash 有,dash 沒有
  • Array:bash 有,dash 沒有
  • source 命令:bash 有,dash 只有 .
  • local 變數:bash、dash 都支援,但行為細節不同
  • echo -e:bash 預設啟用,dash 預設不啟用

在 container 寫 shell 腳本時,shebang 不要寫 #!/bin/sh 然後用 bash 語法。要用 bash 就寫 #!/usr/bin/env bash,或在 image 裡確認 bash 存在。

Signal 處理的差異#

不同 shell 對 signal 的處理也不一樣,這直接影響 container shutdown。

bash 的 trap + wait#

bash 的 wait 在收到有 trap 的 signal 時,會中斷返回,讓 trap handler 跑。這個 pattern 可靠:

#!/bin/bash
term() { kill -TERM "$child"; }
trap term TERM INT

./app &
child=$!
wait "$child"          # SIGTERM 來時被中斷
wait "$child"          # 第二次 wait 拿正確 exit code

dash 的限制#

dash 的 wait 在某些版本不會被 signal 中斷以執行 trap,或 trap 會在 wait 結束後才跑。這意味著:

  • 在 dash 寫 trap + wait 的 entrypoint,SIGTERM 可能要等 child 自然結束後才會處理
  • Container shutdown 不再是即時的

busybox ash#

ash 的行為介於兩者之間,依 BusyBox 版本而異。實務上 alpine 寫 entrypoint 也建議直接 exec,不要依賴 trap + wait。

解法一:exec 替換#

最穩定的做法是讓 shell 退場,用 exec 把自己替換成目標應用。

#!/bin/sh
set -e
echo "config = $APP_CFG"
exec /usr/local/bin/myapp "$@"
  • exec 後 shell 不再存在
  • Container PID 1 直接是 myapp
  • SIGTERM 走 kernel 直送 myapp,不經過 shell

解法二:docker run –init#

如果無法改 image(例如使用第三方 image),可以在 run 時加 --init,docker 會自動把 tini 注入當 PID 1。

docker run --init my-image
  • Docker daemon 內建 tini
  • PID 1 變成 tini
  • Tini 負責 reap zombie 與 forward signal
  • 應用變成 PID 2,但 SIGTERM 會被 tini 正確轉發

Kubernetes 沒有對應的 --init 旗標,要自己在 image 裡加 tini 或 dumb-init。

解法三:在 image 裡裝 tini#

FROM ubuntu:22.04
RUN apt-get update && apt-get install -y tini && rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["python", "app.py"]

或從 GitHub release 下載靜態 tini:

FROM alpine:3.19
ADD https://github.com/krallin/tini/releases/download/v0.19.0/tini-static /tini
RUN chmod +x /tini
ENTRYPOINT ["/tini", "--"]
CMD ["python", "app.py"]

這樣 PID 1 是 tini,python 是 PID 2。Tini 會 forward SIGTERM 給 python,並 reap 任何孤兒。

一張對照表#

Shell/bin/sh 在哪些 distro支援 arraytrap+wait 可靠啟動速度
bashRHEL、CentOS
dashUbuntu、Debian
busybox ashAlpine視版本

延伸閱讀#