SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 更新于 2026-08-22

Podman 与 Docker 在 VPS 上到底有什么区别

Podman 默认无守护进程且以非 root 用户运行。本文说明这会如何影响 compose 文件、quadlet、1024 以下端口、Docker socket 与卷的所有权。

Podman 与 Docker 实际有哪些不同

Podman 和 Docker 都能在 VPS 上运行相同的 OCI(开放容器计划)镜像,因此选择哪一个并不取决于能够运行哪些软件。区别在于进程模型。Docker 运行一个 root daemon,由它管理所有容器;docker 命令只是一个小型客户端,用于请求该 daemon 执行操作。Podman 没有 daemon:podman run 会在调用它的进程下,以您自己的非特权用户身份将容器作为子进程启动。

其他差异都源于这一点。自动启动由 systemd 负责,而不是由 daemon 负责。卷的所有权会经过用户命名空间映射,因此您在主机上使用 ls -l 看到的所有者,不是容器内看到的所有者。除非修改内核设置,否则无法绑定 1024 以下的端口。docker CLI(命令行界面)通过包装器仍可继续使用,但如果某项操作需要 Docker socket,就会失效。

无守护进程:启动容器时实际运行的是什么

在 Docker 主机上,pstree -a 会显示以 root 身份运行的 dockerd,其旁边是 containerd,每个运行中的容器对应一个 containerd-shim-runc-v2。您的应用是该 shim 的子进程,而 shim 是 PID 1 的子进程。容器不会连接到启动它的 shell。停止守护进程后,主机上所有容器的控制平面都会丢失;如果默认的 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 不会停止已经运行的容器;一个容器的监控进程崩溃,也不会连带停止其他容器。

没有守护进程也会带来代价。系统重启后,不会有进程自动启动您的容器。Docker 的 --restart=always 是守护进程在启动时兑现的承诺,而 Podman 使用 systemd 替代它,下面的 quadlet 部分就是用于此目的。

套接字是另一个关键部分。/var/run/docker.sock 是一个由 root 拥有的 API(应用程序编程接口)端点。任何能够向其写入的进程,都可以启动一个挂载主机文件系统的特权容器。将用户加入 docker 组,会以更间接的方式授予该用户 root 权限;建议结合阅读仅向每个服务账户授予其所需的访问权限。除非明确请求,否则 Podman 不会公开套接字;创建的套接字属于 /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 软件包提供 newuidmap 和 newgidmap。它们是 setuid 辅助程序,允许普通用户使用一段从属 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 用户都需要一段从属 ID:

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

Ubuntu 中由 adduser 创建的用户会自动获得一段 ID。由 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.

分配一段 ID,然后重置该用户的存储,使其使用新的映射:

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

首次运行时还可能遇到一个问题:Podman 不会默认使用 Docker Hub。短镜像名称会根据 /etc/containers/registries.conf 中的 unqualified-search-registries 进行解析;在未连接终端的脚本中,拉取操作会因 short-name resolution enforced but cannot prompt without a TTY 而失败。每次都写完整的镜像名称。使用 docker.io/library/nginx:1.27,不要使用 nginx。

无 root 容器在租用服务器上究竟能带来什么

无 root 容器在用户命名空间中运行。用户命名空间是内核功能,可为进程提供独立的用户 ID 映射。在该命名空间内,容器的超级用户 UID(用户 ID)为 0。在命名空间外,也就是 VPS 上,同一进程对应的是您的普通登录用户。容器内的 root 不是主机上的 root。

这就是它实际带来的安全收益。某个镜像要求以 root 运行,某个 Web 应用存在远程代码执行漏洞,或者某种逃逸依赖于进程在命名空间外具有 UID 0:在这些情况下,进程最终获得的是您的非特权用户权限,而不是整台机器的权限。rootless 无法保护您免受内核漏洞影响,也无法保护您自己的文件,因为逃逸后的进程以您的身份运行,可以读取您能够读取的所有内容。隔离单元的重要性至少不低于 UID 映射。下一节将介绍 FreeBSD jail;与从 registry 拉取的分层镜像相比,jail 封装了您管理的完整用户空间,更容易理解这一点,参见 FreeBSD jail:将完整用户空间封装成由您管理的小型系统。

Docker 也可以以 rootless 模式运行。dockerd-rootless-setuptool.sh install 会为每个用户设置独立的 daemon,实际效果很好。区别在于默认方向不同。使用 Podman 时,默认无需额外操作即可使用 rootless。因此,您遇到的第一个问题会是容器无法绑定 80 端口,而不是某个服务在两年间一直悄悄以 root 身份运行。

为什么我的卷文件归 UID 100999 所有?

因为使用的是同一个用户命名空间。容器 UID 0 会映射到您的主机 UID。容器 UID 1 会映射到您 subuid 范围中的第一个 ID,后续 UID 依次递增。对于从 100000 开始的范围,容器 UID 1000 在主机上会映射为 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"

