了解和使用 Rootless Podman
目录
本文是笔者近日在个人存储服务器使用 Rootless Podman 隔离部署应用程序, 尤其是闭源应用程序的过程中总结的经验 (踩坑体验), 分享在此供读者参考. 我们需要意识到: Podman 由于其无守护程序设计, 天然支持 Rootless 模式. 参考官方文档 https://podman.io/docs/installation. 在 Debian 13 中, 直接通过 apt 安装即可: 完成安装后, 执行 需要注意, 截止至本文写作时, 虽然 Podman 的最新 stable 版本是 5.7.1, 但 Debian 13 官方软件包提供的 Podman 的版本是 5.4.2, 本文所有例子均基于 5.4.2 测试. 在正式开始使用 Podman 前, 我们需要了解一些基本概念. 命名空间是 Linux 提供的一种内核级别环境隔离的机制, 容器化技术重度使用了命名空间机制来实现容器的隔离. 关于命名空间的详细内容此处不再赘述, 读者可以阅读文末的参考文献, 此处仅介绍我们最常打交道的, 也是给普通用户带来最多 “问题” 的用户命名空间和网络命名空间. 不严谨地说, 用户命名空间提供了一种使容器内用户能够映射到宿主机上的(通常是非特权的)用户的机制, 而容器中的 “root” 仅在容器的用户命名空间内具有 “完全” 的权限, 对容器的用户命名空间外的资源的操作权限则被限制为 “映射” 到的 “普通用户” 的操作权限, 正如官方文档所言: “Rootless Podman is not, and will never be, root”. 默认情况下, 通过 Rootless Podman 运行容器时, 容器内的 “root” (UID=0, GID=0) 会被映射为运行容器的用户, 其他 UID (GID) 则映射至宿主机上的高位 UID (GID), 这些高位 UID (GID) 往往并没有对应的宿主机用户(组), 则文件权限按照 “Others” 处理, 拥有的任何文件对象将被视为由 “nobody” (65534, 可以通过 可以看到, 容器内的 UID (GID) 0 被映射为宿主机的 UID (GID) 1000; 从 UID (GID) 1 开始的 65536 个 UID (GID) 逐个被映射为宿主机的 UID (GID) 100000、100001, 以此类推. 这里的映射规则是由 前面提到, 默认情况下容器中的 “root” 被映射为运行容器的用户, 对容器外资源的访问权限与当前用户 “基本” 一致. 参考下面的例子, 相信读者就知道 “映射” 是什么含义了: 如果不幸地, 你的容器进程通过某些安全漏洞逃逸了容器, 那么它也只能以映射到的高位 UID (GID) 的权限 (或者说, “nobody” 的权限) 在宿主机上运行, 这大大降低了容器逃逸后的危害. (TBD.) 网络命名空间负责提供容器内网络与宿主机网络以及其他容器网络的隔离. 默认情况下, 各个容器有其独立的网络命名空间, 而不在同一网络命名空间中的进程无法直接通信. Docker Rootless 和旧版本 Podman 使用 下面是一个例子: (TBD.) (TBD.) 参考文档 https://docs.podman.io/en/latest/markdown/podman-run.1.html#network-mode-net Podman 提供多种网络模式: Rootful Podman 默认, 直接桥接到主机网络接口. 创建网络命名空间, 但是不配置网络, 容器内没有任何网络接口. 加入另一个容器的网络命名空间. (笔者注: 更推荐使用 Pod 管理需要通过网络相互访问的容器.) 复用主机的网络命名空间. 虽然性能很好, 但这也使容器能够完全访问宿主机的 abstract unix socket 以及绑定到 localhost 的 TCP / UDP socket, 存在安全隐患. Rootless Podman 默认, 创建一个新的网络命名空间, 使用 其他模式此处暂不展开, 如有需要参考官方文档. 值得一提, 配置镜像仓库 Podman 默认设置下无法像 Docker 那样不提供镜像源时默认从 建议配置 修改 完成上述配置后, 即可以非 root 用户运行 Podman 容器. 测试如下: 当然, Rootless Podman 的配置和使用由于命名空间等问题有和 Docker 不同的地方, 下面不完整地列举一些. 需要配置 一般来说, 通过 请注意, 每个用户的子 UID (GID) 范围不应重叠, 否则可能导致权限冲突和安全问题. 需要解除 Linux 对非 root 用户绑定 1024 以下端口的限制. 如果需要绑定如 80 / 443 端口的话, 可以通过以下命令解除该限制: 用户配置文件位置和生效顺序问题. 以下三个目录保存了 Podman 的配置文件: 包含以下配置文件: 按前述顺序, 后者配置项覆盖前者. 在 Rootless Podman 中, 在 Rootless Podman 中, 这些字段默认为: 按前述顺序读取. 授权文件 命名空间隔离带来的问题. Rootless Podman 在执行用户命名空间隔离时提供了以下模式, 需要我们了解: “nomap”: 不映射宿主机用户, 容器内的所有 UID (GID) 按照前述映射规则依次映射为宿主机高位 UID (GID). 很多时候, 我们需要将宿主机的某个目录挂载到容器内, 使用 Docker 外加关掉 (或者说根本没开启) SELinux 自然可以靠 “root” 的力量忽视 ( 两种情况: 镜像没有配置 此时, 默认以容器内的 “root” (UID=0, GID=0) 运行. 容器内的 “root” 在 Rootless Podman 默认的用户命名空间模式 (“–userns=host”) 下如前所述被映射到宿主机下运行容器的当前用户, 文件系统权限表现和当前用户基本一致. 当镜像遵循最佳实践配置了 (rootless) 此时, 容器内进程以容器内的该 UID (GID) 运行, 而容器内 UID (GID) 在 Rootless Podman 默认的用户命名空间模式 (“–userns=host”) 下如前所述被映射到宿主机下的高位 UID (GID), 此时文件系统权限表现和宿主机上的 “Others” 一样了. 实例: 在这个案例中, “/entrypoint.sh” 是以容器内的 UID=1001、GID=1001 运行的, 被映射到宿主机下的 UID=101000、GID=101000. 显然, 其无法访问权限为 600, 归属 UID=1000、GID=1000 的的宿主机目录 当然, 此时非 init 进程还是以 USER 配置的 UID (GID) 运行的: 为了避免编辑配置文件还得 这个时候, 容器内的 init 进程和非 init 进程在宿主机上均(表现为)以当前用户 “降权” 运行, 文件系统权限表现和当前用户基本一致. Capabilities 关于 Capabilities, 参考 https://man7.org/linux/man-pages/man7/capabilities.7.html, 暂且理解为对权限控制的细化. 无论是 Docker 还是 Podman, 都限制了容器内进程的 Capabilities, 即便是 “root”. Podman 的默认 Capabilities 和 Docker 的默认 Capabilities 是不同的: 在容器化现有应用程序时时, 需要注意两者的差异; 对终端用户来说没有什么直接影响, 但建议启动容器时配置 总体和编写普通 Dockerfile 没什么区别, 但需要注意以下几点: 指定 rootless 推荐: 尽量使用 Distroless 镜像作为基础镜像. 如果应用程序是完全静态编译的, 直接使用 不同语言编写的项目的静态编译方法参考: 对于 C / C++, 具体项目具体分析. 对于 Go, 可以配置 对于 Rust, 可以配置 target 一般来说, 能编译为 … 如果应用程序依赖于 glibc, 可以使用 一些问题: 尝试运行容器时, 报告 “error while loading shared libraries: XXX: cannot open shared object file: No such file or directory” 错误. 此即缺失依赖库问题. 这种情况下, 使用 ldd 查看依赖库, 再参考相关库的安装方法直接从完整镜像复制文件即可, 如笔者在打包 Resilio Sync 时的做法: (笔者评: Resilio Sync 怎么还在用 OpenSSL 1.1.X 啊, 硬要用就不能静态编译进程序里面么… C / C++ 的依赖灾难在这就体现了…) OpenSSL CA 问题. 但凡最终应用程序报告 SSL 相关错误, 大抵是这里所述的问题. 这种情况下, 可以在构建阶段安装 ca-certificates 包, 将 CA 文件复制到最终镜像中, 并正确配置环境变量, 如笔者在打包 qBittorrent 时的做法: (笔者评: 还是喜欢 Rustls + WebPKI, OpenSSL 这种岁月痕迹过于重的库, 唉…) 参考文档: https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html Podman 不像 Docker 那样有守护程序, 需要以其他方式管理容器的自动启动等. Podman 提供了 Quadlet, 以通过 systemd 来管理 Podman 容器. 下面是一个示例 Quadlet 文件: 基本上, Podman Quadlet systemd 文件可以由 上面的例子基本上相当于: 配置完成后, 可以通过 值得指出, 依托于 systemd, 像什么 当然, 还有些注意事项和疑难杂症: 自动启动 笔者本人在部署过程中遇到 根本原因暂未清楚, 或许是 其他需要注意的是, 从 Podman v5 开始, 总的来说, 如果不想折腾, 直接使用 rootful Docker 确实方便, root 解决一切权限问题, 但面对 “飞牛变肥牛” 之类由于厂商漠视安全、产品存在大量安全漏洞不积极修复放任被在野攻击者利用的教训, 对于闭源应用或未得到广泛安全审计的开源应用, 还是建议使用 Rootless Podman 容器化隔离, 并配置 CPU 和内存用量限制; 即便是开源应用, 也建议使用 Rootless Podman 来隔离部署, 以防止由于配置错误或未知漏洞导致的安全问题. 本文是笔者近日在个人存储服务器使用 Rootless Podman 隔离部署应用程序, 尤其是闭源应用程序的过程中总结的经验 (踩坑体验), 分享在此供读者参考, 相关成果开源在以下仓库中: 由于笔者水平和精力有限, 文章中难免出现纰漏, 欢迎评论区批评指正、交流分享. 官方文档, 包括但不限于:导语
--privileged, 安全性直接归零.安装 Podman
podman info 可以查看 Podman 的详细配置信息.Rootless Podman 基本概念
命名空间 (Namespace)
用户命名空间
kernel.overflowuid) 拥有.podman-unshare 工具创建并进入一新用户命名空间, 以便捷地验证上述映射关系 (值得一提, 在调试 Rootless Podman 的权限问题时我们会经常和这个工具打交道). 如:> id
uid=1000(hantong) gid=1000(hantong) groups=1000(hantong),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev)
> podman unshare
> id
uid=0(root) gid=0(root) groups=0(root),65534(nogroup)
> cat /proc/self/uid_map
0 1000 1
1 100000 65536
> cat /proc/self/gid_map
0 1000 1
1 100000 65536
> exit
/etc/subuid 和 /etc/subgid 文件定义的, 通常在使用 adduser (addgroup) 添加用户(组)时会自动设置:> cat /etc/subuid
hantong:100000:65536
> cat /etc/subgid
hantong:100000:65536
> ls -ildn /home/hantong
143631 drwx------ 17 1000 1000 4096 Feb 8 21:53 /home/hantong
> podman unshare ls -ildn /home/hantong
143631 drwx------ 17 0 0 4096 Feb 8 21:53 /home/hantong
> podman unshare touch /tmp/test
> podman unshare ls -iln /tmp/test
3432 -rw-r--r-- 1 0 0 0 Feb 8 21:54 /tmp/test
> ls -iln /tmp/test
3432 -rw-r--r-- 1 1000 1000 0 Feb 8 21:54 /tmp/test
网络命名空间
slirp4netns 用户态网络驱动来实现 Rootless 网络, 性能较差; Podman v5 以后, 默认使用 “性能更优” 的 pasta 代替 slirp4netns. 相对于 slirp4netns, pasta 的特点:pasta 支持 IPv6;pasta 使用主机中的接口名称, 从主机复制 IP 地址并使用主机中的网关地址.> podman unshare --rootless-netns ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
# ...
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 65520 qdisc fq state UNKNOWN group default qlen 1000
link/ether 52:3a:d5:14:43:4c brd ff:ff:ff:ff:ff:ff
inet 192.168.1.200/24 brd 192.168.1.255 scope global eth0
valid_lft forever preferred_lft forever
# ...
> podman unshare ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
# ...
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq state UP group default qlen 1000
link/ether bc:24:11:46:30:a6 brd ff:ff:ff:ff:ff:ff
altname enp6s18
altname enxbc24114630a6
inet 192.168.1.200/24 brd 192.168.1.255 scope global eth0
valid_lft forever preferred_lft forever
# ...
3: lan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq state UP group default qlen 1000
link/ether bc:24:11:40:61:e6 brd ff:ff:ff:ff:ff:ff
altname enp6s19
altname enxbc24114061e6
inet 172.16.0.2/24 brd 172.16.0.255 scope global lan0
valid_lft forever preferred_lft forever
# ...
> curl http://127.0.0.1:1111/ -I
HTTP/1.1 200 OK
content-type: text/html
content-length: 12252
date: Mon, 09 Feb 2026 05:11:24 GMT
> podman unshare --rootless-netns curl http://127.0.0.1:1111/ -I
curl: (7) Failed to connect to 127.0.0.1 port 1111 after 0 ms: Could not connect to server
存储驱动
网络
bridge (桥接).nonecontainer:idhostpasta[:OPTIONS,…]pasta 创建用户态网络堆栈.pasta 的性能对比于直接使用 host 模式仍然有不小的差距. 笔者在个人的存储服务器上进行了简要测试: TCP 吞吐量由于 splice 的使用下降幅度不大, 即便是重负载仍然能有 host 模式下八九成; UDP 吞吐量则受机器负载影响大, 笔者测试时, 遇到机器本身处于重负载状态, UDP 吞吐量最差可能暴跌至 host 模式下的两三成. 造成该问题的原因大抵是 Linux 内核本来对 UDP 优化就差, 缺乏 TCP splice 那种零拷贝的机制; 此外 pasta 应该也没用上 UDP GSO / GRO, 浪费大量 CPU 时间, 在 CPU 受限时自然性能下降.配置 Podman
docker.io 拉取镜像.registries.conf 文件:> sudo nano /etc/containers/registries.conf
unqualified-search-registries 字段, 添加 docker.io: ["docker.io"]
运行 Rootless Podman 容器
> podman run hello
!... Hello Podman World ...!
.--"--.
/ - - \
/ (O) (O) \
~~~| -=(,Y,)=- |
.---. /` \ |~~
~/ o o \~~~~.----. ~~
| =(X)= |~ / (O (O) \
~~~~~~~ ~| =(Y_)=- |
~~~~ ~~~| U |~~
Project: https://github.com/containers/podman
Website: https://podman.io
Desktop: https://podman-desktop.io
Documents: https://docs.podman.io
YouTube: https://youtube.com/@Podman
X/Twitter: @Podman_io
Mastodon: @Podman_io@fosstodon.org
/etc/subuid 和 /etc/subgid 文件.useradd 命令创建用户时, 系统会自动为新用户在 /etc/subuid 和 /etc/subgid 中分配 UID 和 GID 范围; 如果是手动创建用户, 则需要手动添加相应条目. 例如, 为用户 hantong 分配 UID 和 GID 范围:
/usr/share/containers/etc/containers${XDG_CONFIG_HOME}/containers (默认 ~/.config/containers)containers.confstorage.conf/etc/containers/storage.conf 中的某些字段将被忽略:""
storage graph dir (default: "/var/lib/containers/storage")
directory to store all writable content created by container storage programs.
""
storage run dir (default: "/run/containers/storage")
directory to store all temporary writable content created by container storage programs.
"${XDG_DATA_HOME}/containers/storage"
"${XDG_RUNTIME_DIR}/containers"
XDG_DATA_HOME 默认为 ~/.local/share, XDG_RUNTIME_DIR 默认为 /run/user/<uid> (systemd).registries.confpodman login 和 podman logout 命令使用的默认授权文件是 ${XDG_RUNTIME_DIR}/containers/auth.json.host (默认): 映射当前用户的 UID (GID) 到容器内的 “root” (UID=0, GID=0), 其余 UID (GID) 按照前述映射规则依次映射为宿主机高位 UID (GID).> podman run -it --rm --userns=host --name="test" --replace debian:trixie-slim bash
root@0df58777f431:/# id
uid=0(root) gid=0(root) groups=0(root)
root@0df58777f431:/# sleep 3000
^C
root@0df58777f431:/# useradd test
root@0df58777f431:/# su test
$ id
uid=1000(test) gid=1000(test) groups=1000(test)
$ sleep 3000
^C
$ exit
root@0df58777f431:/# exit
exit
# 以容器内 root 用户 (UID=0) 运行 sleep 时
> ps -ax -o pid,user,group,uid,gid,args | grep "sleep 3000"
20953 hantong hantong 1000 1000 sleep 3000
# 以容器内 test 用户 (UID=1000) 运行 sleep 时
> ps -ax -o pid,user,group,uid,gid,args | grep "sleep 3000"
21384 100999 100999 100999 100999 sleep 3000
keep-id: 类似 host, 但是映射当前用户的 UID (GID) 到容器内的相同 UID (GID). 同时, 除非手动指定用户, 忽视镜像的 USER 配置, 在当前用户的 UID 下运行 init 进程.> podman run -it --rm --userns=keep-id --name="test" --replace debian:trixie-slim bash
hantong@cea54e0fd3fb:/$ id
uid=1000(hantong) gid=1000(hantong) groups=1000(hantong)
hantong@cea54e0fd3fb:/$ exit
exit
> id
uid=1000(hantong) gid=1000(hantong) groups=1000(hantong),27(sudo)
> podman run -it --rm --userns=nomap --name="test" --replace debian:trixie-slim bash
root@f3794e591a2c:/# id
uid=0(root) gid=0(root) groups=0(root)
root@f3794e591a2c:/# sleep 3000
^C
root@f3794e591a2c:/# useradd test
root@f3794e591a2c:/# su test
$ sleep 3000
^C
$ exit
root@f3794e591a2c:/# exit
exit
# 以容器内 root 用户 (UID=0) 运行 sleep 时
> ps -ax -o pid,user,group,uid,gid,args | grep "sleep 3000"
28326 100000 100000 100000 100000 sleep 3000
# 以容器内 test 用户 (UID=1000) 运行 sleep 时
> ps -ax -o pid,user,group,uid,gid,args | grep "sleep 3000"
28563 101000 101000 101000 101000 sleep 3000
暴力解决) 权限问题, 但 Rootless Podman 就不行了.USER:USER=UID[:GID]:> podman run -d --restart=unless-stopped -v /home/hantong/.config/openlist:/opt/openlist/data -p 5244:5244 --name="openlist" --replace openlistteam/openlist:latest
> podman logs openlist
Error: Current user does not have write and/or execute permissions for the ./data directory: /opt/openlist/data
# ...
# ...
ARG USER=openlist
ARG UID=1001
ARG GID=1001
# ...
RUN addgroup -g ${GID} ${USER} && \
adduser -D -u ${UID} -G ${USER} ${USER} && \
mkdir -p /opt/openlist/data
# ...
USER ${USER}
# ...
CMD [ "/entrypoint.sh" ]
/home/hantong. 此时, 我们可以配置 Podman 的用户命名空间模式为 “keep-id”, 以使 init 进程 “以当前用户的 UID (GID) 运行”, 从而对 /home/hantong 目录具有访问权限, 如:> podman run -d --userns=keep-id --restart=unless-stopped -v /home/hantong/.config/openlist:/opt/openlist/data -p 5244:5244 --name="openlist" --replace openlistteam/openlist:latest
> podman logs openlist
INFO[2026-02-08 07:14:43] reading config file: /opt/openlist/data/config.json
INFO[2026-02-08 07:14:43] load config from env with prefix:
INFO[2026-02-08 07:14:43] max buffer limit: 1502MB
INFO[2026-02-08 07:14:43] mmap threshold: 4MB
INFO[2026-02-08 07:14:43] init logrus...
start HTTP server @ 0.0.0.0:5244
> ps -ax -o pid,user,group,uid,gid,comm | grep openlist
31334 101000 101000 101000 101000 openlist
> podman exec -it openlist bash
695025dade25:/opt/openlist$ id
uid=1001(openlist) gid=1001(openlist) groups=1001(openlist)
695025dade25:/opt/openlist$ ps -o pid,user,group,comm | grep openlist
1 openlist openlist openlist
26 openlist openlist bash
28 openlist openlist ps
29 openlist openlist grep
695025dade25:/opt/openlist$ exit
exit
> ls -aln /home/hantong/.config/openlist
total 276
drwxrwxrwx 4 1000 1000 4096 Jan 31 16:36 .
drwxrwxr-x 14 1000 1000 4096 Jan 31 16:23 ..
-rw-r--r-- 1 101000 101000 2899 Feb 8 15:14 config.json
-rw-r--r-- 1 101000 101000 4096 Jan 31 16:28 data.db
-rw-r--r-- 1 101000 101000 32768 Feb 8 15:14 data.db-shm
-rw-r--r-- 1 101000 101000 222512 Feb 8 15:14 data.db-wal
drwxr--r-- 2 101000 101000 4096 Jan 31 16:28 log
drwxr-xr-x 2 101000 101000 4096 Jan 31 16:28 temp
sudo 的麻烦, 建议配置为 --userns=keep-id:uid=${UID},gid=${GID}, 其中 UID, GID 为构建镜像时所配置的用户的数字 ID (或者直接指定用户覆盖镜像设置), 如:> podman run -d --userns=keep-id:uid=1001,gid=1001 --restart=unless-stopped -v /home/hantong/.config/openlist:/opt/openlist/data -p 5244:5244 --name="openlist" --replace openlistteam/openlist:latest
4fd47db9916432a8d4631b99c6a6fedd9a9c94c8ca3580e40f0f6bde41b83102
> ls -aln /home/hantong/.config/openlist
total 260
drwxrwxr-x 4 1000 1000 4096 Feb 8 16:13 .
drwxrwxr-x 14 1000 1000 4096 Feb 8 16:13 ..
-rw-r--r-- 1 1000 1000 2842 Feb 8 16:13 config.json
-rw-r--r-- 1 1000 1000 4096 Feb 8 16:13 data.db
-rw-r--r-- 1 1000 1000 32768 Feb 8 16:13 data.db-shm
-rw-r--r-- 1 1000 1000 206032 Feb 8 16:13 data.db-wal
drwxr--r-- 2 1000 1000 4096 Feb 8 16:13 log
drwxr-xr-x 2 1000 1000 4096 Feb 8 16:13 temp
Capability Podman Docker CHOWN ✔️ ✔️ DAC_OVERRIDE ✔️ ✔️ FSETID ✔️ ✔️ FOWNER ✔️ ✔️ MKNOD ❌ ✔️ NET_RAW ❌ ✔️ SETGID ✔️ ✔️ SETUID ✔️ ✔️ SETFCAP ✔️ ✔️ SETPCAP ✔️ ✔️ NET_BIND_SERVICE ✔️ ✔️ SYS_CHROOT ✔️ ✔️ KILL ✔️ ✔️ AUDIT_WRITE ❌ ✔️ --security-opt=no-new-privileges 以禁止容器进程获得额外权限, 或者直接禁用所有 Capabilities (--cap-drop=all) 再按需添加 (--cap-add=CAP_NAME), 防止类似 CAP_BPF 这些后面才引入的高危 Capabilities 被意外添加.编写 Rootless 友好的 Dockerfile
USER, 需要是数字 ID.USER=65532:65532.scratch (适合现代应用程序, 没有奇奇怪怪的依赖) 或 gcr.io/distroless/static (适合传统应用程序, 仍然依赖系统 CA 之类的奇怪玩意).CGO_ENABLED=0, 如果项目以及项目的依赖没有依赖 CGO 的话.x86_64-unknown-linux-musl (amd64) 或 aarch64-unknown-linux-musl (arm64) 目标平台进行静态编译.> rustup target add x86_64-unknown-linux-musl
# ...
> cd /tmp
> cargo new test-static-build
Creating binary (application) `test-static-build` package
note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
> cd test-static-build
> cargo build --release --target x86_64-unknown-linux-musl
Compiling test-static-build v0.1.0 (/tmp/test-static-build)
Finished `release` profile [optimized] target(s) in 1.22s
> ldd /data/Compile/cargo-target-dir/x86_64-unknown-linux-musl/release/test-static-build
statically linked
x86_64-unknown-linux-gnu 就能编译为 x86_64-unknown-linux-musl, 除非依赖到某些奇奇怪怪的 C 库.gcr.io/distroless/base-nossl; 如果应用程序还依赖于 OpenSSL (v3.X), 可以使用 gcr.io/distroless/base.# ...
ARG DEBIAN_BASE_VERSION=trixie-slim
ARG DEBIAN_BASE_HASH=f6e2cfac5cf956ea044b4bd75e6397b4372ad88fe00908045e9a0d21712ae3ba
# ...
FROM --platform=linux/${TARGETARCH} debian:${DEBIAN_BASE_VERSION}@sha256:${DEBIAN_BASE_HASH} AS downloader
# ...
RUN set -e && \
# ...
&& \
ARCH=$(dpkg --print-architecture) \
&& \
case "${ARCH}" in \
amd64) LIBARCH="x86_64-linux-gnu" ;; \
arm64) LIBARCH="aarch64-linux-gnu" ;; \
*) echo "Unsupported architecture: ${ARCH}" && exit 1 ;; \
esac \
&& \
cp /lib/${LIBARCH}/libcrypt.so.1 /tmp/resilio-sync/lib/libcrypt.so.1
# ...
FROM scratch
# ...
ENV LD_LIBRARY_PATH=/opt/resilio-sync/lib
# ...
# ...
ARG ALPINE_BASE_VERSION=3.23.3
ARG ALPINE_BASE_HASH=25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
# ...
FROM alpine:${ALPINE_BASE_VERSION}@sha256:${ALPINE_BASE_HASH} AS downloader
# ...
RUN set -e && \
apk -U upgrade && apk add --no-cache \
ca-certificates=20251003-r0 \
# ...
FROM scratch
# ...
COPY --from=downloader /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
# ...
ENV SSL_CERT_DIR=/etc/ssl/certs \
SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
# ...
使用 systemd 管理 Rootless Podman 容器
[Unit]
Description=FreeNGINX Distroless Container Service
RequiresMountsFor=%t/containers
[Container]
# * Specify the name of the container.
ContainerName=freenginx
# * Specify the container image to use.
Image=ghcr.io/han-rs/container-ci-freenginx:latest
# * Automatically pull the latest image.
AutoUpdate=registry
# * Run the container in user namespace mode with the current user's ID.
# *
# * Don't modify this field unless you know what you're doing.
UserNS=keep-id:uid=65532,gid=65532
# * Use the host's network stack for better performance.
Network=host
# * Publish ports from the container to the host.
# *
# * If Network=host is used, these options will be ignored.
# * The format is [host_ip:]host_port:container_port[/protocol].
# * Please set these according to what you configured.
# PublishPort=80:80
# PublishPort=443:443
# PublishPort=443:443/udp
# * Persists configuration files.
Volume=%h/.local/share/freenginx/conf:/opt/freenginx/conf
# * Persists logs.
Volume=%h/.local/share/freenginx/logs:/opt/freenginx/logs
# * Persists SSL stuffs.
Volume=%h/.local/share/freenginx/ssl:/opt/freenginx/ssl
# * Example of mounting a named volume.
# Volume=cloudflared.volume:/opt/cloudflared/shared-volume
# * Example of mounting a host directory.
# Volume=/host/dir:/container/dir
[Service]
# * Always restart the container if it stops.
Restart=always
# * Give the container more time to start.
TimeoutStartSec=900
# * Generate configuration extracting real IP from HTTP headers added by Cloudflare.
#
# * If you gate your servers behind Cloudflare, uncomment the following line.
# ExecStartPre=%h/.local/share/freenginx/scripts/cloudflare-real-ip-helper.sh
# * For Podman v5.4 or lower, we have to manually specify reload command.
ExecReload=/usr/bin/podman exec freenginx /opt/freenginx/sbin/nginx -s reload
[Install]
# * Enable the service to start at boot.
WantedBy=default.target
podman run 命令逐参数转换而来, 官方文档也给出了对照表, 编写并不困难.
systemctl --user ${CMD} ${CONTAINER_NAME}.service 来管理该容器, 如:> systemctl --user status freenginx.service
● freenginx.service - FreeNGINX Distroless Container Service
Loaded: loaded (/home/hantong/.config/containers/systemd/freenginx.container; generated)
Active: active (running) since Sat 2026-02-07 19:29:12 CST; 21h ago
Invocation: eda008ef44dc4e79b198a0c69a7ca32f
Process: 25667 ExecStartPre=/home/hantong/.local/share/freenginx/scripts/cloudflare-real-ip-helper.sh (code=exited, status=0/SUCCESS)
Main PID: 25696 (conmon)
Tasks: 134 (limit: 4915)
Memory: 467M (peak: 3.6G)
CPU: 16min 28.154s
# ...
ExecStartPre 之类的钩子也可以使用了, 不再依赖容器本身去实现这些功能, 这允许容器打包者大幅度简化镜像:> podman images
REPOSITORY TAG IMAGE ID CREATED SIZE
ghcr.io/han-rs/container-ci-freenginx latest f583dc3c0362 28 hours ago 3.95 MB
ghcr.io/han-rs/container-ci-resilio-sync latest af6cae8d5f91 47 hours ago 38.3 MB
ghcr.io/han-rs/container-ci-qbittorrent latest 2bf49e5c41e5 2 days ago 45.5 MB
ghcr.io/han-rs/container-ci-cloudflared latest a4bbf0388c6b 2 days ago 28 MB
# ...
sudo loginctl enable-linger $USER, 使得 rootless 用户的 systemd 服务能够在用户未登录时也能自动启动并驻留后台.systemctl --user enable ***, Podman 创建的服务被 systemd 视为 “瞬态” (transient) 的.[Install] 项添加 WantedBy=default.target 使容器自动启动. (存疑: 是 default.target 还是 multi-user.target, 还是两者均使用? 笔者个人部署时仅使用前者.)network-online.target 始终没有 active (systemctl is-active network-online.target 提示 inactive) 的问题, 导致:netplan + systemd-networkd 的问题? 等待进一步研究.podman generate systemd 已经被弃用, 不再推荐使用.结语
参考文献