为什么个人站长该认真看看 Podman:一个不需要 root 也能跑容器的方案
如果你和我一样,是靠一台 VPS 养活几个网站的个人站长,那你多半已经用过 Docker。Docker 很好用,但用久了总有几个地方让人不太舒服:默认要 root 权限、守护进程常驻、镜像越来越大的时候磁盘清理起来心惊胆战。而 Podman 这几年逐渐成熟,最大的卖点就一句话——它是无守护进程(daemonless)、默认支持 rootless 的容器引擎,命令行跟 Docker 几乎一模一样。
这篇文章不讲空洞的概念,只讲我在一台 2 核 4G 的 Debian 12 VPS 上,用 Podman 从零把一个 PHP + MySQL 的站点跑起来,并且用 systemd 用户服务做到开机自启、崩溃自恢复的完整过程。中间踩过的坑我会全部标出来。
一、Podman 和 Docker 到底差在哪
先说清楚概念,免得你误以为 Podman 是「另一个 Docker 分支」。它俩都遵循 OCI(Open Container Initiative)标准,镜像格式、容器运行时规范都是一样的,所以你从 Docker Hub 拉下来的镜像,Podman 可以直接用。差异在于架构:
- 守护进程:Docker 有一个常驻的 dockerd,所有操作都发给它执行;Podman 没有守护进程,你敲一条 podman run,它就是当前用户下的一个普通子进程,跑完就退,只有容器本身留着。
- 权限模型:Docker 默认 root,即使加了 docker 组,本质上也是给了这个人「隐形的 root 权限」;Podman 从设计上就鼓励 rootless,用普通用户跑容器,容器里就算被攻破,也拿不到宿主机 root。
- 编排:Docker 有 docker compose;Podman 提供了 podman-compose,也支持 podman generate systemd(新版是 quadlet)把你的容器变成 systemd 服务。
- Pod 概念:Podman 原生支持「Pod」——一个 Pod 里的多个容器共享网络命名空间,这其实是照搬 K8s 的模型,对以后想上 k3s 的人是个铺垫。
需要提醒的是:Podman 不是「Docker 的完全平替」。有些依赖 Docker socket(/var/run/docker.sock)的工具,比如某些监控面板、Watchtower 自动更新,在 Podman 下要额外开 podman.socket 才能对接。这个后面会讲。
二、安装:Debian 12 上三行就搞定
Debian 12 的官方源里就有 Podman,不需要加第三方仓库,这点比装 Docker 少折腾一步。
apt update
apt install -y podman podman-compose slirp4netns fuse-overlayfs
podman --version
# podman version 4.3.1 (Debian 12 自带版本)
# 查看是否支持 rootless,普通用户下执行:
podman info --format '{{.Host.Security.Rootless}}'装的时候注意两个依赖:slirp4netns 是 rootless 模式下做用户态网络转发的关键组件,缺了它你的容器连不上外网;fuse-overlayfs 是给非特权用户提供叠加文件系统的驱动,没有它 Podman 会退回到 vfs 驱动,性能差一大截,磁盘占用也会翻倍。
建议先去 /etc/containers/registries.conf 里确认一下镜像源。国内机器上拉 Docker Hub 经常超时,可以把 unqualified-search-registries 换成国内镜像站:
# /etc/containers/registries.conf 末尾追加
unqualified-search-registries = ["docker.io"]
[[registry]]
prefix = "docker.io"
location = "docker.io"
[[registry.mirror]]
location = "docker.m.daocloud.io"三、准备目录与数据卷
rootless 模式下有个习惯要改:不要再用 /var/lib/mysql 这种系统路径,改成用户家目录下的独立目录,并且用 bind mount 挂进去。这样即便容器删掉,数据也还在你自己的目录里,权限也不会打架。
mkdir -p ~/sites/blog/{html,conf,data/mysql,logs}
cd ~/sites/blog
# 注意:MySQL 官方镜像里 mysql 用户 uid 是 999
# rootless 下容器内的 root 映射成宿主机的当前用户,
# 所以这里直接把数据目录给当前用户即可
id
ls -ld ~/sites/blog/data/mysql这里有个 rootless 专属的大坑:容器里的 uid 0 会被映射成宿主机的当前用户(比如 uid 1000),uid 1 映射成宿主机 100000,以此类推。所以你在宿主机的 ls -l 里看到的文件属主会是一串很大的数字(100000+),这是正常的,不要手动 chown 成 root,会直接导致容器启动时报 permission denied。
四、用 podman-compose 拉起 PHP + MySQL
先写一个 compose 文件,结构跟 Docker Compose 基本一样,但有几个字段是 Podman 特有的,我标出来了:
# ~/sites/blog/compose.yaml
version: "3"
services:
db:
image: docker.io/library/mysql:8.0
container_name: blog_db
restart: always
environment:
MYSQL_ROOT_PASSWORD: 换成你自己的强密码
MYSQL_DATABASE: blog
MYSQL_USER: bloguser
MYSQL_PASSWORD: 换成你自己的强密码
volumes:
- ./data/mysql:/var/lib/mysql:Z
- ./conf/my.cnf:/etc/mysql/conf.d/custom.cnf:ro
# rootless 下建议显式指定运行用户,避免权限混乱
userns_mode: "keep-id"
networks:
- blognet
web:
image: docker.io/library/php:8.2-fpm
container_name: blog_web
restart: always
volumes:
- ./html:/var/www/html:Z
depends_on:
- db
userns_mode: "keep-id"
networks:
- blognet
networks:
blognet:
driver: bridge两个关键点:
:Z后缀。SELinux 或启用了文件标签的系统上,不写 Z 会出现「容器内看不到文件」的诡异现象。Debian 默认没开 SELinux,但加上 Z 无害,兼容性强,建议养成习惯。userns_mode: "keep-id"。这是 Podman 特有的,作用是让容器内的 uid 和你宿主机的 uid 一致,避免写入文件后 owner 变成一串大数字。对需要频繁读写挂载目录的 PHP 站点非常有用。
启动:
cd ~/sites/blog
podman-compose up -d
# 看状态
podman ps
podman-compose logs -f web五、把容器变成 systemd 用户服务(开机自启)
这是 Podman 相对 Docker 最香的一点:不需要额外的守护进程,直接生成 systemd 单元,交给 systemd 管理生命周期。但注意,rootless 容器要挂在 用户级 systemd 下(systemctl --user),不是系统级。
# 1. 允许当前用户在注销后仍保留 systemd 用户实例(关键,否则退出 SSH 服务就停了)
loginctl enable-linger $USER
# 2. 生成 systemd 单元文件(Podman 4.x 还支持 generate systemd)
mkdir -p ~/.config/systemd/user
cd ~/sites/blog
podman generate systemd --new --files --name blog_web
podman generate systemd --new --files --name blog_db
# 生成的 .service 文件会出现在当前目录,移到用户 systemd 目录
mv container-blog_web.service container-blog_db.service ~/.config/systemd/user/
# 3. 重载并启用
systemctl --user daemon-reload
systemctl --user enable --now container-blog_db.service
systemctl --user enable --now container-blog_web.service
# 4. 查看状态
systemctl --user status container-blog_web最容易踩的坑:如果不执行 loginctl enable-linger,你一退出 SSH 登录,用户 systemd 实例就被销毁,容器全都停了。很多人第一次用 Podman 做自启,发现「我明明 enable 了怎么还是停」——百分之九十是这个原因。
另外,Podman 4.4 以后官方推荐用 Quadlet 替代 generate systemd(后者在新版已被标记废弃)。Quadlet 的写法是写一个 .container 文件放到 ~/.config/containers/systemd/,systemd 启动时会自动把它转成 service。如果你用的 Podman 版本较新,可以往这个方向迁移:
# ~/.config/containers/systemd/blog-web.container
[Unit]
Description=Blog PHP-FPM container
[Container]
Image=docker.io/library/php:8.2-fpm
ContainerName=blog_web
Volume=%h/sites/blog/html:/var/www/html:Z
[Service]
Restart=always
[Install]
WantedBy=default.target然后 systemctl --user daemon-reload && systemctl --user start blog-web 即可。Quadlet 的好处是配置文件更干净,也不需要每次改完 compose 都重新 generate。
六、几个 rootless 特有的常见故障
1. 容器起不来,报 cannot clone: Operation not permitted
这通常是因为内核的 user namespace 没放开。检查一下:
sysctl user.max_user_namespaces
# 如果输出是 0,需要打开
echo "user.max_user_namespaces=15000" >> /etc/sysctl.d/99-podman.conf
sysctl --systemDebian 12 默认是开着的,但有些精简镜像或云厂商定制内核会关掉,尤其是一些国内厂商的模板。
2. 低端口绑定失败(想用 80 端口)
rootless 下普通用户无法直接监听 1024 以下端口。解决办法有两个:一是把宿主机端口映射到 8080 之类的高位端口,前面用 Nginx 反代;二是给 podman 二进制赋 capabilities(不推荐,等于变相提权)。我选的是第一种,反正前面本来就有 Nginx 做 TLS 终结。
# 把容器 80 映射到宿主机 8080
-p 8080:803. 容器日志无限膨胀吃满磁盘
Podman 的日志默认走 journald(跟 Docker 的 json-file 不同),所以要用 journalctl 的容量限制来管:
# /etc/systemd/journald.conf
SystemMaxUse=500M
SystemMaxFileSize=50M
MaxRetentionSec=2week
systemctl restart systemd-journald
# 立刻清理历史日志,只留最近 3 天
journalctl --vacuum-time=3d顺带提醒,podman system prune -a 会同时清掉未被使用的镜像、容器和网络,但默认不会删数据卷,这一点比 Docker 稍微保守一点,算是好事。
七、值不值得从 Docker 换过来
我的结论是分情况:
- 如果你只有一台 VPS、跑几个静态站或 PHP 站:Podman 完全够用,而且 rootless 带来的安全性提升是实打实的,值得换。命令行几乎不用重新学。
- 如果你重度依赖 Docker 生态(Portainer、Watchtower、各种只认 docker.sock 的工具):继续用 Docker 更省事,Podman 的 socket 兼容层虽然能用,但偶发问题不少。
- 如果你以后打算上 K8s / k3s:先熟悉 Podman 的 Pod 概念会有帮助,迁移心智负担小很多。
对我这种「一个人、一台机器、几个站」的草根站长来说,Podman 最大的价值不是性能,而是出问题时的心态——知道哪怕容器被人打了,拿到的也只是我那个普通用户名,而不是整台机器的 root。这种安全感,比省那点内存重要得多。