SSD Nodes Learn 🎉 VPS $5.50/月起
指南 Matt Connor作者: Matt Connor · 已更新 2026-08-13

Podman 與 Docker 在 VPS 上有何不同?

Podman 預設採 rootless 且沒有 daemon,會影響 compose、quadlet、自動啟動、低於 1024 的埠,以及 volume 擁有權。

Podman 與 Docker 實際上有哪些差異

Podman 與 Docker 都能在 VPS 上執行相同的 OCI(open container initiative)映像,因此選擇重點不是能執行哪些軟體,而是程序模型。Docker 會執行一個擁有所有容器的 root daemon,而 docker command 是向該 daemon 要求執行工作的輕量型用戶端。Podman 沒有 daemon:podman run 會在目前呼叫它的程序下,以您自己的非特權使用者身分,將容器啟動為子程序。

其他差異都源自這項基本事實。自動啟動會改由 systemd 負責,而不是由 daemon 負責。Volume 的擁有權會經過 user namespace 對映,因此您在主機上使用 ls -l 看到的擁有者,不一定是容器內看到的擁有者。低於 1024 的埠在變更 kernel 設定前會拒絕繫結。docker CLI(command line interface)可透過 wrapper 持續運作,但如果某項功能需要 Docker socket,就會無法繼續。

沒有 daemon:啟動容器時實際執行的程序

在 Docker 主機上,pstree -a 會顯示由 root 執行的 dockerd,旁邊有 containerd,而每個執行中的容器各有一個 containerd-shim-runc-v2。您的應用程式是該 shim 的子程序,而 shim 是 PID 1 的子程序。容器與啟動它的 shell 之間沒有直接連結。停止 daemon 後,主機上所有容器的控制平面都會失效;如果預設的 live-restore 設定已關閉,systemctl restart docker 也會重新啟動您的容器。

Podman 沒有對等的程序。啟動容器後,您會得到一個 conmon(容器監控程序),負責維持容器的主要程序,且由執行該命令的使用者擁有。

podman run -d --name web -p 8080:80 docker.io/library/caddy:2
ps -o user,pid,ppid,args -C conmon
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080

ps 應列出由您的登入使用者而非 root 執行的 conmon,而 curl 應輸出 200。由於沒有集中式服務負責容器,sudo apt upgrade podman 不會停止目前已在執行的程序;某個容器的監控程序當機,也不會連帶影響其他容器。

缺少 daemon 也會帶來代價。系統重新開機後,沒有任何程序會自動啟動您的容器。Docker 的 --restart=always 是由 daemon 在開機時履行的承諾;Podman 則以 systemd 取代這項功能,以下的 quadlet 章節就是為此而設。

socket 是另一個重點。/var/run/docker.sock 是由 root 擁有的 API(應用程式介面)端點,任何能寫入該端點的程序,都可以啟動掛載主機檔案系統的特權容器。將使用者加入 docker 群組,等同於以較迂迴的方式授予該使用者 root 權限;這一點值得與只授予各服務帳號所需的存取權限一併閱讀。除非您明確要求,Podman 不會公開 socket;而取得的 socket 只屬於 /run/user/<uid>/podman/podman.sock 的單一使用者。

在 Ubuntu 24.04 安裝 Podman,並確認 rootless 確實生效

sudo apt update
sudo apt install -y podman uidmap
podman --version
podman info | grep -i rootless

uidmap 套件提供 newuidmapnewgidmap。這些是 setuid 輔助程式,可讓一般使用者取得一段 subordinate ID 範圍。缺少這些輔助程式時,rootless 容器無法啟動。podman info 應輸出 rootless: true

截至 2026 年 8 月的確認結果,Ubuntu 24.04 提供 Podman 4.9,Debian 13 提供 Podman 5.x。版本差異很重要,因為 quadlet 檔案需要 4.4 或更新版本,而 .pod quadlet 檔案需要 5.0。從上游文件複製範例前,先執行 podman --version

每個 rootless 使用者都需要一段 subordinate ID 範圍:

grep "$USER" /etc/subuid /etc/subgid

在 Ubuntu 上,使用 adduser 建立的使用者會自動取得範圍。使用 useradd -M 或設定工具建立的使用者通常不會取得範圍,錯誤訊息會明確指出這一點:

Error: cannot find UID/GID for user deploy: no subuid ranges found for user "deploy" in /etc/subuid - check rootless mode in man pages.

