Podman 工程手册:从 Rootless 用户边界到可交接容器运行时
Podman 最值得团队采用的部分,不是把 docker 替换成 podman,而是把“谁拥有容器状态、谁能控制这些进程”从宿主 root 收回到明确用户。Podman 命令参考把 rootless 用户命名空间、默认 socket、连接优先级与用户存储写成公开语义。代价也在这里:普通用户、root 用户和其他账号拥有彼此不可见的容器存储;bind mount 需要同时解释宿主权限、容器 UID 和 SELinux 标签;依赖 Docker API 的工具只能获得兼容入口,不能获得行为完全等价的承诺。
在 Linux 上,Podman 可以直接使用宿主内核运行 rootless 容器。在 macOS 和 Windows 上,Linux 容器仍要进入由 podman machine 管理的虚拟机。Podman Machine 参考明确说明这些平台必须使用 Machine;“rootless”指的是虚拟机内的用户模型,不等于没有 VM、没有 socket 或没有持久磁盘。
安装并确认当前用户的运行环境
在受支持的 Ubuntu/Debian 或 Fedora/RHEL 系统,由具有 sudo 权限的管理员安装发行版提供的包。先查看版本;后续 Quadlet 路线需要包含 Quadlet 的 Podman 4.4 及以上版本和 systemd 用户会话。包来源和平台差异见 Podman 安装说明。
# Ubuntu / Debian,使用发行版已签名的软件仓库
sudo apt update
sudo apt install -y podman uidmap
podman --version
# Fedora / RHEL 系,改用这一组,不与 apt 组同时执行
sudo dnf install -y podman
podman --version安装包需要管理员权限,后面的 podman run 以当前普通用户执行,不加 sudo。首次拉取镜像需要访问其 registry;企业网络使用批准的代理、镜像仓库和 CA,不关闭签名或 TLS 校验。macOS/Windows 的客户端按官方安装说明取得,再使用下面的 Machine 路线。
安装包来源、客户端版本、默认连接、rootless 状态和存储根是第一组交接证据:
podman version
podman system connection list
podman info --format 'rootless={{.Host.Security.Rootless}} graphRoot={{.Store.GraphRoot}} driver={{.Store.GraphDriverName}}'
podman ps -a
podman images --digests
podman volume lsLinux 团队基线若选择 rootless,rootless=true 应稳定成立,graphRoot 应落在当前用户可解释的数据目录。随后分别执行普通用户 podman images 与经批准的 sudo podman images,两边结果不同是用户边界生效,不是镜像丢失。不要复制内部 graph root 来“同步”;可交付镜像应进入受控 Registry,并以 Digest 复核。
macOS 或 Windows 还要保存 Machine 身份:
podman machine list
podman machine info
podman system connection list
podman info新建团队专用 Machine 时显式给出资源和 rootful 选择:
podman machine init --rootful=false --cpus 4 --memory 4096 --disk-size 40 dev-podman
podman machine start dev-podman
podman machine inspect dev-podmanCPU、内存、磁盘和 VM provider 都是开发机基线的一部分。切换 Machine 的 rootful/rootless 默认值会把连接指向另一套存储;对象通常只是暂时不可见,不应在没有盘点时执行 reset。
Rootless 的真实权限边界
Rootless Podman 会为当前用户创建用户命名空间,并使用 /etc/subuid、/etc/subgid 提供额外映射范围。先核对辅助程序和映射:
command -v podman newuidmap newgidmap
grep "^${USER}:" /etc/subuid /etc/subgid
podman unshare cat /proc/self/uid_map
podman info --debug映射范围必须存在、足够且不与团队制度冲突。管理员调整映射后,旧 pause process 可能继续持有原命名空间;先停止当前用户的容器,再执行 podman system migrate,不要手工删除 PID 或存储元数据。
Rootless 降低了常驻高权限 daemon 的暴露面,却不是强沙箱。容器仍共享宿主内核,宿主目录挂载、设备、capability、seccomp、网络和内核漏洞仍决定风险。陌生镜像先以只读文件系统、非 root 用户、最少 capability 和无宿主 socket 的方式验证,不能因为命令由普通用户发起就跳过镜像来源与运行参数审查。
用正反实验验证端口和用户映射
下面的 Linux/macOS Bash 夹具使用全新名称和随机回环端口,不占用预设宿主端口。Podman 发布端口允许省略宿主端口,再由 podman port读取实际映射。先确认 podman info 的 rootless 状态和当前 Machine/连接指向预期环境;macOS/Windows 的 huser 属于 Machine 内宿主用户,不是桌面系统账号。
(
set -eu
test "$(podman info --format '{{.Host.Security.Rootless}}')" = true
work="$(mktemp -d "${TMPDIR:-/tmp}/te05-podman.XXXXXXXX")"
name="$(basename "$work")"
cleanup() {
label="$(podman inspect "$name" --format '{{ index .Config.Labels "te05.lab" }}' 2>/dev/null || true)"
if [ "$label" = "$name" ]; then
podman rm -f "$name" >/dev/null || printf 'container cleanup failed: %s\n' "$name" >&2
fi
rmdir -- "$work" || printf 'retain nonempty lab directory: %s\n' "$work" >&2
}
trap cleanup EXIT
podman run -d --name "$name" --label "te05.lab=$name" -p 127.0.0.1::8080 \
docker.io/nginxinc/nginx-unprivileged:1.28.2-alpine
podman inspect "$name" --format '{{.State.Status}} {{.HostConfig.PortBindings}}'
host_port="$(podman port "$name" 8080/tcp | awk -F: '{print $NF}')"
case "$host_port" in ''|*[!0-9]*) printf 'invalid mapped port: %s\n' "$host_port" >&2; exit 23 ;; esac
body="$(curl -q --noproxy '*' --retry 10 --retry-connrefused --retry-delay 1 \
--fail-with-body --silent --show-error --max-time 5 "http://127.0.0.1:$host_port/")"
test -n "$body"
podman top "$name" user huser
printf 'HTTP_OK host_port=%s container=%s\n' "$host_port" "$name"
)inspect 应显示运行中,HTTP 检查退出 0 并打印 HTTP_OK;top 的 user 与 huser 分别是容器内与 Podman 宿主侧身份,官方格式说明可核对。HTTP 或镜像拉取失败时子 shell 会退出并只清理标签属于本轮的容器;若清理失败,错误会给出精确名称,先用 podman inspect 核对再处理。团队夹具应固定经批准的镜像 Digest;这里的公开标签只适合首次演示。
反向实验在 Linux 普通用户的 rootless Podman 下验证宿主目录权限:同一镜像、用户映射与 :Z 标签条件保持不变,只改变新建目录的写权限。先拉取镜像以排除 registry 故障,再做可写、拒绝、恢复可写三次对照:
(
set -eu
test "$(podman info --format '{{.Host.Security.Rootless}}')" = true
image=docker.io/library/alpine:3.22
podman pull "$image"
fixture="$(mktemp -d "${TMPDIR:-/tmp}/te05-podman-mount.XXXXXXXX")"
cleanup() {
if [ -d "$fixture" ] && [ ! -L "$fixture" ] && [ -O "$fixture" ]; then
chmod 700 "$fixture"
rm -f -- "$fixture/input.txt" "$fixture/output.txt"
rmdir -- "$fixture" || printf 'retain nonempty fixture: %s\n' "$fixture" >&2
fi
}
trap cleanup EXIT
printf 'seed\n' > "$fixture/input.txt"
podman run --rm -v "$fixture:/work:Z" "$image" sh -c 'printf "ok\n" > /work/output.txt'
test "$(cat "$fixture/output.txt")" = ok
rm -- "$fixture/output.txt"
chmod 500 "$fixture"
if error="$(podman run --rm -v "$fixture:/work:Z" "$image" \
sh -c 'printf "ATTEMPTING_WRITE\n"; printf "bad\n" > /work/output.txt' 2>&1)"; then
printf 'unexpected write success\n' >&2; exit 23
fi
case "$error" in *'ATTEMPTING_WRITE'*'Permission denied'*) printf 'EXPECTED_DENIAL\n' ;;
*) printf 'unexpected failure: %s\n' "$error" >&2; exit 24 ;; esac
chmod 700 "$fixture"
podman run --rm -v "$fixture:/work:Z" "$image" sh -c 'printf "recovered\n" > /work/output.txt'
test "$(cat "$fixture/output.txt")" = recovered
printf 'RECOVERED_WRITE\n'
)预期依次看到 EXPECTED_DENIAL 和 RECOVERED_WRITE,目录中的输出文件最终为 recovered。拒绝检查还要求容器内先打印 ATTEMPTING_WRITE,避免把镜像拉取、挂载准备失败误当成实际写入被拒绝。若第一次可写就失败,先检查当前用户、用户映射、卷共享和 SELinux AVC,不能把它算作权限反例通过。:Z 会为本轮私有目录设置容器专用标签;共享目录须另评估 :z,二者都会改宿主标签,大目录重标记有成本,系统目录不应这样处理。这里控制了标签差异,不能据此声称验证了“缺少 :Z 会失败”。UID 不匹配优先从镜像用户、--userns=keep-id 和宿主目录 owner 解决;:U 会递归改 owner,必须先在副本验证。
网络问题要从 backend 和用户态转发开始
Rootless 网络可能经过用户态组件,低端口、源地址、VPN、企业 DNS 和宿主回连的行为不能从 rootful bridge 推断。记录:
podman info --debug
podman network ls
podman network inspect podman项目验收至少覆盖容器出网、容器间 DNS、宿主访问发布端口、容器访问明确的宿主入口和端口冲突反例。低端口绑定失败时先改用高端口;确需改变系统非特权端口下限,应由主机 owner 评估整机影响,不能通过临时切到 rootful 掩盖设计问题。
Docker API Socket 是高权限兼容入口
依赖 Docker API 的 IDE 或测试库可以连接 Podman system service。Linux rootless 常见入口是用户级 socket;以下命令以运行 Podman 的同一普通用户执行,XDG_RUNTIME_DIR 应指向该用户的运行时目录:
systemctl --user enable --now podman.socket
export DOCKER_HOST="unix://${XDG_RUNTIME_DIR}/podman/podman.sock"
pong="$(curl -q --noproxy '*' --fail-with-body --silent --show-error \
--unix-socket "${XDG_RUNTIME_DIR}/podman/podman.sock" http://d/v1.40/_ping)"
test "$pong" = OK
printf 'API_PING_OK\n'预期打印 API_PING_OK;失败时先检查用户级 socket 状态、当前 UID 与 XDG_RUNTIME_DIR,不要切到 rootful socket 试图“修复”权限。_ping 返回 OK 只证明连接可达。Build、network、volume、healthcheck、event、exec 和清理 API 仍要由真实项目夹具验证。把这个 socket 挂入容器,相当于把当前用户的容器控制权交给容器内进程;除非夹具明确需要且镜像可信,否则不挂载。网络暴露优先 SSH 转发或双向 TLS,不保留无认证 TCP 监听。
用户级 podman.socket 通过 systemd socket activation 按需启动服务,并没有把 Podman 变成一个拥有全部状态的中心 daemon。若登录会话结束后仍需服务,loginctl enable-linger 必须作为主机策略审查;不再需要兼容 API 时执行:
systemctl --user disable --now podman.socket
unset DOCKER_HOSTCompose 必须固定真正的 Provider
podman compose调用外部 Compose Provider。两台机器即便都能执行该命令,也可能由不同程序解析同一 YAML:
podman compose version
podman compose --help
printf 'provider=%s\n' "${PODMAN_COMPOSE_PROVIDER:-auto-selection}"团队应在 containers.conf 或受控环境入口固定经过验证的 provider 路径和版本,然后保存 config 输出、健康状态、错误传播和 volume 保留结果。下面只创建独立的无状态 Nginx 实验项目;先用 command -v docker-compose 或 command -v podman-compose 找到已安装且批准的 Provider,把其绝对路径赋给 PODMAN_COMPOSE_PROVIDER。在普通用户的 Bash 中运行,不能把其他项目的 compose.yaml 作为实验文件:
(
set -eu
provider="${PODMAN_COMPOSE_PROVIDER:?set an audited provider executable}"
case "$provider" in /*) ;; *) printf 'provider must be an absolute path\n' >&2; exit 23 ;; esac
test -x "$provider"
export PODMAN_COMPOSE_PROVIDER="$provider"
podman compose version
work="$(mktemp -d "${TMPDIR:-/tmp}/te05-compose.XXXXXXXX")"
project="$(basename "$work" | tr '[:upper:].' '[:lower:]-')"
compose() { podman compose -p "$project" -f "$work/compose.yaml" "$@"; }
started=0
cleanup() {
if [ "$started" = 1 ]; then
compose down >/dev/null || printf 'project cleanup failed: %s\n' "$project" >&2
fi
if [ -d "$work" ] && [ ! -L "$work" ] && [ -O "$work" ]; then
rm -f -- "$work/compose.yaml"
rmdir -- "$work" || printf 'retain nonempty project directory: %s\n' "$work" >&2
fi
}
trap cleanup EXIT
cat > "$work/compose.yaml" <<'YAML'
services:
preview:
image: docker.io/nginxinc/nginx-unprivileged:1.28.2-alpine
ports:
- "127.0.0.1::8080"
YAML
compose config
started=1
compose up -d
compose ps
test -n "$(compose ps -q preview)"
compose logs --no-color preview
compose down
test -z "$(compose ps -q preview)"
started=0
printf 'COMPOSE_OK project=%s\n' "$project"
)预期 config 能解析出唯一项目与 preview 服务,ps 在 up 后显示运行实例、down 后不再列出实例,最后打印 COMPOSE_OK。若 ps -q 等选项被所选 Provider 拒绝,就不能把它当成与项目所需 Compose 行为兼容;先核对 Provider 版本和 podman compose --help。端口冲突反例应改在另一份独立夹具中执行,不能为了演示去占用其他项目端口。此实验没有命名卷;真实项目还须单独验证普通 down 后卷仍在、恢复值可读。只有数据 owner 同意重置时才执行 down --volumes。
Quadlet 负责长期服务,不负责掩盖容器参数
Linux 上的 Quadlet 由 systemd generator 生成服务单元。以下仅在当前普通用户的 user systemd 会话验证。先运行 systemctl --user status 确认会话可用,再检查 ss -ltn '( sport = :18087 )' 没有监听。用 install -d -m 700 "$HOME/.config/containers/systemd" 创建目录,并确认 test ! -e "$HOME/.config/containers/systemd/te05-preview.container" 通过;再运行 systemctl --user cat te05-preview.service,预期找不到同名单元。若已有同名对象,改用其他名称和端口,不覆盖既有服务。把下面内容保存为 te05-preview.container。文件名决定生成的 te05-preview.service。Quadlet 基础用法给出了目录、reload 与服务关系,Quadlet 键参考列出 ReadOnly、Tmpfs、NoNewPrivileges 的含义。
[Unit]
Description=Local preview service
[Container]
Image=docker.io/nginxinc/nginx-unprivileged:1.28.2-alpine
ContainerName=te05-preview
PublishPort=127.0.0.1:18087:8080
ReadOnly=true
Tmpfs=/tmp:rw,size=64m,mode=1777
NoNewPrivileges=true
[Service]
Restart=on-failure团队部署可把标签替换成验证过的真实 Digest。实验暂不写 [Install],因此不会让它在下次会话自动启动。以保存文件的同一普通用户运行以下检查;HTTP 失败时仍须停止本轮服务并检查 journal:
(
set -eu
systemctl --user daemon-reload
trap 'systemctl --user stop te05-preview.service || true' EXIT
systemctl --user start te05-preview.service
systemctl --user --no-pager status te05-preview.service
journalctl --user -u te05-preview.service --no-pager
curl -q --noproxy '*' --fail-with-body --silent --show-error \
--max-time 5 http://127.0.0.1:18087/
printf 'QUADLET_HTTP_OK\n'
)
if systemctl --user is-active --quiet te05-preview.service; then
printf 'service remained active after test\n' >&2
exit 23
fi
printf 'QUADLET_STOPPED\n'QUADLET_HTTP_OK 表示 HTTP 内容可取回;QUADLET_STOPPED 表示测试结束后服务不再活跃。停止后用 ss -ltn '( sport = :18087 )' 确认端口释放。只在确认该文件由本次实验创建后,执行 rm -- "$HOME/.config/containers/systemd/te05-preview.container" 与 systemctl --user daemon-reload。若要改为长期随用户会话启动,先核对镜像 Digest、端口、数据卷和身份,再在文件末尾加入 [Install] 与 WantedBy=default.target 并 reload;官方说明指出 Quadlet 生成的 unit 不用 systemctl enable。登录退出后的运行还取决于 linger 策略。不要编辑生成后的 unit。
代理、CA、Registry 与镜像身份
拉取链可能发生在本机 Podman、Podman Machine 内服务、Compose Provider 或容器内进程。宿主 curl 成功不能证明 VM 内的 Registry TLS 成功。取证至少包括:
podman info --debug
podman login --get-login registry.example.com
podman pull "registry.example.com/your-project/app@${IMAGE_DIGEST:?set approved digest}"
podman image inspect "registry.example.com/your-project/app@${IMAGE_DIGEST}"凭证进入系统批准的 auth store,不写入仓库、Containerfile、镜像层或构建日志。企业 CA 分别考虑宿主、Machine/运行时和容器镜像;关闭 TLS 校验只能作为隔离诊断,不能落成团队默认值。
故障先看边界,不先清空存储
| 现象 | 先取证 | 常见边界 | 修复后的反证 |
|---|---|---|---|
普通用户看不到 sudo podman 的镜像 | id、rootless、graphRoot | 两套用户存储 | 两边清单各自可解释,镜像用 Registry 迁移 |
| bind mount 只读或拒绝 | owner、UID map、SELinux AVC | 用户映射或标签 | 非 root 进程可写目标目录,其他目录仍拒绝 |
API _ping 成功但测试库失败 | 客户端日志、失败 API、service 日志 | 兼容字段不足 | 必需 API 夹具全部通过,或明确保留 Moby |
| Compose 在两台机器不同 | provider 路径、版本、config | 外部 provider 漂移 | 固定 provider 后归一化模型相同 |
| Machine 切 rootful 后对象消失 | connection、rootful、两套清单 | 默认连接切到另一存储 | 回切可见,重要制品已推 Registry |
全局 podman system reset 或无范围 prune 会摧毁证据,也可能影响同一用户的其他项目。先用 label、名称、project 和 Digest 缩小对象,再决定停止、删除容器、删除卷还是回收 cache。
升级、交接与退出
Podman 的团队合同至少包含支持平台、安装来源、版本窗口、rootless 策略、Machine 资源、网络 backend、Compose Provider、Socket 策略、Registry/CA、Quadlet owner 和项目清理入口。升级先在代表性 Linux、macOS、Windows、CPU 架构上运行同一正反夹具,再分批推广。
卸载前保存状态:
podman ps -a
podman images --digests
podman volume ls
podman network ls
podman system connection list
podman machine list业务卷先由 owner 导出并做恢复演练;短期凭证随后撤销;Socket 和用户服务停止;Machine 是否删除单独批准。清理只针对明确 project 与 te05-* 夹具,结束后重新运行清单,证明目标对象消失、其他项目仍存在。
如果团队是在 Docker、Podman 与 Rancher Desktop 之间做迁移,不应在这篇产品手册里继续堆另一套产品教程;用替代运行时选型与迁移对照项目事实,再进入对应产品主文。
权威资料与规范地址
安装、平台与运行边界
- Podman 安装说明:https://podman.io/docs/installation
- Podman 命令参考:https://docs.podman.io/en/latest/markdown/podman.1.html
- Podman Machine 参考:https://docs.podman.io/en/latest/markdown/podman-machine.1.html
容器、端口与控制入口
- 容器运行与端口发布:https://docs.podman.io/en/latest/markdown/podman-run.1.html
- 端口映射查询:https://docs.podman.io/en/latest/markdown/podman-port.1.html
- 进程身份显示:https://docs.podman.io/en/latest/markdown/podman-top.1.html
- Podman system service:https://docs.podman.io/en/latest/markdown/podman-system-service.1.html
