在容器技术普及之前,Systemd 已经成为大多数 Linux 发行版的事实标准初始化系统。把它搬进 Docker 容器后,很多运维同学会习惯性地执行 systemctl start nginx,结果却收到 Failed to get D-Bus connection 或者 System has not been booted with systemd as init system 的报错。出现这些问题的本质并不是 Docker 不支持 Systemd,而是容器的进程模型与 Systemd 的设计假设存在冲突。容器默认只启动一个前台进程,进程退出后容器生命周期就结束;Systemd 则期望自己作为 PID 1 接管整个系统的初始化、服务编排、日志和资源控制。因此想让 Systemd 在容器里正常工作,必须主动调整镜像配置、启动参数和运行环境。

下面会从 PID 1 与 cgroup 两个核心障碍开始分析,再给出可用的 Dockerfile 与运行命令,最后讨论常见故障与生产建议。
核心障碍:PID 1 与 cgroup 的配合
Linux 内核对 PID 1 有特殊对待:它负责收养所有孤儿进程,并且只有它收到 SIGTERM 或 SIGINT 时不会被默认忽略。Systemd 作为 PID 1 时,会注册大量信号处理器、挂载必要的文件系统、启动日志服务和 D-Bus。普通 Docker 容器如果直接把 systemd 作为启动命令,首先面临的问题就是它需要挂载 cgroup 文件系统来管理服务的资源限制。
在宿主机上,Systemd 通过 cgroup 树来划分 slice、scope 和 service 单元。容器默认只获得一个相对隔离的 cgroup 命名空间,且很多系统目录(比如 /sys/fs/cgroup)以只读方式挂载或者根本没有挂载完整。Systemd 启动时会尝试重新挂载 cgroup v2 或 v1 的控制器,这一步失败就会直接进入 emergency mode 或者报错退出。解决办法通常有两个方向:一是通过 systemd.unified_cgroup_hierarchy=0 内核参数回退到 cgroup v1(不推荐新系统),二是在容器启动时以特权模式运行并挂载宿主机部分 cgroup 路径。后者虽然可行,但会削弱容器隔离性,只适合测试或特殊镜像。
另一个关键点是 D-Bus。Systemd 通过 D-Bus 暴露管理接口,systemctl 命令依赖这个通道与 Systemd 通信。如果容器内没有运行 dbus-daemon 或者没有正确设置 DBUS_SESSION_BUS_ADDRESS,即使 Systemd 进程活着,执行 systemctl 也会提示无法连接。因此完整方案需要在启动脚本里先拉起 dbus,再执行 systemd。
可行的镜像配置与启动方式
以 CentOS 或 Ubuntu 为基础镜像来构建支持 Systemd 的容器时,通常需要安装 systemd 软件包、设置容器启动命令为 /sbin/init 或 /lib/systemd/systemd,并确保 cgroup 权限足够。下面是一个基于 Ubuntu 22.04 的 Dockerfile 示例,展示了必要的安装与清理步骤。
FROM ubuntu:22.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && \
apt-get install -y systemd systemd-sysv dbus dbus-user-session && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# 让 systemd 作为 PID 1 启动
CMD ["/sbin/init"]
构建镜像后,运行容器时不能使用普通的 docker run -d 直接启动,否则仍会看到 Failed to mount cgroup 之类的错误。需要添加 --privileged 参数以及挂载 cgroup 目录。如果宿主机使用 cgroup v2,可以尝试以下命令:
docker run -d \ --name systemd-test \ --privileged \ --tmpfs /run \ --tmpfs /run/lock \ -v /sys/fs/cgroup:/sys/fs/cgroup:rw \ systemd-image:latest
这里 --tmpfs /run 和 /run/lock 是为 Systemd 提供可写的运行时目录。如果缺少 /run/lock,某些服务会卡住。--privileged 则放开容器对设备、cgroup 等内核资源的访问限制。生产环境中如果不想全特权,也可以尝试使用 --cap-add SYS_ADMIN 并配合 --security-opt seccomp=unconfined 来部分放开,但兼容性因运行时而异。
启动后进入容器执行 systemctl status 验证。若一切正常,能看到 Systemd 的状态为 running,并且 systemctl list-units 能列出目标服务。如果仍然报错,可以检查容器日志:docker logs systemd-test 会输出 Systemd 的启动过程,定位是 cgroup 挂载失败还是 D-Bus 未启动。
服务运行与常见错误排查
在容器内用 systemctl 启动 OpenSSH、Nginx 或自定义服务时,很多人会遇到服务状态显示 failed,但手工执行二进制却没有问题。这是因为 Systemd 管理服务时会通过 cgroup 来追踪进程,而容器内缺少完整的 systemd 日志和会话环境。例如 sshd 启动时要求 /run/sshd 目录存在且权限正确,Systemd 单元文件通常会自动创建该目录,但容器启动时如果没有重新生成 unit 文件或者 tmpfiles.d 没有执行,就可能缺失目录导致失败。
排查这类问题的顺序是:先查看 systemctl status 输出中的具体报错行,再用 journalctl -xe 查看日志。容器内 Systemd 默认会把日志写到 journal,可以通过 journalctl -u 服务名 来查看。若日志为空,可能是 /var/log/journal 不可写或 systemd-journald 未启动。手动执行 systemctl start systemd-journald 有时可以恢复。另一个容易被忽略的问题是容器停止时的优雅退出。默认 docker stop 会向 PID 1 发送 SIGTERM,Systemd 收到后会给所有服务发送停止信号并等待超时。如果某个服务忽略了 SIGTERM,容器会在 10 秒后被强制杀掉,导致数据不一致。可以在 unit 文件中明确添加 TimeoutStopSec 和 KillSignal 来优化停止行为。
代码示例:自定义一个简单服务,验证 Systemd 管理效果。创建 /etc/systemd/system/demo.service 文件:
[Unit] Description=Demo Service After=network.target [Service] ExecStart=/usr/bin/sleep infinity Restart=always TimeoutStopSec=5 [Install] WantedBy=multi-user.target
然后在容器内执行 systemctl daemon-reload && systemctl enable demo && systemctl start demo。如果服务能正常启动并保持在 running 状态,说明 Systemd 的核心功能已经可用。如果报错 Unit demo.service not found,通常是路径或 SELinux 权限问题,需要检查挂载的 /etc/systemd/system 目录是否可写。
生产环境建议与替代方案
虽然通过上述配置可以在 Docker 中运行 Systemd,但大多数生产环境并不推荐这样做。原因很明确:容器被设计为单进程、不可变、快速替换的单元,而 Systemd 带来的是重量级的系统管理,增加了镜像体积、启动时间和攻击面。强行在容器里跑 Systemd 往往是为了在测试环境中复现完整的系统服务,或者为了某些需要 systemctl 管理多进程的传统应用。如果没有这些需求,更合理的方式是使用轻量初始化程序如 tini 或 dumb-init 来处理僵尸进程和信号转发,或者直接用 docker-compose 管理多个容器,让每个容器只跑一个服务。
如果确实要在 Kubernetes 或生产集群中使用类似 Systemd 的能力,可以考虑 Podman 的 systemd 集成,或者使用 KubeVirt 这类虚拟化方案。Podman 支持以 rootless 方式运行 container 并由 Systemd 在宿主机层面管理容器生命周期,这比在容器内部运行 Systemd 更加符合容器化理念。无论是选择哪种方案,理解 Systemd 对 PID 1、cgroup、D-Bus 和信号的依赖,都有助于快速定位问题并做出合理架构决策。
最后再强调一点:如果只是需要某个服务在容器启动时自动运行,完全可以自己写一个简单的 entrypoint 脚本,通过 & 或者 exec 来管理多个进程,而不必引入 Systemd。例如使用 supervisord 作为进程管理器,它比 Systemd 轻量,对容器更友好。只有在无法修改应用架构、必须复现宿主机系统行为时,才考虑在 Docker 里运行 Systemd,并按照本文的注意事项做好 cgroup 挂载、特权模式、D-Bus 启动和优雅停止配置。