指派範圍後,重設該使用者的儲存區,讓系統套用新的對映:

sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 deploy
podman system migrate

首次執行時還有一個容易忽略的問題:Podman 不會預設使用 Docker Hub。簡短映像檔名稱會根據 /etc/containers/registries.conf 中的 unqualified-search-registries 解析;在沒有連接終端機的 script 中,pull 會以 short-name resolution enforced but cannot prompt without a TTY 失敗。每次都應寫出完整名稱。請使用 docker.io/library/nginx:1.27,不要使用 nginx

無 root 容器在租用伺服器上實際帶來什麼好處

rootless 容器會在 user namespace 中執行。這項 kernel 功能會為程序建立專屬的 user ID 對應表。容器內的 superuser 是 UID (user ID) 0。在 namespace 外,也就是 VPS 上,該程序的身分仍是你的普通登入使用者。容器中的 root 不是主機上的 root。

這就是它實際提供的安全提升幅度。要求以 root 執行的 image、存在遠端程式碼執行漏洞的 Web 應用程式,以及依賴程序在主機外部具備 UID 0 的逃逸攻擊,最後取得的都是未具特權使用者的權限,而不是整台機器的權限。rootless 無法防護 kernel 漏洞,也無法保護你自己的檔案,因為逃逸後的程序會以你的身分執行,能讀取你可以讀取的任何內容。

Docker 也能以 rootless 模式執行。dockerd-rootless-setuptool.sh install 會設定每位使用者專屬的 daemon,運作效果良好。差異在於預設方向不同。使用 Podman 時,預設就是 rootless,不需要另外設定。因此你第一次遇到的問題會是容器無法繫結 port 80,而不是某個服務在 root 身分下悄悄執行了 2 年。

為什麼我的 volume 檔案擁有者是 UID 100999?

原因是同一個 user namespace。Container UID 0 會對應到 host UID。Container UID 1 會對應到 subuid range 中的第一個 ID,之後依序遞增。若 range 從 100000 開始,container UID 1000 在 host 上就會對應到 100999。

mkdir -p "$PWD/data"
podman run --rm --user 1000 -v "$PWD/data:/data" docker.io/library/alpine:3 sh -c 'id -u; touch /data/f'
ls -ln "$PWD/data"

Container 會輸出 1000。Host listing 顯示擁有者為 100999,因為 100000 加上 1000 再減去 1 就是 100999。這不是故障,而單純執行 chown 也無法修正,因為 unprivileged user 完全無法在 namespace 外變更檔案擁有權。

有 4 種處理方式:

  • podman unshare chown 1000:1000 "$PWD/data" 會在相同的 user namespace 內執行 chown,此時數字的意義與 container 內相同。
  • -v "$PWD/data:/data:U" 會要求 Podman 替你修正 source directory 的擁有權。請只對全新的 directory 使用,不要用在重要資料上。
  • --userns=keep-id 會將 host UID 對應到 container 內的相同 UID,讓新建立的檔案由你擁有。
  • 使用 -v appdata:/data 這類 named volume 可避開這個問題,因為 Podman 會在你自己的 storage 內建立 volume,並設定正確的擁有權。

如果你曾在 Docker 中處理過這個問題,這是同一個問題,只是多了一層。許多 image 提供的 PUID 與 PGID 變數會設定 container 內程序使用的 UID,而在 rootless Podman 下,該 UID 還會再對應一次。Rootless container 內的 PUID=1000 仍會寫入由 100999 擁有的 host 檔案。設定數字時,請將第二次對應納入考量;或者將資料移到 named volume,避免繼續處理這個問題。

另外補充 2 點 mount 相關事項。在 Fedora 和 RHEL 範例中看到的 :z:Z flags 是 SELinux relabel 選項;Ubuntu 使用 AppArmor,因此這些選項在 Ubuntu 上不會發揮作用。Rootless Podman 也無法 mount 你的 user 無法讀取的 host directory。這是預期的權限限制,不是故障。

為什麼 rootless Podman 拒絕發布 port 80?

因為繫結 1024 以下的埠需要目前使用者沒有的權限。錯誤訊息會指出修正方式:

Error: rootlessport cannot expose privileged port 80, you can add 'net.ipv4.ip_unprivileged_port_start=80' to /etc/sysctl.conf (currently 1024), or choose a larger port number (>= 1024): listen tcp 0.0.0.0:80: bind: permission denied