容器显示 1000。主机列表显示所有者为 100999,因为 100000 加上 1000 再减去 1 等于 100999。没有任何内容损坏,普通的 chown 也无法修复此问题,因为在命名空间之外,您的非特权用户完全不能更改文件所有权。

有以下 4 种解决方法:

  • podman unshare chown 1000:1000 "$PWD/data" 会在同一个用户命名空间中执行 chown,此时这些数字的含义与容器内一致。
  • -v "$PWD/data:/data:U" 要求 Podman 为您修正源目录的所有权。请在新目录上使用,不要用于包含重要数据的目录。
  • --userns=keep-id 会将您的主机 UID 映射为容器内的相同 UID,因此新文件会归您所有。
  • 使用 -v appdata:/data 等命名卷可以避免这个问题,因为 Podman 会在您自己的存储中创建该卷,并预先设置正确的所有权。

如果您在 Docker 中遇到过这个问题,本质上是同一个问题,只是多了一层映射。许多镜像提供的 PUID 和 PGID 变量会设置容器内进程使用的 UID;在 rootless Podman 中,该 UID 随后还会再次映射。rootless 容器中的 PUID=1000 仍会创建归主机 UID 100999 所有的文件。设置这些数字时,请考虑第二次映射;或者将数据移到命名卷中,不再处理这个问题。

关于挂载,再说明两点。您在 Fedora 和 RHEL 示例中看到的 :z 和 :Z 标志是 SELinux 重新标记选项,而 Ubuntu 使用 AppArmor,因此这些标志在那里不起作用。rootless Podman 也不能挂载您的用户无法读取的主机目录,这是预期的限制,不是故障。

为什么 rootless Podman 拒绝发布 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 端口上发布服务,再在前面放置反向代理;证书正应由前端的 nginx 使用 certbot 申请和续期。

rootless 发布还会改变应用看到的内容。Podman 4.x 默认使用带有 rootlesskit 端口处理程序的 slirp4netns,转发的连接会带有重写后的源地址,因此访问日志会将每个访问者记录为 10.0.2.100。Podman 5.0 将默认值改为 pasta,可以保留真实的客户端地址。在 4.x 中,--network slirp4netns:port_handler=slirp4netns 可恢复真实源地址,但吞吐量会有所下降。

这里有一个好消息。rootless 发布的端口是由普通进程拥有的普通监听套接字,因此防火墙的入站规则会对它生效。Docker 通过写入 NAT(网络地址转换)规则和自身的转发 accept 规则来发布端口,这正是 已发布的 Docker 端口会忽略您以为能阻止它的 ufw 规则 的原因。Rootful Podman 使用类似的底层机制,因此也有相同的问题。Rootless Podman 不会。

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 ps 和 podman ps 应列出相同的容器,因为系统中只有一组容器。名称解析也可以正常工作:Podman 的默认网络后端 netavark 会运行 aardvark-dns,因此用户定义网络中的容器可以通过名称相互访问。

但仍存在实际限制。凡是挂载 /var/run/docker.sock 的配置,都必须改为指向 Podman socket,或者删除该配置。network_mode: host 在用户命名空间下的行为不同。不同 podman-compose 版本对带有 condition: service_healthy 的 depends_on 支持不一致。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.

无需守护进程即可自动启动:quadlet 单元

Quadlet 是 systemd 生成器。它会将描述容器的简短文件转换为启动时使用的实际 systemd 服务。对于 rootless 用户,文件放在 ~/.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 可以几乎为空,因为节标题会创建卷:

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

服务名称来自文件名:caddy.container 会变为 caddy.service。不要运行 systemctl --user enable caddy。生成的单元无法启用,systemd 会返回 Failed to enable unit: Unit /run/user/1000/systemd/generator/caddy.service is transient or generated.。[Install] 节负责在启动时启动容器,daemon-reload 则会在您编辑文件后重新生成单元。

现在说明几乎所有人都会遇到的设置:

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

应看到 Linger=yes。如果未启用 linger,最后一个 SSH 连接关闭后,systemd 会拆除整个用户会话,因此所有 rootless 容器都会随之停止,并且不会在启动时恢复。退出登录后消失的容器,原因始终是这个。

由于容器是普通服务单元的主进程,systemd 自身的控制机制会直接生效。[Service] 节中的 MemoryMax= 和 CPUQuota= 与使用 systemd 限制的其他服务完全相同。这要求使用 cgroup v2(控制组版本 2)。自 22.04 起,Ubuntu 默认使用 cgroup v2。使用 podman info | grep -i cgroup 确认。

更新也有对应的机制。AutoUpdate=registry 加上 systemctl --user enable --now podman-auto-update.timer 会检查注册表中同一标签是否有更新的镜像,重启单元;如果新容器无法启动,则回滚到之前的镜像。先运行 podman auto-update --dry-run,查看它将执行哪些更改。旧的 podman generate systemd 命令仍然存在,但已弃用,因此新内容应使用 quadlet 编写。

