ARTICLE DETAIL

资讯详情

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

macOS Docker容器systemd not running报错:根因排查与解决方案

macOS Docker容器systemd not running报错:根因排查与解决方案 如果你在 macOS 上装了 Docker Desktop拉了一个ubuntu:22.04之类的镜像然后想当然地在容器里执行systemctl start ssh大概率会看到一串让你怀疑人生的报错——systemd not running on this host。我第一次遇到时第一反应是 Docker Desktop 坏了重装了两次才发现事情没那么简单。这篇文章不是翻译文档就是一次完整的问题排查实录。我会把报错背后的机制、定位的步骤、能落地的几套方案以及我踩过的坑全部摊开讲。适合在 Mac 上用 Docker 跑 Linux 容器、偶尔需要 systemd 环境的开发者也适合那些打算把容器当虚拟机用、结果被 init 系统教育了一顿的朋友。内容偏底层但我会尽量说人话保证你能照着排查。1. 触发条件复盘哪些容器最容易在 Docker Desktop 上报这个错1.1 三种高频触发场景装软件、跑服务、写脚本先说结论systemd not running on this host 不是每次启动容器都出现。它需要满足容器里真的有人在尝试用 systemd这个前提。根据我自己的复现最常见的是下面三种场景。第一种是拉取 Ubuntu、Debian、CentOS 这类完整发行版镜像然后在容器里执行系统服务管理命令。比如你想在容器里开个 SSH习惯性输入systemctl start ssh或者用systemctl enable设置开机自启这种命令在容器环境里几乎必然失败。因为容器的 PID 1 默认是bash或镜像里自定义的入口进程根本不是 systemdsystemctl 连不上 systemd 的 socket。第二种是安装脚本内部调用了 systemd 工具。有些软件包的install.sh很霸道装完就想通过 systemctl 帮你注册服务。比如某些数据库、消息队列、监控代理的官方安装脚本在容器里跑一半会突然抛出一句 systemd 相关的错误。表现形式五花八门但根子只有一个脚本假设自己跑在一台带 systemd 的完整 Linux 主机上。第三种是我自己踩到的在 Dockerfile 构建阶段写RUN systemctl enable xxx。这种通常发生在你照着网上教程抄 Dockerfile 时别人在虚拟机里写的步骤你原封不动搬进容器。构建过程中报错而且报错信息容易被忽略因为 docker build 的输出很长真正出错的就那一行。1.2 一句话判断你的报错是否属于这一类如果你不确定自己是不是这个问题可以在容器里快速跑两条命令复核一下。ps -p 1 -o pid,comm,args systemctl is-system-running如果第一条命令输出的是bash、sh或者你的业务进程第二条命令直接报错那基本可以确定容器里压根没有 systemd 在运行报错是正常的、符合预期的。这个思路很朴素——你在一台没装 systemd 的旧式 Linux 上敲 systemctl效果完全一样容器上下文只是把这件事放大到了 macOS 上而已。不要一上来就怀疑 Docker Desktop 安装有问题。这个错误和 Docker 本身的关系更像是你拿着遥控器去开空调结果发现家里根本没装空调——问题不在遥控器。2. systemd 在 macOS 容器里不工作的本质PID 1 争夺战与缺少 init 栈2.1 容器的 PID 1 到底是谁要理解这个报错必须先把 PID 1 这个概念弄清楚。在 Linux 系统里PID 1 是所有进程的祖先负责回收孤儿进程、管理信号转发、维护系统的整体运行状态。正常物理机或虚拟机上PID 1 大概率是 systemd 或 sysvinit。容器稍有不同。Docker 启动容器时会执行镜像里定义的 ENTRYPOINT 或 CMD 作为 PID 1。这意味着如果你跑的是ubuntu:22.04镜像且命令是bash那么容器里的 PID 1 就是一个 bash 进程systemd 并不存在。这个设计是刻意的容器被认为是单进程模型你只需要跑一个业务进程不需要像完整操作系统那样管理几十个系统服务。所以当你进入容器执行systemctl时实际发生的是一个普通用户态进程尝试通过 UNIX socket 连接 systemd但这个 socket 根本不存在于是各种报错就来了。标题里那句 systemd not running on this host直译就是这台主机上 systemd 没在跑——在容器语境下这个主机指的就是容器自己。2.2 systemd 启动时为什么死磕 PID 1systemd 有个很硬性的要求它只接受自己作为 PID 1 来初始化整个系统。如果你在容器里手动运行/lib/systemd/systemd它通常会拒绝工作或者半途崩溃。原因是 systemd 启动初期要做的事情非常多比如挂载 cgroup、建立/run目录结构、启动 socket 监听、接管会话等这些操作都默认自己拥有完整的初始化权限。流程上大致是这样systemd 启动时检测自己是否 PID 1如果不是会打印类似 System has not been booted with systemd as init system (PID 1) 的提示然后退出。就算你用了某种黑科技让它跑起来它接管系统服务的逻辑也是围绕我是启动流程的第一个进程设计的没有这个前提很多单元就起不来。这也是为什么容器里跑 systemd 这么别扭——你等于在一个已经运行着的环境里强行插入另一个 init两个管家在抢同一个院子的管理权不冲突才怪。2.3 Docker Desktop 的双重特殊Linux VM 里的容器还是没有 init在 macOS 上这个问题的感知会更严重一点因为 Docker Desktop 本身就是嵌套环境。Docker Desktop for Mac 并不是直接在 macOS 上跑容器而是先利用 macOS 的虚拟化能力启动了一个轻量级 Linux 虚拟机在虚拟机里跑 dockerd再由 dockerd 创建容器。也就是说macOS 上运行 Docker 容器时已经存在两层隔离第一层是 Linux 虚拟机第二层是容器。虚拟机的 PID 1 通常是某个精简 init早期是 HyperKit 相关进程后来迁移到 Apple 虚拟化框架系统或容器里的 PID 1 又各不相同。macOS 宿主本身没有 Linux 的 init 概念它用的是 launchd跟你容器里那套 systemd 完全是两码事。这种嵌套带来的直接后果是你在容器里想通过 systemd 管理服务需要穿透两层隔离去获得权限而 Docker 默认不会给你这个权限。相比之下在原生 Linux 的 Docker 里跑 systemd 虽然也麻烦但好歹宿主机是 Linux容器可以挂载宿主 cgroup、共享一些内核特性在 macOS 上这个共享被虚拟机挡了一道路径更绕也更难成功。3. 排查链路实录从报错出处到根因确认的四步定位法3.1 第一步确认报错由谁抛出排查类问题最忌讳跳步。我先讲第一步也是很多人容易忽略的确认报错到底是哪一层抛出来的。同样一句 systemd not running on this host可能来自三个完全不同的地方宿主机终端、容器内部、Dockerfile 构建阶段。宿主机终端出现这个信息通常是因为你直接执行了docker exec ...并调用了某个脚本容器内部出现可能是登录 shell 的提示Dockerfile 构建阶段出现则是 RUN 指令执行时。修复手段完全不同所以先问自己一句我是在哪个环节看到报错的。实际操作上我会先跑一个干净的容器做对照实验排除现有容器环境干扰docker run -it --rm ubuntu:22.04 bash进入容器后故意执行systemctl如果复现报错就说明问题确定在容器内没有 systemd跟你的业务项目无关。这一步能快速把排查范围缩小到容器运行机制层面而不是 Docker Desktop 安装损坏之类的问题。3.2 第二步查看容器 PID 1 的身份确认问题在容器侧后第二步就是看 PID 1 到底是谁。这一步是整条排查链路的核心因为 systemd 的启动条件跟 PID 1 强绑定。docker exec container ps -p 1 -o pid,comm,args也可以直接进容器内执行ps -p 1 -o comm cat /proc/1/cmdline | tr \0 我见过几种典型输出bash、sh、/entrypoint.sh、java、node甚至有时候是/bin/sleep很多人测试容器习惯跑sleep infinity。只要不是systemd那就直接命中问题本质容器生命周期里根本没有 systemd 在运行。这里补充一个细节某些基础镜像的 PID 1 是tini或dumb-init这是通过--init参数注入的轻量 init 进程。有 init 进程确实能正确转发信号、回收僵尸进程但它不等于 systemdsystemctl照样连不上。所以别看到 PID 1 不是 bash 就以为万事大吉先确认它是不是 systemd 才算数。3.3 第三步检查 systemd 环境痕迹如果 PID 1 不是 systemd第三步就是检查系统里有没有 systemd 的运行痕迹。systemd 在运行时会创建/run/systemd/system目录和/run/systemd/private等 socket 文件这是它工作的重要标志。ls -la /run/systemd/system systemctl is-system-running在普通容器里这个目录大概率不存在systemctl is-system-running会直接报错。注意报错信息可能五花八门有的版本输出offline有的输出failed to get properties还有的干脆说System has not been booted with systemd as init system。无论哪种结论都一样systemd 没有在线。这一步还有个作用区分没装 systemd和装了但没启动两种情况。基础镜像如ubuntu:22.04默认是装了 systemd 这个软件包的你甚至能which systemctl找到二进制但它只是躺在磁盘上没有任何服务在运行。这就是很多人迷惑的地方——明明systemctl命令存在为什么不能用因为命令存在不等于守护进程在跑相当于你有车钥匙但发动机没启动。3.4 第四步核对特权模式与 cgroup 视图第四步用来排除一个误解是不是加了--privileged就能让 systemd 跑起来。准确来说特权模式解决的是权限问题而 systemd 需要的不只是权限还有正确的启动上下文。docker inspect container --format {{.HostConfig.Privileged}} docker inspect container --format {{.HostConfig.CgroupnsMode}}如果输出是false和host说明容器跑在默认 cgroup namespace 下。systemd 要正常工作通常需要能挂载/sys/fs/cgroup、创建子 cgroup、管理资源控制器这些操作在容器里会被一堆 Capability 限制挡着。即便特权模式全开Docker 对这种嵌套 init 的支持也不算好后面我会展开讲。走到这一步排查链路就闭合了报错来自容器侧、PID 1 不是 systemd、systemd 运行痕迹不存在、特权模式也不足以解决问题。根因已经非常清楚下面的章节就是方案了。4. 四套能落地的解决方案按项目场景选择别把兼容层当万能药4.1 方案 A不需要 systemd就从脚本里去掉 systemctl大多数场景尤其是开发环境你的应用根本不需要 systemd。它要的只是某个进程在前台跑起来比如 Nginx、Redis、Node 服务。这时候正确的做法是改启动命令把服务进程直接拉到前台绕开 systemctl。我举个例子。假设你需要在容器里跑 SSH原本习惯性想执行systemctl start ssh换成/usr/sbin/sshd -D-D表示以前台模式运行不脱离终端。这种改法对很多服务都适用Redis 是redis-serverNginx 是nginx -g daemon off;MySQL 是mysqld。核心思想就一句话让业务进程成为 PID 1别让 systemd 当中间层。如果你的项目启动脚本里写死了systemctl可以做一个兼容层函数比如在进入业务逻辑前先拦截调用if ! systemctl is-system-running /dev/null 21; then echo systemd unavailable, running service in foreground mode 2 # 这里直接 exec 对应服务的前台命令 fi这种做法不优雅但胜在改动小、能快速跑通。适合临时验证、本地调试不适合作为正式镜像的长期方案。4.2 方案 B把服务进程放到前台让业务进程成为 PID 1方案 B 和 A 思路一脉相承但更彻底直接改 Dockerfile把 ENTRYPOINT 和 CMD 设计成前台进程。好处是镜像在任何环境下都能跑不依赖 systemdCI/CD、Kubernetes 里也不会炸。一个典型对比假设你要打包一个带 cron 的镜像。幼稚的写法是启动时service cron start或者systemctl start cron这两种都会遇到同样的问题。成熟做法是直接让 cron 以前台模式跑FROM ubuntu:22.04 RUN apt-get update apt-get install -y cron CMD [cron, -f]这里的关键是最后一个进程不能退出否则容器就结束了。所以如果有多个进程要同时跑你需要一个进程管理器但不像 systemd 那么重。常见的几个方案tini、s6-overlay、supervisord。我个人习惯用tini因为 Docker 本身也内建了--init选项它会注入一个轻量 init 来回收僵尸进程。docker run -it --init ubuntu:22.04 bash但要再强调一次tini解决的是进程管理和信号转发不是 systemd。它能让你前台进程跑得更稳健但救不了systemctl的调用。方案 B 的真正意思是调整整体设计让没必要的东西彻底不出现在容器里。4.3 方案 C必须用 systemd 时特权模式 systemd 基础镜像如果你确实要测 systemd 单元文件或者某个教程序列依赖 systemd 环境那可以尝试在容器里内嵌 systemd。Docker Hub 上有现成的 systemd 基础镜像比如jrei/systemd-ubuntu它已经配好了 PID 1 入口。启动命令大致长这样docker run -d --name systemd-ubuntu \ --privileged \ --cgroupnshost \ -v /sys/fs/cgroup:/sys/fs/cgroup:rw \ jrei/systemd-ubuntu:22.04这段命令做了三件事特权模式放开系统调用和挂载权限--cgroupnshost让容器与宿主共享 cgroup 命名空间再手动挂载/sys/fs/cgroup。在原生 Linux 上这套组合的成功率比较高。但在 macOS 的 Docker Desktop 上我的实测体验很一般新版本 Docker Desktop 底层虚拟机默认用 cgroup v2容器内再套一层 systemd 挂载 cgroup 时常报各种资源控制错误。所以这条方案我只推荐给本地虚拟机没装、临时看一眼 systemd 单元行为的场景。真正常态化使用我建议直接看下一个方案。4.4 方案 D需要完整 init 环境直接换 Linux 虚拟机如果你的项目要跑完整的 systemd 环境——比如测试开机自启服务、验证systemctl restart行为、模拟一台真正的主机——那就别在容器里勉为其难了。macOS 上最稳定的做法是用轻量级 Linux 虚拟机。我最近常用的是 MultipassCanonical 出品一条命令就能拉起 Ubuntu 虚拟机里面是完整 systemdbrew install multipass multipass launch ubuntu --name dev --cpus 2 --mem 4G --disk 20G multipass shell dev进入 shell 后systemctl status直接可用跟真正的 Linux 服务器行为完全一致。这种方式还有个额外好处你在 VM 里怎么折腾都不会影响 macOS 宿主跑坏了删掉重建就行不会像 Docker 容器那样陷入权限不够、挂载失败的泥潭。4.5 四套方案怎么选一张对照表我整理了一张对照表方便你按自己的场景快速选择。场景推荐方案理由跑单个 Web 服务、开发调试方案 A / B不需要 systemd前台进程最简单写启动脚本偶尔用到 systemctl 但可绕过方案 A改动最小改命令或加兼容层必须验证 systemd 单元文件、服务依赖方案 D虚拟机容器内 systemd 限制多容易踩坑临时复现一个依赖 systemd 的安装脚本方案 C / D看时间成本选长期用选 DCI 流水线里模拟 Linux 服务行为方案 B 或专用 runnerCI 中容器内跑 systemd 更痛苦四个方案不是互斥的完全可以混合使用。我的默认倾向是能用前台进程解决的绝不碰 systemd真的需要 systemd直接开虚拟机。5. 实操中容易误判的三个坑日志、特权模式与 Docker Desktop 的资源边界5.1 坑 1服务启动失败 ≠ systemd 未运行排查报错时有个常见的逻辑陷阱容器里某个服务起不来终端里又看到 systemd 相关的字眼于是认定是 systemd 没跑导致。但实际可能只是服务本身的配置文件写错了或者端口被占用systemd 词汇只是恰好出现在日志旁边。我遇到过一次很典型的例子某个 Java 应用在容器里启动失败日志里有一行指向systemd的上下文我围着 systemd 排查了半小时最后发现是 JVM 参数里堆内存设置超过了容器内存限制。所以先冷静把报错日志完整读一遍确认进程退出到底是因为权限、资源还是确实在调用 systemd 接口。用docker logs和docker events辅助判断别被报错里的关键词带偏。5.2 坑 2--privileged 并不能唤醒 systemd有朋友一看到 systemd 报错第一反应就是给容器加--privileged然后跑一个普通的ubuntu镜像指望 systemd 能活过来。测试方法也很直接容器起来后执行systemctl status。结果是依然报错。原因前面说过systemd 要求自己是 PID 1普通镜像的 PID 1 不是你手动执行的 systemd。特权模式解决的是 Capability 问题改变不了进程启动顺序。哪怕你手动在容器里执行/lib/systemd/systemd它也会拒绝在非 PID 1 环境下正常工作。要在容器里让 systemd 成为 PID 1必须用专门做过的镜像或者自己写 Dockerfile把 ENTRYPOINT 指向 systemd 二进制。这里面还有个隐藏细节systemd 要求挂载/run为 tmpfs、/sys/fs/cgroup可写普通容器的默认挂载不一定满足。所以就算 PID 1 解决了挂载不对照样起不来。5.3 坑 3忽视 Docker Desktop 资源限制对容器的连带影响Docker Desktop for Mac 跑容器时所有容器共享一个 Linux 虚拟机的计算和内存资源。如果你默认虚拟机只有 2 核 4G然后在里面跑一个--privileged加 systemd 的容器系统服务一多内存很容易被打满。内存不足会引发各种连锁反应服务 OOM、systemd 单元启动超时、cgroup 操作失败。这个坑的麻烦在于报错往往不是资源不足这么直白而是各种混乱的初始化报错。我当时检查 cluster 里容器一直起不来白白折腾了很久后来打开 Docker Desktop 的 Settings Resources 看内存占用才发现虚拟机压力早就爆了。建议在 Docker Desktop 里给 Linux VM 预留充足内存另外通过docker stats实时观察容器内存占用而不是等报错出来再排查。5.4 额外发现重启 Docker Desktop 后容器环境变化带来的假象还有一个容易迷惑的现象你把 Docker Desktop 重启一次某些容器里的环境表现会变化导致误判。原因是 Docker Desktop 的 Linux VM 会被重建底层内核版本、cgroup 挂载方式都可能变。比如有的版本默认 cgroup v2有的迁移到不同的虚拟化框架容器内看到的/sys/fs/cgroup结构就不一样。我建议你在排查 systemd 相关问题前先确认一下当前 Docker Desktop 底层虚拟机的内核行为变化。一个简单办法是启动一个特权容器看内核和 cgroup 版本docker run -it --privileged ubuntu:22.04 uname -a docker run -it --privileged ubuntu:22.04 stat -fc %T /sys/fs/cgroupstat -fc %T /sys/fs/cgroup输出是cgroup2fs说明走的是 cgroup v2。这一步能帮你判断某些 systemd 方案是否还有尝试的意义。6. 在 macOS 上用 systemd 的更优路径虚拟机方案与工具边界6.1 先分清你到底是哪一层需求聊完解决方案和坑最后说点更底层的认知在 macOS 上容器和虚拟机的边界应该非常清晰。容器是进程隔离虚拟机的完整操作系统隔离。凡是跟 init 系统、内核模块、系统服务管理相关的需求本质上都更靠近虚拟机那一侧。这并不是说容器不能跑 systemd——它只是需要额外配置和调用特殊的基础镜像。但从可用性和维护性来看在 macOS 上让容器兼容 systemd远比直接建一个 Ubuntu VM 更折腾。分清需求层级能帮你省掉后面所有连环坑。6.2 Multipass 十分钟建一个带完整 systemd 的 Ubuntu VM我把 Multipass 再具体展开一下因为它的体验真的接近开箱即用。安装命令brew install --cask multipass安装完成后拉取并启动一个 Ubuntu 实例multipass launch ubuntu --name dev --cpus 2 --mem 4G --disk 20G multipass shell dev进入 shell 后你可以直接执行systemctl status这里面的 systemd 是虚拟机开机阶段就启动的PID 1 就是它所有单元管理、日志查询、服务依赖都能用跟生产 Linux 服务器没区别。虚拟机文件系统挂载在宿主~/下通过multipass mount实现比如multipass mount ~/code dev:/home/ubuntu/code调试代码时修改宿主机文件虚拟机里立刻同步。这个体验对日常开发、跑需要 systemd 环境的工具链非常实用。6.3 Colima、Lima 与 Docker Desktop 的边界关系除了 MultipassmacOS 上还有 Lima 和 Colima 这类轻量虚拟机运行时。Cilima 的定位很特殊它也是通过 Lima 在 macOS 上创建 Linux 虚拟机然后在虚拟机里跑容器运行时。很多从 Docker Desktop 迁移的人会选择 Colima 作为替代。但要注意边界Colima 的虚拟机和 Multipass 类似虚拟机内部是有 systemd 的可它暴露给你的主要是 Docker 容器环境。容器内依然不会自动获得 systemd因为 Colima 跑容器的方式和 Docker Desktop 本质一样。所以迁移到 Colima 并不会解决容器内 systemd 的问题它解决的只是 license、性能和底层虚拟化偏好问题。如果你需要的是真·systemd 环境在 Multipass 这类纯 VM 工具里操作而不是经过一层容器抽象。工具选型的关键永远是看你到底想操作哪一层。6.4 我的个人使用习惯与建议说下我现在的工作流供参考。公司项目里大部分服务都用容器跑但所有涉及 systemd 的自动化脚本、服务编排验证、安装包测试我都统一放到 Multipass 虚拟机里做。容器镜像里刻意不去兼容 systemd 脚本遇到需要 systemctl 的第三方安装器能绕则绕绕不了就开一个 Linux 虚拟机。这样做的好处极其明显容器镜像保持精简和初始化简单系统服务管理行为在完整虚拟机里验证两边互不污染。Docker Desktop 持续稳定运行也不会因为 nested systemd 的暴力操作把底层 Linux VM 搞出奇怪状态。如果你经常需要在 macOS 上碰到 systemd 相关的问题强烈建议把容器内跑 systemd从选项里直接划掉。最后分享一个小经验无论用什么方案先把报错的完整原文和当时的 Dockerfile、启动命令存进笔记里。这类 init 相关问题在不同 Docker Desktop 版本下表现差异很大留档能帮你在升级 Docker Desktop 后少走弯路。真到了哪一天 macOS 上的 Docker 原生物理机支持变好这部分记录还能帮助你快速判断技术演进的方向。
返回列表