有兩種可行作法。降低整台主機的限制:

echo 'net.ipv4.ip_unprivileged_port_start=80' | sudo tee /etc/sysctl.d/99-podman.conf
sudo sysctl --system
sysctl net.ipv4.ip_unprivileged_port_start

最後一個命令應回傳 80。請清楚了解這項設定的作用:現在主機上的每個使用者都能繫結 80 和 443,不只限於執行容器的使用者。在只有單一管理員的 VPS 上,這是可以接受的取捨。在包含其他使用者帳號的主機上則不適用。另一種作法是在 8080 上發布,再在前方放置反向代理;這正是你要讓 certbot 在 nginx 上申請並續期憑證 的架構。

Rootless 發布也會改變應用程式所看到的來源資訊。Podman 4.x 預設使用 slirp4netns 搭配 rootlesskit port handler,轉送的連線會使用重新改寫的來源位址,因此存取日誌會將每位訪客都記錄為 10.0.2.100。Podman 5.0 將預設值改為 pasta,可保留真實的用戶端位址。在 4.x 上,--network slirp4netns:port_handler=slirp4netns 可還原真實來源位址,但會降低部分吞吐量。

這裡有一個值得注意的優點。Rootless 發布的埠是由一般程序擁有的普通 listening socket,因此防火牆的輸入規則會套用至該埠。Docker 會透過寫入 NAT(network address translation)規則及自身的轉送允許規則來發布埠,這正是 已發布的 Docker 埠會忽略你以為能封鎖它的 ufw 規則 的原因。Rootful Podman 使用類似的網路處理方式,也會遇到相同問題。Rootless 不會。

Podman 是否仍能使用我的 Docker Compose 檔案?

大多數情況可以,方式有兩種。第一種是使用 podman-compose。這是獨立的實作,會讀取相同的檔案,並驅動 Podman CLI:

sudo apt install -y podman-compose
podman-compose up -d
podman ps

第二種是讓真正的 Docker Compose 透過每位使用者專用的 socket,連線至 Podman 相容 Docker 的 API:

systemctl --user enable --now podman.socket
export DOCKER_HOST="unix:///run/user/$(id -u)/podman/podman.sock"
docker compose up -d
podman ps

docker compose pspodman ps 應列出相同的容器,因為實際上只有一組容器。名稱解析也能正常運作:Podman 的預設網路後端 netavark 會執行 aardvark-dns,因此使用者定義網路上的容器可以透過名稱找到彼此。

但仍有一些限制。凡是掛載 /var/run/docker.sock 的設定,都必須改為指向 Podman socket,或移除該掛載。network_mode: host 在 user namespace 下的行為不同。不同 podman-compose 版本對 depends_on 搭配 condition: service_healthy 的支援程度不一。restart: always 不會自行在重新開機後恢復,下一節會處理這個問題。Compose 仍適合用單一檔案描述 多容器服務堆疊;在 Podman 下,它則是轉換層。若某個堆疊預計維護多年,請將它轉換為 quadlet,維護單一抽象層,而不是同時維護兩套。

Pods: the idea Docker has no answer for

A pod is a group of containers that share one network namespace. Podman starts a small infra container to hold that namespace open, and the members then reach each other on 127.0.0.1 with no user-defined network and no service discovery involved.

podman pod create --name app -p 8080:80
podman run -d --pod app --name app-cache docker.io/library/redis:7
podman run -d --pod app --name app-web docker.io/library/nginx:1.27
podman pod ps
podman ps --pod

podman pod ps should show the pod Running with three containers, counting the infra container. The web container now reaches Redis at 127.0.0.1:6379 rather than at app-cache:6379. Two rules follow from the shared namespace: publish ports on the pod and never on a member, and no two members may listen on the same port.

This is the Kubernetes model, and Podman leans into it. podman kube generate app > app.yaml writes a Kubernetes manifest from what is running (older packages spell it podman generate kube), and podman kube play app.yaml recreates it on another host. Quadlet has a .kube unit type that runs such a file as a systemd service. It is a genuinely different way to group services, and it is the strongest reason to choose Podman if Kubernetes is anywhere in your future.

不使用 daemon 自動啟動:quadlet units

