Docker Compose 這幾年慢慢長成了一個獨立的服務框架。它有自己的 restart policy、自己的網路模型、自己的健康檢查、自己的日誌驅動——這些在 Linux 上本來都已經有一套成熟解答,只是被 Docker daemon 全部繞開重做一次。Docker 29 把最低 API 抬到 1.44 讓 Watchtower 之類的工具集體失聯之後,這個「容器世界跟 Linux 服務世界不共用同一套基礎設施」的裂縫變得特別明顯。Podman Quadlet 走的是相反方向:把容器塞回 systemd 的依賴圖,讓 .container 檔跟 .service 檔一起被 systemd 認得,日誌走 journalctl,開機順序走 After=,自動更新走 podman auto-update.timer。
Quadlet 在 Podman 4.4 就併進主線,到 Podman 5 之後幾乎所有 compose 常見場景都能對應寫出來。它不是 compose 的替代品,而是把「宣告一組容器」這件事重新掛到 Linux 本來就有的服務管理器上——結果是配置檔還是宣告式的,但底層跑起來的是原生 systemd unit,沒有第二個 daemon 在後面盯著容器有沒有活著。
Docker Compose 的裂縫從哪裡開始
Compose 檔案裡寫 restart: unless-stopped 看起來很直覺,但這條規則是 Docker daemon 在跑,不是 systemd。整臺機器重開之後容器起不起得來取決於 Docker daemon 有沒有正常啟動、docker socket 有沒有被別的服務搶先,以及 daemon 本身有沒有及時完成 recovery。這條路徑上任何一環卡住,容器就跟著不見。
日誌走 json-file 或 local driver 是另一個問題。整套 Linux 生態的日誌工具鏈——journalctl、logrotate、systemd-journal-gatewayd、遠端 forward——全部繞不進去,非得靠 Docker 自家的 log driver 或者外掛一支 Fluent Bit 之類的收集器。生產環境要把容器日誌塞回集中式日誌平臺,等於要多養一組管線。
還有依賴管理。Compose 的 depends_on 只能保證啟動順序,沒辦法表達「等 Postgres 的 systemd unit 完全 ready 之後才啟動這個容器」。systemd 有 Requires=、After=、Wants= 這一整套依賴語意,Compose 全部沒有——它自己就是一個孤島,跟宿主機的其他服務誰也不認識誰。
Quadlet 是 systemd generator,不是新工具
理解 Quadlet 最關鍵的一點:它本身不是一支常駐程式,也不是新的容器 runtime。它是一支 systemd generator,被放在 /usr/lib/systemd/system-generators/podman-system-generator。systemd 每次 daemon-reload 或開機時會執行這支 generator,讀取 /etc/containers/systemd/ 或 ~/.config/containers/systemd/ 下的 .container、.pod、.network、.volume、.kube、.build、.image、.artifact 檔,即時展開成標準的 .service unit 檔。
寫出來的 unit 檔完全就是 systemd 眼中的普通服務,systemctl start、systemctl status、systemctl restart、journalctl -u 這一整套指令直接可用。沒有第二個管理平面,也不需要 docker compose 那種需要記住工作目錄跟專案名稱的心智模型。
一個最基本的 .container 檔長這樣:
1 | # /etc/containers/systemd/caddy.container |
存檔後 systemctl daemon-reload && systemctl start caddy.service 就上線。[Unit]、[Service]、[Install] 是標準 systemd 段落,[Container] 是 Quadlet 專屬——它會在展開時被翻成一段 ExecStart=/usr/bin/podman run ...。
自動更新不用再靠 Watchtower
AutoUpdate=registry 這一行是 Quadlet 跟 compose 差距最直接的地方。啟用之後,systemd 有一支預先安裝的 podman-auto-update.timer——預設每天跑一次 podman auto-update,發現 registry 上的 image digest 變了就 pull 新版、重啟對應的 service。整條路徑上沒有任何第三方元件,Podman 5 之後這個 timer 還會做健康檢查失敗自動 rollback。
這件事對長期困擾 self-host 圈的「容器版本追不動」有直接的效益。Watchtower 這兩年沒動、Dockge 拉不動新版容器的討論之所以會浮出來,本質上是因為容器自動更新這件事一直被外掛在容器 runtime 之外。Quadlet 把它做在 Podman 自己的工具鏈裡,加上一個 systemd timer 就搞定。
自動更新不是全部場景都適合。資料庫、有狀態服務、需要嚴格版本鎖定的核心元件不應該打開 AutoUpdate=registry。實務做法是把邊緣服務(反向代理、CDN、輔助工具)設為自動更新,核心服務走人工手動 podman pull + systemctl restart 的模式。
多容器組合:.pod、.network、.volume
Compose 檔案最方便的地方是把多個容器、共享網路、共享 volume 寫在同一份 YAML 裡。Quadlet 把這件事拆成四種檔案,分開描述,但透過 unit 相互引用組合起來。
一個 WordPress + MariaDB 的組合大致長這樣:
1 | # wordpress.network |
1 | # wordpress-db.volume |
1 | # mariadb.container |
1 | # wordpress.container |
檔名裡的 .network、.volume、.container 都會被 generator 認得,展開時自動處理相互依賴。Requires=mariadb.service 用的是 systemd 原生依賴語法——這比 compose 的 depends_on 表達力強得多,還能寫成 BindsTo=、PartOf= 等更精細的關係。
值得注意的是共享 volume 的路徑寫法:Volume=wordpress-db.volume:/var/lib/mysql 會被解成一個由 Podman 管理的 named volume。如果要用宿主機路徑,寫 Volume=/opt/wordpress/db:/var/lib/mysql:Z——注意 SELinux 系統上一定要加 :Z 或 :z,這是 Podman 相較 Docker 更嚴格的地方。
.kube 檔案讓 Quadlet 直接吃 Kubernetes YAML
Quadlet 額外提供一種 .kube 檔,指向一份 Kubernetes 的 Pod/Deployment YAML,Podman 會用 podman kube play 執行。這個路徑對於已經在寫 Kubernetes manifest 的團隊很有用——同一份 YAML 可以在 Kubernetes 上跑,也可以在單機 Podman 上跑,開發環境跟生產環境用同一套宣告。
1 | # nginx-app.kube |
這條路徑對於已經有 Kubernetes 資產的團隊,是把「小型部署放單機、大型部署放叢集」的一個乾淨切法。單機不需要拉整套 K3s 或 Minikube,直接用 Podman 就能執行同樣的 YAML。
Podlet:把既有 compose 檔翻成 Quadlet
從 Docker Compose 搬到 Quadlet 這件事不用手動一行行改。Podman 官方維護的 podlet 是一支 Rust 寫的 CLI,可以直接吃 compose.yaml 產出對應的 .container、.volume、.network 檔:
1 | podlet compose --absolute-host-paths /opt/app compose.yaml |
--absolute-host-paths 這個參數是實務上一定要加的——compose 檔案常常寫 ./config:/etc/config 這種相對路徑,但 Quadlet generator 執行時的工作目錄不會跟 compose 檔一樣,所以相對路徑會解析失敗。這個坑很多人第一次搬遷都會踩到。
Podlet 沒辦法翻譯的部分包括 healthcheck 的複雜條件、build stage、compose 的 secret 機制、deploy.replicas、IPAM 設定、${VAR} 環境變數插補——這些需要人工對照 Quadlet 文件補回去。多容器共享網路的 .network 檔通常也需要人工調整命名跟連線方式。
實務建議是先跑一次 podlet compose 得到雛型,把生成的檔案放進 /etc/containers/systemd/ 之後 systemctl daemon-reload,用 systemctl list-unit-files | grep container 確認 unit 有被展開,再逐一啟動測試。第一次的容錯率不會太高,但一旦架構跑起來,後續改動比改 compose 檔更接近改 systemd unit——對熟悉 Linux 服務管理的維運者來說負擔更輕。
哪些場景還是 Compose 比較順
Quadlet 不適合所有情境。開發環境需要頻繁 docker compose up/down 快速拉起整組服務、跟 IDE 深度整合、需要跨 macOS/Windows/Linux 一致行為的場景,Compose 加 Docker Desktop 或 Podman Desktop 還是比較舒服。Quadlet 是為單臺 Linux 生產伺服器上長期跑服務設計的,重心在整合而不是快速迭代。
多節點編排也不是 Quadlet 的目標。有橫向擴展、跨機器排程需求時該直接上 Kubernetes 或 Nomad,Quadlet 提供的是「單機、宣告式、跟 systemd 深度整合」這個垂直切面。
需要動態產生容器(例如 CI runner 每次 job 拉一個新容器、或者 GitLab CI 的 Docker executor)也不適合,Quadlet 的檔案是靜態的、需要 daemon-reload 才會生效。這種場景直接呼叫 podman run 或者用 Podman 的 REST API 更合理。
為什麼要把容器交還 systemd
自架服務跑久了會發現:整臺機器上的服務越來越多,一部分是原生 systemd 管的 nginx、postfix、cron,一部分是 Docker Compose 管的容器,兩邊管理方式不同、日誌位置不同、開機順序需要各自檢查。當機器規模到幾十臺、幾百個服務的時候,這種分裂會直接影響維運品質。
Quadlet 的價值不在於它有什麼 compose 沒有的功能,而在於它把「容器」這件事收回到 Linux 服務框架該有的位置。同一套 systemctl、同一份 journalctl 日誌、同一個 systemd-analyze critical-chain 分析依賴——維運者需要學的東西少一層,除錯的路徑短一段。對於長期經營自架服務的團隊,這個「回歸基礎設施」的方向比追新工具實在得多。
想在臺灣本地跑 Podman Quadlet 部署自架服務、或需要一臺穩定的 Linux VPS 練手 systemd + 容器整合,NCSE Network 提供搭載 Intel Xeon Gold CPU 與 NVMe SSD 的 VPS 主機,是方電訊機房直連,適合長期跑生產級容器服務。前往 ncse.tw 了解方案細節。