一個容易踩雷的事實#
很多開發者預設 /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 -> dashUbuntu 從 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 codedash 的限制#
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 | 支援 array | trap+wait 可靠 | 啟動速度 |
|---|---|---|---|---|
| bash | RHEL、CentOS | 是 | 是 | 慢 |
| dash | Ubuntu、Debian | 否 | 否 | 快 |
| busybox ash | Alpine | 否 | 視版本 | 快 |
延伸閱讀#
- DashAsBinSh:https://wiki.ubuntu.com/DashAsBinSh ↗
- Tini:https://github.com/krallin/tini ↗
- Docker docs:
docker run --init