ARTICLE DETAIL

资讯详情

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

Podman容器里启用systemd:让systemctl正常工作的完整攻略

Podman容器里启用systemd:让systemctl正常工作的完整攻略 前两天有个朋友跑来问我说他照着网上搜到的教程用podman run拉了一个rockylinux:9容器进去之后想执行systemctl start sshd结果直接被怼了一句System has not been booted with systemd as init system。这问题我早几年折腾Podman的时候也撞过后来翻了不少文档、做了几次实验才算彻底搞明白容器里默认的1号进程根本不是systemd你在里面敲systemctl人家当然不认账。这篇文章就把Podman容器中启用systemd这件事从头到尾掰开揉碎讲清楚。你会看到systemd和容器之间的天然矛盾、Podman的--systemd参数到底在背后做了什么、cgroup和dbus要准备到什么程度最后再走一遍实操在Rocky Linux 9容器里让vncserver以systemd服务的形式跑起来再讲讲怎么用Quadlet让宿主机systemd来托管这些容器。适合正在搞容器化迁移、想在容器里跑完整系统服务、或者单纯被容器里起不了systemd折磨过的朋友。1. 为什么容器里跑systemd这么费劲1.1 先搞清楚容器里的一号进程是谁Linux容器本质上就是宿主机上的几个进程只不过通过namespace做了隔离。每个容器都有一个PID 1而这个进程扮演的角色远比你想的重要它负责回收孤儿进程、接收并处理内核信号、还要承担整个容器生命周期里最后收尸的工作。默认情况下你podman run rockylinux:9进去PID 1大概率是/bin/bash或者镜像里指定的CMD。它也能干活但你别指望它替你管理子进程、帮你把daemon拉起来、处理信号转发。我打个比方普通容器像是你租了个单间屋里只有你自己想开火做饭得自己架炉子而systemd容器是拎包入住的整套公寓物业、水电、安保都是现成的你把服务往里一放人家自动帮你看着。问题在于systemd这个物业很霸道——它要求自己是PID 1它要管理cgroup它还要访问一堆特殊挂载点。传统容器那种一个进程一个容器的模型跟它天生八字不合。1.2 systemd的真实需求清单要让systemd在容器里正常工作不只是把它放到PID 1就行。它有一份严格的需求清单cgroup文件系统必须是可读写的systemd要靠它来跟踪和管理服务进程。只读的/sys/fs/cgroup会让systemd直接罢工。d-bussystemd的工具集systemctl、journalctl要通过d-bus和systemd守护进程通信。容器里没跑dbussystemctl就报Failed to connect to bus。/run、/tmp等目录systemd在启动时要创建运行时目录如果文件系统是只读的或者不合适的挂载类型它也会出问题。特权能力挂载cgroup、创建设备节点、设置主机名等工作都需要一定权限普通容器默认给不了这么多。看到这里你应该明白网上很多容器里起systemd的教程之所以失败不是因为命令不对而是这些前置条件没满足差一步都跑不通。1.3 Docker没做到的事Podman怎么补上的用过Docker的朋友都知道官方建议用systemd-docker、tini这类外部工具来作为PID 1或者手动挂载cgroup写一堆环境变量和挂载参数麻烦不说还容易踩版本兼容的坑。Podman从设计上就非常系统管制友好。它提供了一个原生的--systemd参数支持true、always、false三个取值自动化了把容器入口改成systemd这个动作。加上Podman天生支持无root运行rootless模式、生成的命令可以和宿主机systemd无缝集成可以说在容器里跑系统服务这件事上Podman的思路就是Docker的痛我懂所以我换个方式搞。后面几个章节我会把这套机制的实际用法、参数区别和踩坑点全部讲透。2. 动手前的准备工作镜像、权限和依赖2.1 挑选一个适合跑systemd的镜像不是所有镜像都适合跑systemd。像alpine这种轻量发行版压根就没编译systemddebian:bookworm-slim这种精简镜像里也是残的。我个人的建议是想省事就直接选官方做了systemd适配的镜像比如rockylinux:9、centos:stream9。这些镜像里预装了/sbin/init而且官方文档明确说它们是为systemd容器准备的。如果你非要用Ubuntu那就选ubuntu:22.04以上的版本然后手动在里面装systemd和dbus。我也试过能用但多一层手工步骤出问题的时候排查起来更麻烦。如果只是实验环境我强烈建议从Rocky Linux或者CentOS Stream开始Linux发行版打包好的systemd镜像能省掉不少无谓的折腾。还有一个细节别在官方镜像上一股脑装一堆东西。每个容器里服务越少systemd管理起来越清晰出问题的面也越小。我见过有人把nginx、MySQL、PHP全都塞进一个systemd容器里最后自己都分不清哪个服务崩了这个习惯不建议学。2.2 cgroup挂载到底该怎么做在让容器里的systemd跑起来这件事上cgroup挂载是翻车最多的一环。我先把不同的挂载策略和适用场景列出来后面实操部分再走完整命令。方案A特权模式 直接挂载宿主机cgroup。启动容器时加--privileged再手动-v /sys/fs/cgroup:/sys/fs/cgroup:rw。这种方式最粗暴兼容性也最好但特权模式等于把宿主机的很多安全边界都放开了生产环境要慎用。方案B只给必要的权限 挂载cgroup。用--cap-add SYS_ADMIN配合-v /sys/fs/cgroup:/sys/fs/cgroup:rw。这个比特权模式收敛一些但还是给了容器很强的内核操控能力。方案C靠Podman自己的cgroup namespace隔离。Podman默认会给容器做cgroup namespace隔离再配合--systemdalways容器内的systemd在自己的cgroup子树里工作。这种方式最干净但要求宿主机是cgroup v2且Podman版本不能太老。我实测下来如果你用的是RHEL 9、Rocky 9这类宿主机内核和Podman版本都比较新直接用方案C最省事命令可以减掉一长串。如果是旧环境老老实实走方案A。后面第4章里的vncserver案例我会按方案C来写所以权限参数不会太多。2.3 dbus务必要装好你可能觉得systemd跑起来了不就能systemctl了吗但有个额外的坑容器里一般没有dbus守护进程在跑。我一开始做实验的时候容器起了systemd也在PID 1上可是敲systemctl status照样报错Failed to connect to bus: No such file or directory原因就是没有dbus。systemctl是客户端它得通过d-bus和systemd的守护进程通信。镜像里如果没装dbus或者没启动dbus服务客户端和服务器之间没通道自然连不上。解决办法很简单Rocky系用dnf install -y dbus dbus-brokerUbuntu用apt install -y dbus dbus-broker。进容器之后再把dbus-broker放出来systemctl enable dbus-broker.service systemctl start dbus-broker.service这一步不做后面所有的systemctl操作都会卡在连接总线的报错上而且特别隐蔽因为排查的时候第一反应往往去查systemd的状态了一颗耗子屎坏了一锅汤。3. 两种最常见的启动方式对比Podman的--systemd参数是这一整套机制的核心但它有不同的取值和细节行为。我先说说它们的区分。3.1 方式一--systemdtrue自动探测--systemdtrue其实就是默认值在大多数Podman版本里。它的逻辑很简单如果容器的入口命令Entrypoint或CMD是/sbin/init、/usr/lib/systemd/systemd这类systemd相关的可执行文件Podman就会把容器改成systemd模式让systemd成为PID 1。举个实际例子你用podman run -d rockylinux:9 /sbin/init这种命令时Podman检测到你要跑/sbin/init就会自动将启动流程交给systemd。好处是普适性很强不显式加参数也能跑。但它也有个让我难受的地方如果你在命令行上直接写/bin/bash或者不写/sbin/init它就不会启用systemd模式而是老老实实跑bash。所以每次启动时你都得记得入口得是/sbin/init少写一次就白折腾。3.2 方式二--systemdalways强制接管如果你不想每次都在启动命令里带/sbin/init或者担心镜像里的CMD不是你想要的入口直接用--systemdalwayspodman run -d --name rocky-systemd \ --systemdalways \ --privileged \ rockylinux:9这里要特别注意--privileged。在--systemdalways模式下Podman会把容器的入口强制指向systemd而systemd启动时要挂载cgroup、读写一堆系统目录普通权限根本不够。我实测过不加--privileged的话容器经常起来几秒就崩了日志里全是各种Permission denied。加上之后容器起来就会自动进入systemd管理状态不需要你手动管入口命令。这也是我日常最常用的启动方式它能省掉入口命令写没写这种烦琐细节直接把systemd变成一个硬约束。3.3 验证systemd是否成功接管不管用哪种方式启动进去之后第一件事就是验证。我自己的验证习惯分三步# 1. 看PID 1 ps -p 1 -o comm # 预期输出systemd # 2. 看systemd是否在位 systemctl is-system-running # 预期输出running 或者 degraded # 3. 看是否真能管服务 systemctl list-units --typeservice --staterunning只要PID 1是systemd且systemctl不再报System has not been booted...就说明接管成功。如果is-system-running给的是degraded也别慌多半是某个服务没起来用systemctl --failed看下挂了哪个再逐一排查。4. 实战在Rocky 9容器里用systemd管好vncserver4.1 场景与准备工作为什么要拿vncserver当案例因为这是我在网上看到热搜词里反复出现的场景而且它特别有代表性RHEL 9系把vncserver改成了systemd unitvncserver.service你如果还想沿用老的直接敲vncserver命令方式系统会直接提示你应该用systemd管理。既然它天生跟systemd绑定那把它放进systemd容器里就是最契合的用法。我先准备一个目录结构并写一个简单的Dockerfilemkdir -p ~/rocky-vnc/systemd-units cd ~/rocky-vncDockerfile内容如下FROM rockylinux:9 RUN dnf -y update \ dnf -y install tigervnc-server dbus dbus-broker \ dnf clean all这里有个小心得tigervnc-server在Rocky 9里是系统自带的systemd unit来管理的所以不需要额外写unit文件。但如果你装的是别的服务比如自定义脚本或第三方应用就得自己在/etc/systemd/system/下放一个.service文件。后面我会补一个通用模板。4.2 从Dockerfile到systemd容器镜像构建好之后用第3章说的方式启动podman build -t rocky-vnc . podman run -d --name vnc-container \ --systemdalways \ --privileged \ -p 5901:5901 \ rocky-vnc这个命令里我用了--privileged。有朋友会问第2章不是说了优先走cgroup namespace隔离吗怎么又用特权模式了这里我得解释清楚vncserver要监听TCP端口、创建虚拟终端还会用到一些和输入设备、显示设备相关的内核接口权限需求比单纯跑nginx要复杂。在这种案例里优先保证可用性比追求最小权限更实际所以直接特权模式实验或者内网环境很稳妥。如果要把这套东西做成生产级再慢慢按需裁剪权限也不迟。容器起来之后进到里面podman exec -it vnc-container bash先确认systemd在跑systemctl is-system-running systemctl status vncserver:1.service正常情况下你会看到vncserver:1.service被加载但状态可能是inactive因为还没启用。接下来设置VNC密码并启用服务vncpasswd systemctl enable vncserver:1.service --now注意vncserver:1.service这个unit文件里默认读取的是/etc/tigervnc/vncserver.users里的用户映射还有/etc/tigervnc/vncserver-config-defaults里的配置。我一开始没管这两个文件启动后日志提示找不到对应的用户后来把想跑VNC的普通用户加进vncserver.users就好了。4.3 接管之后的服务启停与日志排障现在systemd已经彻底接管了vncserver。平时你怎么在真机上操作容器里就怎么操作唯一的区别是端口已经通过-p映射到宿主机了。systemctl restart vncserver:1.service systemctl status vncserver:1.service看日志用journalctl这个命令在容器里同样适用journalctl -u vncserver:1.service -f-f是跟随模式日志会像tail -f一样滚动输出。我用这个命令排查过好几次VNC起不来的问题比如找不到用户、密码文件权限不对基本都是在日志里一眼看到的。如果你在容器里装了其他自定义服务比如一个Python写的App想用systemd管起来模板可以这样写[Unit] DescriptionMy Python App [Service] Typesimple WorkingDirectory/opt/myapp ExecStart/usr/bin/python3 /opt/myapp/main.py Restarton-failure StandardOutputjournal StandardErrorjournal [Install] WantedBymulti-user.target把它放到容器的/etc/systemd/system/myapp.service然后systemctl enable --now myapp以后就能用systemd管理这个App的启停和日志了。日志要输出到终端的话把StandardOutputjournal改成StandardOutputinherit跑podman logs -f时就能看到应用的标准输出。5. 用Quadlet把systemd容器交给宿主机systemd托管5.1 为什么不建议用podman generate systemd硬转到了这一步容器里的systemd已经能跑了但还有一个很现实的问题每次宿主机开机难道要手动podman start吗当然不是。Podman官方当年给过一个podman generate systemd命令可以把容器转成宿主机systemd服务。我最早用的就是这个方案后来发现它有个毛病容器一旦重建或者你调整了启动参数生成的unit文件就过期了又得重新生成一遍才能对上。维护成本不算低。现在更推荐的是Quadlet。Podman 4.4自带了Quadlet的支持它的思路是你写一个.container文件放在/etc/containers/systemd/目录下然后宿主机上的systemd会自动识别并把它当作一个容器单元来管理。容器参数变了直接改文件重新systemctl daemon-reload就行。5.2 写一个.container单元文件我直接拿上一章的VNC容器做例子。在宿主机上创建/etc/containers/systemd/vnc-container.container[Unit] DescriptionVNC Container managed by systemd [Container] Imagerocky-vnc ContainerNamevnc-container Systemdalways Privilegedtrue PublishPort5901:5901 [Install] WantedBymulti-user.target每个字段的含义简单说下[Container]段里的Systemdalways就是把前面命令行里的--systemdalways搬进来了。Privilegedtrue对应当前场景下的特权要求。PublishPort5901:5901等价于-p参数。[Install]段的WantedBymulti-user.target表示开机进入多用户模式后自动启动。5.3 宿主机systemd和容器内systemd如何协同写完之后在宿主机上执行systemctl daemon-reload systemctl enable --now vnc-container.service注意这里的服务名是vnc-container这是Quadlet根据.container文件名自动生成的。现在你在宿主机上敲systemctl status vnc-container.service能看到的是这个容器进程的状态。而进到容器里敲systemctl status vncserver:1.service看到的是容器内systemd管理的服务状态。两者是嵌套关系很容易搞混我建议大家心里把这条线理清宿主机systemd → 容器本身vnc-container → 容器内systemd → vncserver服务排查问题时也按这个层级来先看宿主机的容器单元有没有起来再进容器看具体服务状态。比如VNC连不上第一步一定是先在宿主机上看systemctl status vnc-container.service确认容器进程活着然后再进容器看systemctl status vncserver:1.service。只盯一头很容易被误导。6. 常见问题与排查实录6.1 一张问题速查表我把这几年实操下来碰到的典型问题整理成一张速查表方便你现场对着看错误现象根本原因解决方案System has not been booted with systemd as init systemPID 1不是systemd用--systemdalways重新启动Failed to connect to bus: No such file or directory容器内没有dbus在跑安装并启动dbus或dbus-broker容器启动后立刻退出特权不足systemd挂载cgroup失败加--privileged或补齐相关capabilitiessystemctl有意义但is-system-running显示degraded有服务启动失败用systemctl --failed查看失败服务vncserver:1.service启动失败日志提示找不到用户没配置vncserver.users在/etc/tigervnc/vncserver.users加用户映射容器里podman logs看不到应用日志systemd服务日志默认走journal服务unit里加StandardOutputinheritQuadlet服务启动后容器名冲突之前已存在同名容器先用podman rm -f清理旧容器6.2 我踩过的几个特别隐蔽的坑第一个坑是手动挂载cgroup后systemd反而起不来。有段时间我图省事把宿主机的/sys/fs/cgroup直接只读挂进容器结果容器内systemd一直报cgroup mount failed。后来才明白systemd需要在这个目录里创建子目录来管理服务只读挂载等于把它的手脚绑死了。如果你用的是老式挂载方案务必用rw权限。第二个坑是在容器里敲systemctl卡了非常久。排查半天才发现是dbus-broker没起来systemd客户端一直等总线的响应。Ubuntu镜像尤其容易遇到这个问题镜像里经常默认不带dbus。现在凡是做systemd镜像我都会先装一轮dbus相关包再说省得后面还要回头补。第三个坑是日志看不着。在容器里跑journalctl看起来是正常的但有时候宿主机上想用podman logs看应用输出却什么都没有。这个就是我在5.2模板里写过的StandardOutputjournal导致的——systemd把服务输出吞进journal而不是发到容器的stdout。想让podman logs也能看到就把unit文件的StandardOutput改成inherit。6.3 一个小技巧容器内跑进程别忘信号处理系统服务在关闭时要能接收系统信号比如SIGTERM这本来是systemd负责的事。你在systemd容器里跑应用直接用systemd管信号处理会非常规范。但如果你在容器里又手动开了一堆后台进程比如nohup python xxx 这种systemd根本不知道它们存在容器停止时这些进程会成为孤儿还可能没机会优雅退出。所以容器里能用systemd管理的尽量都交给它手动后台进程能不搞就不搞。最后再聊几句我的体会用Podman跑systemd容器这几年我最深的感受是它确实把Docker里搞systemd从玄学变成了工程实践。最开始我也被一堆报错打得晕头转向后来理解了底层原理——cgroup、dbus、PID 1这三件套到位了systemd在容器里就能跑这三样缺一个它就能给你整出千奇百怪的错误来。如果你刚开始尝试别一上来就在生产环境里折腾先用Rocky Linux 9官方镜像按第4章的流程跑一遍vncserver把整个链路摸熟。等你对--systemdalways和Quadlet的组合拳有了手感再逐步把权限收窄、把服务做多你会发现自己对Linux系统管理的理解也跟着深了一层。最后再分享一个小技巧在容器里跑systemd服务时systemctl status的输出不如宿主机上那么丰富遇到疑难问题第一时间去翻journalctl -u 服务名 -e里面的日志往往能直接告诉你答案。记住这条可以帮你少走很多弯路。
返回列表