Docker Compose 使用者要在應用程式啟動前跑一次資料庫遷移、把掛載卷的擁有者改成非 root 使用者、或先生成一份設定檔,過去十年幾乎只有一條路可以走——寫一個獨立的 service,用 depends_on 加 condition: service_completed_successfully 綁定它跟主服務的順序。這條路徑能動,但每個用過的人都知道 docker compose ps 會多出一堆已經跑完退出的殭屍條目、重啟服務時得記得手動 --force-recreate、多段串起來的初始化步驟只能靠 depends_on 一路接下去。2026 年 7 月釋出的 Docker Compose v5.3.0 加了 pre_start 生命週期,這件事才正式從 Kubernetes 那邊的 init container 觀念補齊到 Compose 這邊。
service_completed_successfully 從來不是為這個場景設計的
depends_on 加 condition 這個組合是 Compose v2 為了取代舊版 v1 的簡單依序啟動加進來的,本意是描述服務之間的健康狀態依賴。service_healthy 讓後端等資料庫的 healthcheck 通過再啟動,這個場景合理;但把一次性的遷移工作包成一個「service」,只為了跑完退出然後被 service_completed_successfully 觀察到,本質上是在把一次性任務硬套到長期服務的抽象上。
副作用實務上會冒出來的幾個:docker compose ps 每次都會看到 migrate Exit 0 這種殭屍條目佔位;docker compose down 之後再 up,那個 migrate service 會照著 pull image 分配網路建立容器再退出,跑一輪就得等它,即使遷移根本已經做過;改設定檔重啟主服務時,如果沒下 --force-recreate,那個 migrate 不會被重建也不會重跑,變成該跑的時候沒跑;要串連續三個初始化步驟——先建 schema、再灌 seed data、再建索引——就得寫三個 service,兩個 depends_on 鏈,維護起來很難看。
有些團隊為了避免這些副作用,改用 shell script 在主服務啟動前跑遷移,塞進 entrypoint。這條路把應用程式的映像檔跟資料庫工具的責任綁在一起,違背單一容器單一責任的原則;重跑一次遷移還得整個 restart 主服務,殺傷力更大。
pre_start 把 init 從 peer 變成 subordinate
Compose v5.3 的 pre_start 用一個很直接的模型解掉這件事——它是主服務的一個子步驟,不是獨立 service。宣告寫在服務底下:
1 | services: |
三個步驟會依序在獨立的臨時容器裡跑,每一個要 exit 0 才會進到下一個,任何一個非零退出整個服務啟動就中止。三個容器預設繼承 app 的映像檔、進到 app 定義的網路、看得到 depends_on 裡的 db、共用 app 掛載的 volume。跑完之後容器被回收,docker compose ps 只會看到 app 一個條目,看起來就跟一般啟動一樣乾淨。
Compose 也記得每個步驟上次跑的定義跟結果——只要指令沒變、上次成功、沒下 --force-recreate,第二次 docker compose up 就會跳過。這件事對日常維運差別很大:主服務改個環境變數重啟一次,遷移不會重跑;改了遷移指令或加了新步驟才會重跑;上次跑失敗的一定會重跑一次。這套語意跟大多數團隊心裡「什麼時候該跑 migration」的直覺對得起來。
掛載卷擁有者修正這個場景差別最明顯
自架 PostgreSQL、MinIO、Grafana、Prometheus 這些服務在 Docker 底下常見的第一個坑,就是主機上 bind mount 的目錄擁有者是 root,容器裡的服務用 non-root 使用者啟動,直接吃 permission denied。過去的標準做法是寫一個 init service 掛同一個 volume,執行 chown -R 1000:1000 /data 之後退出:
1 | services: |
用 pre_start 改寫是這樣:
1 | services: |
差別在於 init container 現在明確跟 grafana 的 volume 共享——同一份 ./grafana-data:/data 定義只寫一次,chown 完之後 grafana 拿到的就是同一顆卷。不用 depends_on、不用另一個 service 條目、也沒有殭屍容器占位。要在 grafana 底下多加一個「若設定檔不存在就複製一份預設」的步驟,直接在 pre_start 陣列多加一項就好。
生命週期上有幾件事要先想清楚
pre_start 的容器是在服務容器被 create 之後、start 之前執行的。這個順序很關鍵——服務容器已經拿到 IP、掛好 volume,只是 process 還沒起來。init container 跟服務容器共用網路命名空間、共用 volume mount,改動立刻生效。實務上這代表 init container 修改的檔案、寫入的資料,服務容器啟動時就看得到。
但幾件事要注意。Init container 跑一次就是一次,不會為每個 replica 分別跑。用 deploy.replicas: 3 開三個 web 副本,pre_start 只跑一次;服務 scale up 加副本時,pre_start 也不會為新副本再跑。這件事對純粹的 stateless web 服務沒差,但如果 init 做的是每個副本各自需要的初始化——例如寫個 per-replica 的暫存目錄——就不適用。官方文件明確標記 per_replica: true 選項目前還不支援,只能自己想辦法。
另一個是它跟 configs 跟 secrets 的分工。想把一份靜態 config 塞進容器不該用 pre_start 寫 script,Compose 原生的 configs 定義做這件事更乾淨;想動態產生一份取決於環境的 config,pre_start 才有用武之地。同理,寫死的密碼字串塞進 secrets 就好,不用 pre_start 去 curl Vault 拿——那應該是主服務自己 startup 的責任。
還有 post_start 跟 pre_stop 這兩個 hook。它們跟 pre_start 名字很像但機制完全不同——post_start 跟 pre_stop 在既有的服務容器裡執行指令,不會另外開容器;pre_start 是獨立的臨時容器。要在服務啟動後跑一個 warm-up 請求,用 post_start;要在服務關閉前 flush buffer,用 pre_stop;要在服務啟動前跑遷移,用 pre_start。
版本相容跟部署路徑
pre_start 只在 Docker Compose v5.3.0 之後可用。臺灣多數 VPS 上跑的 Debian 12 或 Ubuntu 22.04 預設倉庫還在 Compose v2 舊分支,直接 apt 裝的版本不會有這個欄位。實務上要能用,兩條路:從 Docker 官方 apt 倉庫裝新版 docker-compose-plugin,或者用 Docker Desktop 的內建版本(開發機才適用)。VPS 上比較穩的做法是把 Docker Engine 跟 Compose plugin 都盯著官方 apt 倉庫升級,順帶避開發行版倉庫落後 6 到 12 個月的節奏差。
寫在 compose 檔裡的 pre_start 對舊版是硬性 breaking——舊版 parser 遇到不認識的欄位會 warn 或直接錯誤退出,取決於版本。這件事對團隊裡有人用舊版本的情境要注意,最好把 docker compose version 加進 CI 檢查跟部署 script。
有些人可能會想乾脆等到 Docker Swarm 或 Kubernetes 也支援類似欄位再統一。目前狀況是 Swarm 已經事實停止大規模開發,Kubernetes 的 initContainer 語意跟 Compose 的 pre_start 相近但不完全一樣(K8s 的 initContainer 每個 Pod 各跑一次,Compose 的 pre_start 是 service 層級)。想從 Compose 遷移到 K8s 的團隊,pre_start 概念上是可以直接對應過去的,只是要意識到 replica 語意的差異。
什麼時候該換、什麼時候先不用
該換的情境很直接:docker compose ps 列表被殭屍 init service 塞滿的部署、有多段初始化步驟串成 depends_on 鏈的 compose 檔、chown 或 chmod init 用得很頻繁的服務、資料庫遷移或 seed data 常常忘記 --force-recreate 導致跑掉的環境。這些場景改寫成 pre_start 之後,compose 檔會短很多,維運心智負擔會小很多。
先不用換的情境:Compose 版本鎖在舊版沒辦法升的老專案、init 需要在多副本間各跑一次的少數場景、init 步驟本身複雜到需要獨立日誌跟監控(那應該是個 real service,不是 init)。另外目前 pre_start 的權限跟 volume 定義不能覆寫服務本身的設定——例如想在 init 用 root 跑但主服務用 non-root,或想在 init 掛一顆額外的 volume——這些限制在 GitHub issue #13934 有討論但還沒有解,遇到就得暫時繼續用舊做法。
單機 VPS 上跑 Docker Compose 是很多小型自架服務的預設配置,這種環境每一個 compose 檔的複雜度都會直接反映在維運成本上。pre_start 這個看似小的欄位真正的價值不在於功能新——service_completed_successfully 本來就做得到大部分事——而在於它把「這是一次性的初始化」這個意圖用型別化的語法明確表達出來,docker compose ps 的輸出更乾淨、上線 script 更好寫、六個月後回頭看 compose 檔也讀得懂。
要跑得穩、跑得動,最底層還是要有一臺不掉線的 VPS。NCSE Network 提供臺灣是方電訊機房的 VPS 服務,Intel Gold CPU 加 NVMe SSD,適合跑長時間的 Docker Compose 部署跟自架服務。有興趣可以到 ncse.tw 了解方案細節。