Quadlet 是 systemd generator。它會將描述容器的簡短檔案轉換為開機時使用的完整 systemd service。rootless user 的檔案放在 ~/.config/containers/systemd/,root 的檔案放在 /etc/containers/systemd/

~/.config/containers/systemd/caddy.container

[Unit]
Description=Caddy web server
After=network-online.target

[Container]
Image=docker.io/library/caddy:2
ContainerName=caddy
PublishPort=8080:80
Volume=caddy-data.volume:/data
Environment=TZ=UTC
AutoUpdate=registry

[Service]
Restart=always
MemoryMax=512M

[Install]
WantedBy=default.target

~/.config/containers/systemd/caddy-data.volume 幾乎可以是空的,因為 section header 會建立 volume:

[Volume]
systemctl --user daemon-reload
systemctl --user start caddy
systemctl --user status caddy
journalctl --user -u caddy -n 50

service 名稱來自檔名:caddy.container 會成為 caddy.service。請勿執行 systemctl --user enable caddy。產生的 unit 無法啟用,systemd 會回覆 Failed to enable unit: Unit /run/user/1000/systemd/generator/caddy.service is transient or generated.[Install] section 會在開機時啟動容器,而 daemon-reload 會在你編輯檔案後重新產生 unit。

接下來是幾乎所有人都會忽略的設定:

sudo loginctl enable-linger deploy
loginctl show-user deploy --property=Linger

預期會看到 Linger=yes。未啟用 linger 時,最後一個 SSH 連線關閉後,systemd 會終止整個 user session,因此所有 rootless container 也會一併停止,且不會在開機時重新啟動。登出後就消失的容器,一律是這個原因。

由於容器是一般 service unit 的主要程序,systemd 自身的控制項會直接套用。[Service] section 中的 MemoryMax=CPUQuota=,其行為與 使用 systemd 限制的其他 service 完全相同。這需要 cgroup v2(control group version 2);Ubuntu 自 22.04 起預設使用此版本。使用 podman info | grep -i cgroup 確認。

更新也有對應的機制。AutoUpdate=registry 加上 systemctl --user enable --now podman-auto-update.timer 會檢查 registry 上相同 tag 是否有較新的 image,重新啟動 unit;如果新容器無法啟動,則回復到先前的 image。先執行 podman auto-update --dry-run,查看它會進行哪些變更。較舊的 podman generate systemd command 仍然存在,但已被標示為 deprecated,因此新的設定請使用 quadlet。

Docker 別名適用與不適用的範圍

sudo apt install -y podman-docker
sudo touch /etc/containers/nodocker
docker ps

podman-docker 會安裝一個呼叫 Podman 的 /usr/bin/docker wrapper。若沒有 nodocker 檔案,每次呼叫都會先顯示 Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg.。這個 wrapper 涵蓋日常使用的命令:runpslogsexecbuildpullpushinspectcpvolumenetwork

無法相容的項目較少,但差異更明顯。Swarm mode 沒有對應功能,因此 Swarm stack 無法部署。需要連線至 Docker socket 的工具,必須匯出 Podman socket;部分工具仍會察覺差異。Traefik 的 Docker provider 指向 /run/user/<uid>/podman/podman.sock 時可以運作,但 Watchtower 完全沒有對應位置,因為 podman auto-update 已負責這項工作。儲存空間彼此分離,因此 Podman 看不到你先前使用 Docker pull 的映像,而忙碌中的 Docker 主機上的 podman images 一開始會是空的。

逐步遷移執行中的服務堆疊

  1. 建立或選擇擁有容器的非特權使用者,並確認該使用者在 /etc/subuid 中具有可用的範圍。
  2. 重新提取來自 registry 的所有映像,並使用完整限定名稱。Podman 使用自己的映像儲存區,不會讀取 Docker 的映像儲存區。
  3. 使用 docker save app:1.4 | podman load 將本機建置的映像移轉過去。
  4. 停止 Docker 容器,將每個 volume 的內容從 /var/lib/docker/volumes/<name>/_data 複製出來,再使用 podman unshare chown -R 1000:1000 <path> 修正擁有權。
  5. 處理連接埠配置:在反向代理後方發布 1024 以上的連接埠,或設定 net.ipv4.ip_unprivileged_port_start
  6. 每個容器建立一個 quadlet 檔案,執行 systemctl --user daemon-reload,然後啟動每項服務。
  7. 執行 sudo loginctl enable-linger <user>,重新啟動 VPS,重新登入,並確認 podman ps 再次列出每項服務。

