首頁

目前文章總數:203 篇

  

最後更新:2025年 10月 25日

0011. 為什麼 Docker 容器會隨應用程式關閉而停止?PID 1 原理、單一服務原則與避免殭屍容器

日期:2025年 11月 08日

標籤: Docker Docker-Compose Container Ubuntu Linux

摘要:Docker


應用所需:已安裝 Docker
解決問題:說明為何當容器內的應用程式關閉,外部容器也會跟著停止
延伸議題:避免殭屍容器,落實 => 1 個容器只用於 1 個應用
範例檔案:本篇範例代碼
基本介紹:本篇分為 3 部分。
第一部分:問題描述
第二部分:Linux 特性與容器應用的關係
第三部分:標題是正確 - 符合單一職責與最佳實踐






第一部分:問題描述

Step 1:運行範例代碼

下載範例檔案來說明,執行以下在 Ubuntu 主機上

docker-compose up -d




Step 2:檢查容器內的應用程式

我們選擇一個 Container 檢查

docker exec staticweb_ipinfoexample_1 ps aux


可以觀察到 PID = 1 的應用程式是我們 DockerFile 的啟動

CMD ["nginx", "-g", "daemon off;"]




Step 3:問題描述 - 嘗試刪除容器內的應用程式

執行以下刪除容器內的 Process 指令

docker exec staticweb_ipinfoexample_1 kill -15 1


指令拆解說明:

指令拆解 說明
docker exec 在該容器裡執行一個命令
kill 傳送訊號給某個進程。
-15 表示訊號 SIGTERM (訊號中止, signal terminate)
-1 PID 編號 1 ,在容器內該 Process 的 ID 編號為 1


執行後可以觀察到容器也跟著停止 - 為何呢?




第二部分:Linux 特性與容器應用的關係

Step 1:Linux PID 1 Process 與容器停止的關係

在任何 Linux 中,PID = 1 具有特殊性,並且是 init 程序,Linux 內核設計使其特性如下:

1. 系統啟動後的第一個程序
2. 所有其他程序的祖先
3. 負責回收孤兒程序
4. 當 PID 1 程序終止時,內核會發出 SIGKILL 給所有其他程序並關閉系統


關鍵在於 PID 1 結束時,就會關閉系統

Step 2:Linux PID 1 Process 與容器停止的關係 - 可解釋為何同步停止

在 Docker Container 中,只要確保 DockerFile 實現以下,就能讓容器與其內部應用程式其中 1 項停止時,就關閉容器運行

1. 容器啟動時,ENTRYPOINT 或 CMD 指定的程序成為 PID 1
2. 容器擁有自己的 PID namespace,與宿主機隔離(這是必定的,與宿主機做映射)
3. 當容器內的 PID 1 程序結束時,容器就會停止


因為容器的應用程式 PID = 1,因此容器(可以視為容器內 Linux 系統),也就會同步停止運行。

Step 3:Linux PID Namespace 的原理

容器透過 namespace (命名空間)將容器與宿主機內核隔離。
命名空間限制每個程序能看到什麼(如其他程序、用戶、檔案系統),因此用 namespace 隔離後,雖然容器與宿主機都有 PID = 1 但並不會衝突
如下圖,宿主機有自己的 init PID = 1 並且容器在:




Step 4:參考資料

Linux 的核心文件:https://docs.kernel.org/
Docker .net https://docs.docker.net.tw/contribute/guides/



第三部分:標題是正確 - 符合單一職責與最佳實踐

Step 1:最佳實踐 - 容器內只有 1 個應用

Docker 官網的說明,有表述容器最佳的實踐是 1 個容器只用於 1 個應用

It's best practice to separate areas of concern by using one service per container. That service may fork into multiple processes




Step 2:最佳實踐 - 這樣設計的優點

除了符合單一職責外(1 個容器只有 1 個應用,負責 1 件事情),最大的優點是便於監控容器運行狀態與追蹤錯誤

好處 說明
隔離責任 (Separation of Concerns) 每個服務/進程有自己的容器,就不會因為其中一個服務故障或更新造成其他的服務被影響。
擴展性 (Scalability) 當某服務流量變大,可以只擴展那個服務的容器,不需要同時帶動其他不需要擴展的服務。
資源控制與配置清晰 比如記憶體、CPU、環境變數、網路端口等,每個容器比較容易設定與限制。
維護與佈署簡單 更新、回滾、測試會比較簡潔;日誌/監控也比較清晰。
Docker 契合設計哲學 Docker 的許多機制(entrypoint/CMD、資源隔離、life-cycle 管理)本身就是以單一進程為主進行設計的。



Step 3:單一容器,多個應用的影響 - 殭屍容器

殭屍容器:容器還在運行,但內部的應用程式已經無法正常工作
這種現象出現在未落實容器的最佳實踐,或者在容器內的主程式未控管好子程序時發生

以下是潛在造成殭屍容器的 DockerFile 範例,通常出現在 管理多個服務多個 Port應用程式不是 PID=1 都是可能觸發

# ❌ 反面教材:不要這樣做!
# 這個 Dockerfile 演示了容器內運行多個應用的問題

FROM ubuntu:20.04

# 安裝多個服務
RUN apt-get update && apt-get install -y \
    nginx \
    redis-server \
    mysql-server \
    supervisor \
    && rm -rf /var/lib/apt/lists/*

# 創建 supervisor 配置目錄
RUN mkdir -p /var/log/supervisor

# 配置 supervisor 管理多個服務
COPY <<EOF /etc/supervisor/conf.d/supervisord.conf
[supervisord]
nodaemon=true
user=root

[program:nginx]
command=/usr/sbin/nginx -g "daemon off;"
autostart=true
autorestart=true
stderr_logfile=/var/log/supervisor/nginx.err.log
stdout_logfile=/var/log/supervisor/nginx.out.log

[program:redis]
command=/usr/bin/redis-server
autostart=true
autorestart=true
stderr_logfile=/var/log/supervisor/redis.err.log
stdout_logfile=/var/log/supervisor/redis.out.log

[program:mysql]
command=/usr/bin/mysqld_safe
autostart=true
autorestart=true
stderr_logfile=/var/log/supervisor/mysql.err.log
stdout_logfile=/var/log/supervisor/mysql.out.log
EOF

# 創建 nginx 配置
RUN echo "server { listen 80; root /var/www/html; }" > /etc/nginx/sites-available/default

# 創建測試頁面
RUN echo "<h1>Multi-App Container (BAD PRACTICE)</h1>" > /var/www/html/index.html

# 暴露多個端口(也是不好的實踐)
EXPOSE 80 6379 3306

# ❌ 問題:使用 supervisor 作為 PID 1
# 這會導致殭屍容器問題
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]



Step 4:殭屍容器 - 影響

如果照著上面的 DockerFile 執行,如下圖,當容器的 supervisord (PID=1)存活的情況下,但是容器內的 Nginx、Redis、Mysql 都死亡




嚴重的影響:增加排查問題的複雜度,並且會大大增加公司運營成本,原本公司只需要監控容器的存活,延伸變成還需監控到容器內每個服務的運行狀態
未來假如容器內部的服務擴增與減少,維運人員都需要同步改寫監控的腳本,因此 1個容器多個應用程式,盡量禁止