news 2026/11/3 4:03:11

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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-bus:systemd的工具集(systemctl、journalctl)要通过d-bus和systemd守护进程通信。容器里没跑dbus,systemctl就报"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这种轻量发行版压根就没编译systemd,debian: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隔离,再配合--systemd=always,容器内的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-broker,Ubuntu用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 方式一:--systemd=true,自动探测

--systemd=true其实就是默认值(在大多数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 方式二:--systemd=always,强制接管

如果你不想每次都在启动命令里带/sbin/init,或者担心镜像里的CMD不是你想要的入口,直接用--systemd=always:

podman run -d --name rocky-systemd \ --systemd=always \ --privileged \ rockylinux:9

这里要特别注意--privileged。在--systemd=always模式下,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 --type=service --state=running

只要PID 1是systemd,且systemctl不再报"System has not been booted...",就说明接管成功。如果is-system-running给的是degraded也别慌,多半是某个服务没起来,用systemctl --failed看下挂了哪个,再逐一排查。

4. 实战:在Rocky 9容器里用systemd管好vncserver

4.1 场景与准备工作

为什么要拿vncserver当案例?因为这是我在网上看到热搜词里反复出现的场景,而且它特别有代表性:RHEL 9系把vncserver改成了systemd unit(vncserver@.service),你如果还想沿用老的"直接敲vncserver命令"方式,系统会直接提示你应该用systemd管理。既然它天生跟systemd绑定,那把它放进systemd容器里就是最契合的用法。

我先准备一个目录结构,并写一个简单的Dockerfile:

mkdir -p ~/rocky-vnc/systemd-units cd ~/rocky-vnc

Dockerfile内容如下:

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 \ --systemd=always \ --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] Description=My Python App [Service] Type=simple WorkingDirectory=/opt/myapp ExecStart=/usr/bin/python3 /opt/myapp/main.py Restart=on-failure StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target

把它放到容器的/etc/systemd/system/myapp.service,然后systemctl enable --now myapp,以后就能用systemd管理这个App的启停和日志了。日志要输出到终端的话,把StandardOutput=journal改成StandardOutput=inherit,跑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] Description=VNC Container managed by systemd [Container] Image=rocky-vnc ContainerName=vnc-container Systemd=always Privileged=true PublishPort=5901:5901 [Install] WantedBy=multi-user.target

每个字段的含义简单说下:

  • [Container]段里的Systemd=always,就是把前面命令行里的--systemd=always搬进来了。
  • Privileged=true对应当前场景下的特权要求。
  • PublishPort=5901:5901等价于-p参数。
  • [Install]段的WantedBy=multi-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用--systemd=always重新启动
Failed to connect to bus: No such file or directory容器内没有dbus在跑安装并启动dbus或dbus-broker
容器启动后立刻退出特权不足,systemd挂载cgroup失败加--privileged或补齐相关capabilities
systemctl有意义,但is-system-running显示degraded有服务启动失败用systemctl --failed查看失败服务
vncserver@:1.service启动失败,日志提示找不到用户没配置vncserver.users在/etc/tigervnc/vncserver.users加用户映射
容器里podman logs看不到应用日志systemd服务日志默认走journal服务unit里加StandardOutput=inherit
Quadlet服务启动后容器名冲突之前已存在同名容器先用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模板里写过的StandardOutput=journal导致的——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,把整个链路摸熟。等你对--systemd=always和Quadlet的组合拳有了手感,再逐步把权限收窄、把服务做多,你会发现自己对Linux系统管理的理解也跟着深了一层。

最后再分享一个小技巧:在容器里跑systemd服务时,systemctl status的输出不如宿主机上那么丰富,遇到疑难问题,第一时间去翻journalctl -u 服务名 -e,里面的日志往往能直接告诉你答案。记住这条,可以帮你少走很多弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/24 3:59:54

购物商城App期末大作业:安卓高分项目从0到1实战指南

简介:在安卓应用开发中,SQLite作为轻量级本地数据库,是管理结构化数据的常用方案;RecyclerView则通过ViewHolder复用高效渲染列表界面,二者结合能快速搭建数据驱动型应用。理解其原理,有助于在课程设计中实…

作者头像 李华
网站建设 2026/10/13 18:02:57

Flutter鸿蒙开发实战:首页布局拆解与组件组合指南

把Flutter鸿蒙开发的系列写到第五篇,前几篇把环境搭建、工程骨架和状态管理聊完,总算可以动手写一个真实的页面了。这篇文章我准备聚焦在首页基础布局上:从拿到设计稿怎么拆结构开始,逐个讲透Flutter布局里最常用的Flex、Column、…

作者头像 李华
网站建设 2026/10/24 3:58:34

WPF DispatcherTimer异步任务取消策略:CancellationToken与MVVM封装实践

聊到 WPF 里的 DispatcherTimer,十个人里九个第一反应是“定时刷新数据的玩意儿”。这没错,但真正到了生产环境,尤其是把 DispatcherTimer 和 async/await 这种异步任务捏在一起用的时候,问题就远不是“设置个 Interval、挂个 Tic…

作者头像 李华
网站建设 2026/10/24 4:02:07

SpringBoot+Vue学生课外活动管理系统全栈实战:从需求拆解到部署避坑

最近帮人做了一套基于SpringBoot的学生课外活动管理系统,前后端完整跑通,折腾了小半个月。这个项目技术栈不算复杂,但麻雀虽小五脏俱全——用户体系、权限控制、活动发布、报名审核、消息通知、数据统计全都要顾到。如果你正在准备JavaWeb方向…

作者头像 李华
网站建设 2026/10/24 10:16:11

基于Spring Boot和MyBatis Plus的代码作业查重系统实战解析

简介:这是一套基于Spring Boot和MyBatis Plus的代码作业查重系统完整源码,面向需要实现作业提交、自动查重与评分管理场景的各类开发者,适合作为毕业设计或企业级教学平台的基础。系统覆盖学生、教师、管理员三类角色,支持作业发布…

作者头像 李华