ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Podman容器内运行systemd:以Rocky 9跑VNC服务全攻略

Podman容器内运行systemd:以Rocky 9跑VNC服务全攻略 前几天帮一个朋友把他那台跑在Rocky 9上的VNC服务迁进Podman容器我一开始觉得这就是个常规操作拉镜像、起容器、映射端口完事。结果真正动手才发现最折腾的根本不是VNC本身而是让容器里的systemd老老实实把服务拉起来。那段时间我翻了不少文档试了各种参数组合最后把Podman的systemd机制、cgroup v2挂载、Quadlet声明式配置这些脉络串在一起才算彻底跑通。这篇内容就把我折腾完之后的完整配置思路记录下来包括运行参数怎么选、两种容器内启动systemd的方式、宿主机systemd托管容器的方案以及几个高频报错的排查过程。适合想把Rocky、Ubuntu这类完整系统镜像当轻量虚拟机来用的人尤其是需要在容器里跑sshd、VNC、MySQL这类以systemd unit方式管理的服务时这份配置可以直接参考。1. 为什么容器里非要有systemd1.1 容器PID 1的职责和systemd的特殊地位容器本质上一组受限的进程PID 1通常是镜像指定的启动命令它承担着整个容器进程树的祖师爷角色。这个进程要负责回收僵尸进程、转发终止信号还要保证容器退出时所有子进程能干净地结束。很多人图省事直接用bash、sleep infinity甚至一个空壳脚本当PID 1这样短期跑个测试没问题但一旦容器里挂了好几个服务就会出现子进程变僵尸没人收、kill容器时信号发不进去这类怪毛病。systemd在宿主机上是所有进程的大管家放进容器里效果也一样。它能在容器内管理服务依赖、自动重启崩溃的服务、用journald集中收日志还能通过socket激活按需启动服务。把systemd作为容器PID 1之后整个容器用起来就像一个独立的小服务器——你熟悉的那套systemctl start、systemctl status、journalctl操作全部照常服务配置文件也能沿用发行版默认的那套迁移成本很低。这里得承认Podman在设计之初就对systemd特别友好项目核心团队本身就深度参与systemd生态所以在Podman容器里跑systemd的成熟度比Docker高出不少。用Docker跑systemd容器经常要额外挂载cgroup、手动处理/run目录而Podman内置了专门的systemd运行模式很多细节自动帮你处理掉了。1.2 什么时候需要什么时候根本不需要我见过不少人一上来就给所有容器加--systemdalways这其实是过度设计。如果你的容器只是跑一个前端Nginx进程、一个Python脚本、一个Node服务那完全没必要引入systemd直接让应用进程当PID 1反而更简单日志就是标准输出podman logs直接能看。真正需要容器内systemd的场景通常有这么几类迁移现成的系统服务。比如原来跑在Rocky 9物理机上的VNC、sshd、MySQL配置里全是systemd unit直接搬进容器还能沿用原有的启动配置。容器内需要跑多个进程组合。比如既要VNC又要桌面环境还要个HTTP服务彼此之间还有依赖关系用systemd管起来比在启动脚本里手工控制顺序靠谱得多。搭建测试/CI环境。想模拟一台完整虚拟机校验服务是否能通过systemd正常拉起、开机自启这种场景下systemd几乎是必需品。判断标准很简单你需不需要在容器里执行systemctl命令如果不需要就别加。加了systemd会额外占掉100MB左右内存还会让容器日志体系复杂化收益和成本要匹配。2. 动手前的基础环境自查2.1 内核cgroup必须是v2想顺利在Podman容器内跑通systemd第一个前提是宿主机内核启用了cgroup v2。systemd从238版本开始就把cgroup v2作为标准运行环境v1下能跑但各种功能受限报错也让人摸不着头脑。检查方法很简单stat -fc %T /sys/fs/cgroup输出是cgroup2fs就说明是v2如果是tmpfs就是v1。绝大多数现代发行版比如Rocky 9、Ubuntu 22.04、Debian 12默认已经是v2了。如果你还在用v1的旧系统要么在grub内核参数里加systemd.unified_cgroup_hierarchy1然后重启要么干脆升级发行版别在上面浪费时间。同时确认Podman版本最好4.4以上podman --version老版本Podman对systemd模式的支持不够完善我早期在1.x版本上测过各种cgroup挂载问题能让人崩溃。4.x之后稳定很多后面要讲的Quadlet也是4.4才正式支持的版本低了建议先升。2.2 containers.conf中的关键配置Podman的全局配置集中在/etc/containers/containers.conf用户级配置在~/.config/containers/containers.conf。和systemd模式相关的核心选项是cgroup_manager默认值已经是systemd了但有些从旧版本升级上来的机器可能还是cgroupfs需确认一下[engine] cgroup_manager systemdcgroup_manager这一项决定了Podman怎么管理容器的cgroup是直接写cgroupfs还是通过systemd的DBus接口来创建。对容器内的systemd来说宿主机的cgroup管理方式必须也是systemd否则嵌套的时候会碰到cgroup路径冲突典型症状就是容器内systemd启动时报Cannot determine cgroup we are running in。另外在[containers]段里可以设置sysctl、ulimit等和systemd相关的基本就cgroup_manager一个。我没把systemd always写进全局配置因为不是所有容器都需要systemd模式全局开了反而会拖慢启动速度。建议按容器单独传参数保持每个容器的行为明确可预期。2.3 关于rootless模式的一个提醒Podman支持rootless运行也就是普通用户不借助root权限直接跑容器。但容器内systemd对cgroup、挂载、DBus权限要求比较多rootless模式下限制相对明显。虽然现代Podman已经能在rootless下跑systemd容器但遇到问题时很难排查因为它涉及用户级systemd的cgroup delegation多个用户、多个容器之间的资源隔离边界也比较模糊。我的建议是如果你没特别强烈的安全隔离要求跑systemd容器直接用rootful模式也就是root用户执行podman命令省心。rootless模式更适合单进程、无systemd需求的场景两者不需要混用。3. 核心参数与运行方式详解3.1 --systemd参数到底在干什么Podman的--systemd参数有三个值这也是最容易让人迷惑的地方参数值行为true容器PID 1如果是systemd自动启用systemd模式如果不是不影响正常运行always无条件启用systemd模式即使PID 1不是systemd也会尝试false完全关闭systemd模式容器不挂载systemd需要的特殊目录所谓systemd模式Podman会在容器启动时自动做这几件事把/run挂载成tmpfs保证systemd运行时需要的临时目录可写把/dev/shm挂载成合适大小的tmpfs创建/run/.containerenv标记文件调整cgroup路径好让容器内systemd能正常识别自身所在cgroup。这里有个细节--systemdtrue是Podman的默认值。默认情况下Podman启动时会检测镜像的PID 1是否是systemd如果是就自动切换模式。那为什么还有那么多人在容器里跑不起systemd多半是镜像本身没有把systemd作为默认启动命令比如Ubuntu的官方镜像启动命令是bash那Podman检测不到systemd自然不启用特殊模式。解决方法是显式指定--systemdalways再指定启动命令为/sbin/init或/lib/systemd/systemd。我习惯在跑Rocky/CentOS/Ubuntu这类系统镜像时直接写--systemdalways强制开启不依赖镜像的默认行为。3.2 权限要不要上--privileged很多教程喜欢一刀切容器里跑systemd直接--privileged。这个参数等于把宿主机的大部分权限直接交给容器安全隔离基本形同虚设。我不推荐作为默认方案尤其容器要暴露到网络上时更得谨慎。systemd正常运行需要的权限其实可以精确控制主要就是几个capabilitypodman run -d \ --name rocky-sys \ --systemdalways \ --cap-addSYS_ADMIN \ --cap-addSYS_PTRACE \ --cap-addAUDIT_WRITE \ --security-opt seccompunconfined \ -p 5901:5901 \ rockylinux:9SYS_ADMIN用来处理挂载相关操作SYS_PTRACE给systemd调试追踪子进程的能力AUDIT_WRITE是内核审计接口。seccompunconfined是绕开容器默认seccomp对部分系统调用的拦截systemd在启动早期会执行很多底层系统调用来探测环境。用这套组合跑systemd基本足够比--privileged风险小得多。真遇到某些服务还需要额外权限比如要加载内核模块、操作网络设备再按需加对应参数而不是一上来就放弃全部隔离。3.3 容器内cgroup的设置容器内cgroup这一项Podman有三个取值enabled、disabled、private。和systemd模式配合时默认的private最合适。它表示Podman为容器创建一个独立的cgroup容器内systemd看到的是一个干净且属于自己的cgroup子树可以在里面自由地创建子cgroup来管理服务。如果设置成enabled意思是容器共享父级cgroup容器内systemd再创建子cgroup时会和宿主机的服务混在一起容易冲突。disabled则完全不挂载cgroupfs容器内systemd基本没法工作连systemctl status都跑不了。所以这个参数一般不用改保持默认就行。如果你改过全局配置注意检查一下podman info | grep cgroup看到Manager: systemd和下面对应的cgroup版本说明环境是符合预期的。4. 实操记录在Rocky 9容器里跑通systemd和VNC4.1 方式一直接运行官方镜像验证systemd环境最省事的路径是直接跑RockyLinux官方镜像这个镜像默认启动命令就是/sbin/init也就是systemd本体podman run -d --name rocky-sys --systemdalways rockylinux:9启动后进容器验证podman exec -it rocky-sys systemctl is-system-running正常情况下会输出degraded或running。degraded不算异常表示有部分服务没起来但systemd本身活着。再随便看几个unitpodman exec rocky-sys systemctl list-units --typeservice --staterunning虽然Rocky 9镜像是最小化安装systemd起来了之后我们要自己装目标服务。这里以VNC服务为例毕竟Rocky 9里VNC的启动方式就是个典型变化——vncserver命令已被标记为legacy官方推荐用systemd unit方式管理# 在宿主机上执行进入容器shell podman exec -it rocky-sys bash # 容器内执行 dnf install -y tigervnc-server mkdir -p ~/.vnc echo 123456 | vncpasswd -f ~/.vnc/passwd chmod 600 ~/.vnc/passwd然后启动VNC服务systemctl start vncserver:1 systemctl status vncserver:1这里vncserver:1就是Rocky 9中替代旧vncserver命令的systemd模板unit冒号后面的:1表示显示编号对应5901端口。能通过systemctl正常拉起VNC就说明容器内systemd已经完全接管了服务管理整个体系是通的。整个过程有个细节值得留意如果容器不是用systemd作为PID 1直接在容器里执行systemctl start vncserver:1会报错System has not been booted with systemd as init system。这个报错本身就是判断systemd模式是否生效的试金石。4.2 方式二用Dockerfile固化镜像如果这套环境要反复复用每次手动安装太浪费时间。更合理的方式是写一个Dockerfile把systemd和服务一起固化进镜像FROM rockylinux:9 RUN dnf install -y tigervnc-server openssh-server \ dnf clean all RUN systemctl enable sshd vncserver:1 2/dev/null || true EXPOSE 22 5901 CMD [/sbin/init]构建并运行podman build -t rocky-sys-image . podman run -d --name rocky-sys \ --systemdalways \ --cap-addSYS_ADMIN --cap-addSYS_PTRACE --cap-addAUDIT_WRITE \ --security-opt seccompunconfined \ -p 22:22 -p 5901:5901 \ rocky-sys-image构建镜像时那个2/dev/null || true是经验之谈很多systemd enable命令在构建阶段因为缺少完整运行环境会报错但其实服务在运行时能被正常拉起。构建时保持宽松运行环境里再验证真实状态即可。CMD改写成[/sbin/init]这步很关键。如果用默认镜像Rocky官方镜像本身CMD也是/sbin/init但自己定制时经常会有其他启动脚本如果启动命令不是systemd前面所有systemd模式设置都白搭。4.3 验证完整运行状态容器运行后从宿主机这一侧也可以看到容器内systemd的进程ps aux | grep -E systemd|vncserver能看到容器内systemd进程的PPID是宿主机上的某个Podman进程层级清晰。日志方面容器内journald会把systemd及其管理服务的日志记录在自己的journal里podman exec rocky-sys journalctl -u vncserver:1 -n 50这里也能看到VNC服务被systemd正确托管没有红色的fail状态启动流程才算完整走通。如果vncserver在容器里能正常监听5901端口外面通过VNC客户端连过去也就水到渠成。5. 进阶宿主机systemd托管Podman容器5.1 老的podman generate systemd方案容器内的systemd解决的是容器内部服务谁来管但容器本身开机自启、崩溃后自动拉起还缺一个宿主机的管家。最早的做法是podman generate systemdpodman generate systemd --new --name rocky-sys /etc/systemd/system/rocky-sys.service systemctl enable --now rocky-sys这个命令把已有容器转换成systemd unit文件--new参数表示每次service启动时重建容器而不是直接复用当前这个。好处是配置快、零学习成本坏处是生成的unit文件篇幅长参数全拼在里面想改个端口就得重新生成一次。后来Podman团队推出Quadlet才算是正路。5.2 Quadlet声明式托管Quadlet的思路是让systemd直接读取Podman的声明文件。在/etc/containers/systemd/目录下放一个带.container后缀的文件systemd daemon-reload时就会自动生成对应的service unit。以我们前面那个Rocky systemd容器为例# /etc/containers/systemd/rocky-sys.container [Unit] DescriptionRocky systemd container with VNC Wantsnetwork-online.target Afternetwork-online.target [Container] Imagerocky-sys-image:latest ContainerNamerocky-sys Systemdalways PublishPort22:22 PublishPort5901:5901 [Service] Restartalways TimeoutStartSec900 [Install] WantedBymulti-user.target启用方式systemctl daemon-reload systemctl enable --now rocky-sys这里systemctl服务名是rocky-sysQuadlet自动把rocky-sys.container映射成同名service。daemon-reload之后可以用systemctl status rocky-sys来做验证。文件里[Container]段的Systemdalways等价于命令行的--systemdalways这步最容易漏掉。不写的话Quadlet会走默认行为对Rocky镜像其实也能自动检测到但为了明确预期还是显式写上。PublishPort字段写法要注意Quadlet语法里不含-p而是用PublishPort分段写法多个端口就写多行。字段名是PascalCase风格别用小写加下划线否则启动时会被忽略。宿主机的systemd托管容器、容器内的systemd托管服务这个双重体系在日志上需要分清楚容器生命周期日志看journalctl -u rocky-sys容器内服务日志看podman exec rocky-sys journalctl -u vncserver:1。两个层面互不干扰排查问题时先判断是容器起不来还是容器内服务起不来方向对了能省很多时间。6. 高频报错排查与避坑实录6.1 常见报错速查报错信息原因解法Failed to mount cgroup at /sys/fs/cgroup宿主机cgroup v1或Podman版本过旧检查cgroup版本升级Podman到4.xSystem has not been booted with systemd as init system容器PID 1不是systemd用--systemdalways并指定启动命令/sbin/initFailed to get D-Bus connection: Operation not permitteddbus服务没起来或权限不足安装dbus-daemon检查是否缺少SYS_ADMINCannot determine cgroup we are running in宿主机cgroup_manager不是systemd修改containers.conf中的cgroup_manager为systemdJob for vncserver:1.service failed服务本身启动失败权限或配置问题journalctl查看具体失败原因逐条排查6.2 我实际踩过的几个坑第一个坑是cgroup_manager配置。我一开始是在一个长期运行的老Podman环境上测试全局cgroup_manager还停留在cgroupfs结果容器内systemd启动时一路报Cannot determine cgroup we are running in我花了大半天查容器参数最后发现改一行containers.conf就解决了。第二个坑是Dockerfile里执行systemctl enable失败。构建阶段我试图在RUN指令里直接enable服务结果因为容器内根本没有跑systemd命令报错导致镜像构建中断。处理方式就是加2/dev/null || true运行阶段再验证。这一点对不熟悉容器构建机制的人来说很坑总感觉构建失败说明配置有问题但其实构建阶段不需要服务真的跑起来。第三个坑是VNC启动失败排查方式比较有代表性。报错里能明显看到权限问题但到底缺哪个capability文档不会直接告诉你。我的做法是在容器里跑systemd-analyze security和strace逐条看系统调用拦截情况最后定位到需要SYS_ADMIN和seccomp放开。这里也提醒一句如果加严格seccomp策略容器内systemd几乎必挂要么不设seccomp策略要么用unconfined。第四个坑是journald日志无节制增长。容器内systemd跑上一段时间/run/log/journal下的日志文件能吃掉上百MB内存。解决方式是在容器内加一个journald限额配置mkdir -p /etc/systemd/journald.conf.d cat /etc/systemd/journald.conf.d/size.conf EOF [Journal] SystemMaxUse50M RuntimeMaxUse20M EOF这个配置建议直接写进Dockerfile而不是每次容器启动后手工加。一套整洁的日志策略能让长周期运行的容器稳定很多。6.3 如果systemd还是起不来的排查思路有些情况下报错并不典型措辞模棱两可。我总结了一个通用排查路径按步骤来基本能覆盖80%的问题第一步确认宿主机cgroup v2环境执行stat -fc %T /sys/fs/cgroup不是cgroup2fs就先解决这个基础问题。第二步检查容器启动参数重点看--systemd值和cgroup设置。第三步用podman exec进入容器直接跑ps -p 1确认PID 1的cmdline是不是systemd相关路径。第四步查看容器内journal用journalctl -b -p err看启动阶段错误。第五步从宿主机用dmesg | grep -i cgroup查看内核层是否有拒绝消息。这套流程走完还没找到原因的场景我目前还没遇到过。真到那一步建议在宿主机上跑一个干净的最小测试容器——不装任何额外服务只启动systemd验证基础能力。如果最小容器能起来再一个个加服务用二分法定位到具体是哪个unit把环境搞坏。我个人在实际操作中体会最深的还是那句老话环境对了一切水到渠成。cgroup v2、cgroup_manager、systemd模式这三个基础点确认到位之后往容器里塞什么服务都顺理成章。真遇到问题也别慌把报错贴进搜索引擎看看报错的前几行而不是最后几行往往线索就在最前面那句cgroup或dbus相关提示里。
返回列表