Docker 别名适用的场景和不适用的场景

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

podman-docker 会安装一个调用 Podman 的 /usr/bin/docker 包装器。如果没有 nodocker 文件,每次调用都会先输出 Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg.。该包装器覆盖您日常输入的命令:run、ps、logs、exec、build、pull、push、inspect、cp、volume、network。

无法兼容的内容更少,但差异更明显。Swarm 模式没有等效功能,因此 Swarm 堆栈无法运行。与 Docker socket 通信的工具需要导出 Podman socket,其中一些工具仍会识别出差异;将 Traefik 的 Docker provider 指向 /run/user/<uid>/podman/podman.sock 后可以正常工作,而 Watchtower 完全没有对应位置,因为 podman auto-update 已经执行了相同的工作。存储彼此独立,因此 Podman 看不到您之前使用 Docker 拉取的镜像;在繁忙的 Docker 主机上,podman images 初始为空。

分步迁移正在运行的服务栈

  1. 创建或选择用于拥有容器的非特权用户,并确认该用户在 /etc/subuid 中具有用户 ID 范围。
  2. 使用完整限定名称重新拉取所有来自镜像仓库的内容。Podman 使用独立的镜像存储,不会读取 Docker 的镜像存储。
  3. 使用 docker save app:1.4 | podman load 迁移本地构建的镜像。
  4. 停止 Docker 容器,将每个卷的内容从 /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,那么生产环境使用相同的引擎也能带来实际好处。

如果 VPS 只运行少量由您端到端控制的服务,或者您希望让每个应用都运行在独立的非特权用户下,并且主机上完全不需要 docker 组,则可以切换到 Podman。发行版的一致性也很重要:RHEL 及其重建版将 Podman 作为受支持的引擎提供,因此在这些系统上使用 Podman 可以减少意外问题。如果您仍想在这些主机上使用 Docker,Rocky Linux 和 AlmaLinux 上的 dnf 安装路径需要先清除已经占用 docker 命令的 podman-docker 包装器。如果您已经使用 systemd 单元管理其他所有服务,那么 quadlet 会像补上缺失的一环,而不是需要重新学习的新工具。

还有一种折中方案值得说明。Rootful Podman 的行为与 Docker 很相似,可通过包装器保留 docker 命令,同时不再需要持续运行的 daemon。但它放弃了 rootless 模式,而 rootless 模式正是会改变安全状况的部分,因此应将其视为过渡方案。

如果您还在构建第一个容器主机,在全新 VPS 上配置并加固 Docker 的路径更短,而且之前学到的内容都不会浪费。两种引擎使用相同的镜像和卷,因此之后迁移时,变化主要在于服务的管理方式,其他方面几乎不变。

FAQ

Podman 是 Docker 的直接替代品吗?

对于输入的命令来说,基本可以。安装 podman-docker 后会提供 /usr/bin/docker 包装器,run、ps、build、logs 和 exec 的行为也相同。但它不能替代 daemon。Swarm 没有对应功能,连接 /var/run/docker.sock 的工具必须改为指向每个用户的 Podman socket;此外,Docker 拉取的镜像对 Podman 不可见,因为两者使用独立的存储。

为什么我退出 SSH 后,rootless Podman 容器会停止?

因为 systemd 会在最后一个登录会话关闭时停止用户会话及其中的所有用户服务。运行 sudo loginctl enable-linger <user>,然后确认 loginctl show-user <user> --property=Linger 输出 Linger=yes。Linger 会在没有活动会话时保持该用户的 systemd 实例运行,这也使容器能在重启后再次启动。

为什么我的卷中的文件归 UID 100999 所有?

Rootless Podman 会将容器 UID 0 映射到您的主机用户,然后将容器 UID 1 及更大的 UID 映射到您的 subuid 范围。如果该范围从 100000 开始,容器 UID 1000 在主机上就会变成 100999。使用 podman unshare chown 1000:1000 /path/to/data 在 namespace 内修正权限;首次运行时使用 :U 标志挂载;或者使用 --userns=keep-id,使容器 UID 与您的 UID 匹配。

我可以继续在 Podman 中使用 docker-compose.yml 吗?

可以,有两种方式。podman-compose 会读取该文件,并直接调用 Podman CLI。或者使用 systemctl --user enable --now podman.socket 启用兼容 socket,设置 DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sock,然后通过该 socket 运行真正的 docker compose。在 network_mode: host、挂载 Docker socket 的服务以及 restart: always 方面可能需要额外处理;后者需要 quadlet unit 和 linger 才能在重启后继续运行。

rootless 真的能让容器更安全吗?

它可以消除一种特定风险:如果进程从 rootless 容器中逃逸,该进程获得的是您的非特权用户权限,而不是 root 权限。这项保护很有价值,也是 rootless Podman 中不存在与 root 等效的 docker 组的原因。但它无法阻止内核漏洞,也无法保护您自己的用户能够读取的文件。因此,仍应执行您在任何服务器上都会进行的其他加固措施。