兩個引擎互不共用任何資源:映像儲存區與網路都各自獨立。因此,遷移期間可以同時執行兩者;唯一可能互相衝突的是主機連接埠編號。先遷移一項服務,觀察一天後再遷移下一項。

Podman 與 Docker:哪一個適合部署在你的 VPS 上?

如果你的技術堆疊使用其他人也會維護的 compose 檔案,或依賴會與 Docker socket 通訊的工具,請繼續使用 Docker。與其他人撰寫的設定保持相容本身就是一項實際優勢,而 Docker 在這方面支援較廣。如果團隊成員的筆記型電腦都執行 Docker,則在 production 使用相同的 engine,也能帶來明確的好處。

如果 VPS 只執行少數由你全程控管的服務,或你希望讓每個應用程式都由各自的 unprivileged user 執行,且主機上完全不需要 docker group,請改用 Podman。與發行版的整合程度也很重要:RHEL 及其重建版本將 Podman 作為支援的 engine,因此在這些系統上使用 Podman,較不容易遇到非預期問題。如果其他服務都已由 systemd units 管理,quadlets 會像是補上缺少的一環,而不是需要重新學習的新工具。

還有一個值得說明的折衷方案。Rootful Podman 的行為與 Docker 很相近,透過 wrapper 保留 docker command,同時移除持續執行的 daemon。不過,它也放棄了 rootless 的部分,而這正是會改變安全性狀態的功能,因此應將它視為過渡方案。

如果你仍在建立第一台 container host,在全新 VPS 上設定並強化 Docker 是較短的路徑,而且先前學到的內容都不會浪費。在這兩個 engine 中,images 與 volumes 都是相同的物件,因此之後搬移時,主要只需改變服務的管理方式,其他部分幾乎不變。

FAQ

Podman 是 Docker 的直接替代品嗎?

就輸入的命令而言,兩者相當接近。安裝 podman-docker 會提供 /usr/bin/docker wrapper,而 runpsbuildlogsexec 的行為也相同。但它不是 daemon 的替代品。Swarm 沒有對應功能;連線至 /var/run/docker.sock 的工具必須改用每位使用者的 Podman socket;此外,Docker 拉取的 image 對 Podman 不可見,因為兩者使用不同的儲存空間。

為什麼我以 rootless Podman 執行的 container 在登出 SSH 後會停止?

因為 systemd 會在最後一個登入工作階段關閉時停止該使用者的 session,以及其中的所有 user service。執行 sudo loginctl enable-linger <user>,然後確認 loginctl show-user <user> --property=Linger 輸出 Linger=yes。Linger 會讓該使用者的 systemd instance 在沒有作用中 session 時持續執行,這也是 container 能在重新開機後再次啟動的原因。

為什麼 volume 中的檔案擁有者是 UID 100999?

Rootless Podman 會將 container UID 0 對應至主機上的使用者,然後將 container UID 1 以上的 UID 對應至你的 subuid 範圍。若範圍從 100000 開始,container UID 1000 在主機上就會變成 100999。請在 namespace 內使用 podman unshare chown 1000:1000 /path/to/data 修正,第一次執行時以 :U flag 掛載,或使用 --userns=keep-id,讓 container UID 與你自己的 UID 相符。

我可以繼續使用 docker-compose.yml 搭配 Podman 嗎?

可以,有兩種方式。podman-compose 會讀取該檔案,並直接操作 Podman CLI。或者使用 systemctl --user enable --now podman.socket 啟用相容性 socket,設定 DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sock,再對該 socket 執行真正的 docker composenetwork_mode: host 可能會遇到相容性問題;掛載 Docker socket 的 service 也可能無法正常運作;restart: always 則需要 quadlet unit 與 linger,才能在重新開機後持續執行。

Rootless 真的能讓 container 更安全嗎?

它能消除一項特定風險:若程序從 rootless container 脫逸,取得的是未具特殊權限之使用者的權限,而不是 root 的權限。這項保護很有價值,也是 rootless Podman 沒有 root 等效 docker 群組的原因。但它無法防止 kernel 漏洞,也無法保護你的使用者本身能讀取的檔案。因此,仍應維持在任何伺服器上都會採取的其他強化措施。