Docker Compose 開機自動啟動設定教學
了解 Docker Compose 服務如何在重開機後恢復,掌握 restart policy 差異、為何 on-failure 無法持續,以及何時應改用 systemd unit。
簡短答案
Docker Compose 服務會在開機時啟動,前提是同時符合兩個條件。Docker daemon 必須啟用為 system service,且檔案中的每項服務都必須設定 unless-stopped 或 always 的 restart policy。將 restart: unless-stopped 加入每項服務,執行一次 docker compose up -d,容器便會在重新開機後自動恢復運作。一般情況不需要其他設定。
只有在啟動順序很重要時,才需要 systemd unit。例如,stack 相依於已掛載的磁碟、VPN 介面,或 Docker daemon 啟動時尚未就緒的 network share。這種情況確實存在,本指南的後半部會說明。如果你還不熟悉 service definition 和 volume,請先閱讀 VPS 上的 Docker Compose 基礎,再回來繼續。
在 compose.yaml 中設定重新啟動原則
每個服務各自設定一行原則。沒有全域切換設定,因此遺漏設定的服務會在重新啟動後維持停止狀態,而堆疊中的其他服務則會啟動。
services:
app:
image: nginx:1.27
restart: unless-stopped
ports:
- "8080:80"
db:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_PASSWORD: change-me
volumes:
- dbdata:/var/lib/postgresql/data
volumes:
dbdata:套用設定後,從執行中的容器讀回該原則:
docker compose up -d
docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' $(docker compose ps -q app)這會輸出 unless-stopped。如果輸出 no,表示檔案已編輯,但容器從未重新建立。
這是最常見的失敗原因。重新啟動原則儲存在容器中,而不是 YAML 檔案中。編輯 compose.yaml 不會變更已存在的容器。docker compose restart 也沒有幫助,因為它只會停止並啟動相同的容器物件,不會變更其設定。只有 docker compose up -d 會比較檔案與執行中的容器、發現原則已變更,並重新建立容器。
如果目前不想重新建立某個容器,可以直接變更其原則:
docker update --restart unless-stopped my-container仍然要同時編輯 YAML 檔案。docker update 會變更執行中的容器,而下一次執行 docker compose up -d 時,會讀取檔案並還原舊值。
每個 restart 值的實際作用
Docker 定義了 4 個值。這些值的差異只會在機器重新開機或 daemon 重新啟動時顯現。
no是預設值。在任何情況下,都不會自動重新啟動容器。always會在容器停止時重新啟動容器。如果您手動停止容器,Docker daemon 下次啟動時,容器仍會重新啟動。這通常令人意外:您上週刻意停止的容器,重新開機後又會執行。unless-stopped的行為類似always,但手動停止的容器在 daemon 重新啟動後仍會保持停止。對於偶爾需要停止以進行維護的服務,應使用此值。on-failure只有在容器以非零結束碼結束時,才會重新啟動容器。您可以限制嘗試次數,如restart: on-failure:3所示。
如果 stack 只要在伺服器運作時保持啟動,unless-stopped 是適合的預設值。只有在希望容器不易維持停止狀態時,才選擇 always。
為何重新啟動:on-failure 無法在重新開機後持續生效
許多人因為 on-failure 聽起來較為謹慎而選用它,卻發現第一次重新開機後所有容器都已停止。原因在於其定義。on-failure 只會對一件事做出反應:容器程序以錯誤碼結束。
重新開機不是錯誤。主機關機時,systemd 會停止 docker.service,而該 daemon 會刻意停止每個容器。容器並未失敗,因此該原則沒有需要回應的事件。系統重新啟動時,daemon 會檢查需要恢復的容器,而已正常停止的 on-failure 容器不在其中。它會維持在 exited 狀態。
您可以直接驗證。將 restart: on-failure 設定在服務上,執行 docker compose up -d,重新開機,然後執行:
docker compose ps -a服務會以 Exited 狀態列出,並顯示類似 Exited (0) 2 minutes ago 的狀態。沒有任何項目損壞,也不會將任何內容記錄為錯誤,因此很難診斷。該原則確實依照其定義運作。
on-failure 仍然相當實用。它適合執行工作且可能發生當機的容器;您可以限制重試次數,避免形成重新啟動迴圈。若要讓長時間執行的服務在重新開機後持續運作,則不應使用此工具。
重新啟動原則只有在 Docker 服務於開機時啟動才會生效
重新啟動原則由 Docker daemon 強制執行。如果 daemon 未啟動,就不會執行任何重新啟動原則。請檢查:
systemctl is-enabled docker
systemctl is-enabled containerd兩者都應輸出 enabled。Docker 官方套件庫中的套件會在安裝時啟用這些服務,因此在全新的伺服器上通常會通過此檢查。如果任一項輸出 disabled,請修正:
sudo systemctl enable --now docker containerd這裡有一個值得理解的陷阱。Ubuntu 也隨附 docker.socket,當某個程式首次與 Docker API 通訊時,它會按需啟動 daemon。使用者看到 docker.socket 已啟用,便以為 daemon 已受到管理,並停用 docker.service 以節省記憶體。開機時沒有程式呼叫 API,因此 socket 從未被存取,daemon 也不會啟動;直到您輸入第一個 docker 命令前,不會有任何容器啟動。Socket 啟用不能取代啟用 docker.service。
systemd unit 是更適合的解決方案
重新啟動原則無法管理與系統其他部分之間的啟動順序。daemon 啟動後,會儘快啟動您的容器。如果您的堆疊將獨立 volume、NFS(network file system)共用資源或加密磁碟中的目錄繫結掛載至容器,容器可能會在該路徑存在之前啟動。Docker 會直接在掛載點建立空目錄,並讓容器使用該目錄啟動,導致資料庫啟動時沒有資料。
符合以下任一情況時,請撰寫 systemd unit。堆疊需要先等待掛載、VPN 介面或其他 unit 就緒。您希望 systemctl stop myapp 和 systemctl start myapp 的運作方式與主機上的其他服務相同。或者,您希望系統關機時能正常停止堆疊,而不是讓堆疊與 daemon 一同被終止。如果您不熟悉 systemd unit,撰寫 systemd service 和 timer 會更詳細介紹檔案格式。
撰寫 systemd 單元
將堆疊放在固定路徑,且不要放在家目錄中。/srv/myapp 是適合的選擇,因為在任何人登入前執行的單元,不需要讀取 /home。
建立 /etc/systemd/system/myapp.service:
[Unit]
Description=myapp docker compose stack
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target
RequiresMountsFor=/srv/myapp/data
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/srv/myapp
ExecStart=/usr/bin/docker compose up -d --remove-orphans
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=0
[Install]
WantedBy=multi-user.target啟用並啟動它:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
systemctl status myapp.service正常運作的單元會顯示 Active: active (exited)。第一次看到時,這看起來可能不正確。但這是正確的:Type=oneshot 搭配 RemainAfterExit=yes 表示單元已執行其命令,命令也已完成,而 systemd 會繼續將單元標示為啟用中,讓 ExecStop 在關機時執行。
每一行都有其用途。Requires=docker.service 表示單元會快速失敗,而不是在已失效的 socket 上執行 docker compose。After= 設定執行順序,因為單獨使用 Requires= 並不會設定順序。RequiresMountsFor= 會讓 systemd 載入該路徑的掛載單元並等待它完成;這正是使用單元而非重新啟動原則的完整原因。TimeoutStartSec=0 可避免 systemd 在仍在擷取大型映像檔時終止啟動工作。
補充說明如何結合這兩種機制。Docker 的文件建議不要將重新啟動原則與主機程序管理員混用。這項警告針對的是會監督容器程序本身,並在 daemon 嘗試執行相同工作時重新啟動容器的程序管理員。Type=oneshot 單元不會監督任何程序,因此在 compose 檔案中保留 restart: unless-stopped,並與此單元搭配使用是沒有問題的,而且這正是所需的設定。systemd 負責開機時的執行順序,daemon 則負責處理凌晨三點發生當機的容器。
使用實際重新開機進行驗證
實際測試無可替代。systemctl restart docker 不會測試掛載順序,而 docker compose down 接著 docker compose up -d 完全不會測試開機流程。
sudo reboot請依照以下順序等待、重新連線並檢查:
uptime
systemctl is-active docker
docker compose psuptime 可確認目前查看的是確實重新開機過的機器。從堆疊目錄執行 docker compose ps,應列出每個服務,狀態均為 running,且運作時間應接近機器的運作時間。顯示為 Exited 的服務就是應檢查的對象。
如果有項目未成功啟動,daemon log 會涵蓋開機期間:
journalctl -u docker.service -b --no-pager | tail -50對於由 unit 管理的堆疊,journalctl -u myapp.service -b --no-pager 會顯示開機時的完整 docker compose 輸出,包括映像檔提取失敗或缺少 .env 檔案的訊息。
會悄悄導致自動啟動失效的因素
使用 docker compose run 建立的容器不會套用檔案中的重新啟動原則。Compose 會將它們視為一次性容器。如果服務似乎忽略其原則,請檢查它是否使用 run 啟動,而不是 up。
磁碟區中的相對路徑,或 env_file 項目中的相對路徑,會以 compose 檔案所在的目錄為基準解析。這在 shell 中執行時有效;對設定 WorkingDirectory 的 unit 也有效。對未設定 WorkingDirectory 的 unit 則會失效,因為此時工作目錄是 /。
Rootless Docker 是另一種情況。daemon 會以使用者服務的形式執行,而使用者服務會在該使用者的最後一個工作階段結束時停止。請為該使用者啟用服務,並允許服務在沒有使用者登入時繼續執行:
systemctl --user enable docker
sudo loginctl enable-linger $USER若沒有 enable-linger,rootless daemon 會在您登出時關閉,容器也會隨之停止。這看起來就完全像是重新啟動原則失效。
最後還有一點。自動安全性更新可能會在固定時間重新啟動伺服器;只有在您的堆疊能自行恢復時,這才是好事。在新機器上完成這項設定,應與 新 VPS 的前十分鐘 中的其他初始工作一併處理。
FAQ
restart: always 與 restart: unless-stopped 有何差異?
兩者都會在容器自行停止時重新啟動容器。差異在於您手動停止容器之後的行為。使用 always 時,下一次 Docker daemon 啟動時,容器會再次啟動,因此重新開機會取消您的手動停止操作。使用 unless-stopped 時,daemon 會記住容器是刻意停止的,並讓容器維持停止狀態。除非您明確需要容器持續保持停止,否則請使用 unless-stopped。
我已加入 restart: unless-stopped,但容器在重新開機後仍未啟動。為什麼?
此原則設定在容器上,而不是檔案中。編輯 YAML 不會更新已存在的容器。執行 docker compose up -d,讓 Compose 重新建立容器,然後使用 docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' $(docker compose ps -q app) 確認。如果輸出 no,表示該容器早於您的編輯內容建立。另一個常見原因是未啟用 docker.service;您可以使用 systemctl is-enabled docker 檢查。
如果我已使用 restart policies,還需要 systemd unit 嗎?
通常不需要。對於只依賴網路的 stack,restart policy 已經足夠,而大多數 stack 都屬於此類。當容器依賴 Docker daemon 啟動時尚未就緒的項目時,請加入 unit,例如外部磁碟、加密磁碟區、NFS 共用或 VPN 介面。unit 可透過 After= 和 RequiresMountsFor= 設定啟動順序,而 restart policy 無法表達這類相依性。
如何永久停止 stack,避免它在下一次重新開機時再次啟動?
使用 unless-stopped 時,docker compose stop 已經足夠,因為手動停止的容器不會在 daemon 重新啟動時恢復。使用 always 時,單純停止並不足夠,容器會在重新開機後再次啟動。請執行 docker compose down 移除容器,或先使用 docker update --restart no my-container 變更原則。如果由 systemd unit 管理 stack,也請執行 sudo systemctl disable myapp.service,否則 unit 會再次啟動 stack。