返回

Docker 容器 OpenRC softlevel 警告与替代方案

Linux

在 Docker/Balena 容器里跑 OpenRC,除了 softlevel 还有啥好招?

在 Alpine Linux 容器里用 OpenRC 启动服务,这事儿听起来有点不寻常,特别是在 Balena 这种面向嵌入式设备的场景下。常规的 Docker 玩法不太会这么干,但 Balena 把 Docker 当作应用模块,情况就不一样了。要把一个原本跑在普通 Linux 系统上的应用(依赖 systemdopenrc 启动服务)搬到 Balena 容器里,直接跑服务就成了刚需。

现在的问题是,在 Alpine 容器里,为了让 OpenRC 服务跑起来,用了 touch /run/openrc/softlevel 这个命令。虽然服务是能跑,但 OpenRC 会打印一堆黄色的警告,看着就让人心里发毛:

/lib/rc/sh/openrc-run.sh: line 108: can't create /sys/fs/cgroup/.../tasks: Read-only file system
... (一堆类似的文件系统只读错误) ...
 * You are attempting to run an openrc service on a
 * system which openrc did not boot.
 * You may be inside a chroot or you may have used
 * another initialization system to boot this system.
 * In this situation, you will get unpredictable results!
 * If you really want to do this, issue the following command:
 * touch /run/openrc/softlevel

这警告直白地告诉你:OpenRC 没能像正常开机那样接管系统,你可能是在 chroot 环境或者用了别的启动方式(比如 Docker 容器),这样跑服务结果是“不可预测”的。softlevel 模式就是官方给的“强行运行”开关。

那么,问题来了:用 softlevel 真的靠谱吗?有没有更好的办法在容器(尤其是 Alpine + Balena 环境)里管理 OpenRC 服务?这种方式会不会埋坑?特别是担心,如果后续使用这个容器镜像,覆盖了 ENTRYPOINTCMD,那是不是还得记得加上 openrc 的启动魔法,不然服务就跑不起来了?

为啥 OpenRC 会抱怨?

理解这个问题,得先明白 OpenRC 是怎么工作的,以及容器环境有啥不一样。

  1. OpenRC 的期望: OpenRC 是一个基于依赖的 init 系统,设计初衷是作为操作系统的第一个进程(PID 1)启动,负责初始化系统硬件、挂载文件系统、根据运行级别(runlevel)启动和停止服务、管理系统日志等。它期望自己完全掌控系统的启动流程。
  2. 容器的现实: Docker 容器不是一个完整的虚拟机,它和宿主机共享同一个 Linux 内核。容器启动时,ENTRYPOINTCMD 指定的命令通常是容器内的 PID 1,而不是 OpenRC。容器内的环境也相对隔离和受限,比如文件系统挂载、设备访问、系统能力(capabilities)等。
  3. /sys/fs/cgroup 只读: Cgroups(控制组)是 Linux 内核用来限制、记录、隔离进程组资源(CPU、内存、磁盘 I/O 等)的机制。OpenRC 在启动服务时,可能会尝试去 cgroup 文件系统 (/sys/fs/cgroup) 下创建或写入任务文件,以便管理服务的资源。但在标准的 Docker 容器里,这个目录通常是从宿主机挂载过来的,并且默认可能是只读的,或者容器缺乏修改它的权限(需要 CAP_SYS_ADMIN 等)。这就是那一堆 "Read-only file system" 错误的来源。
  4. softlevel 的作用: touch /run/openrc/softlevel 这个命令的作用,本质上是告诉 OpenRC:“我知道现在不是标准的启动环境,别检查那个了,继续干活吧。” 它绕过了 OpenRC 对启动环境的检查,允许 rc-serviceopenrc 脚本在非 PID 1 且环境可能不完全符合预期的容器内运行。但它解决不了底层的权限问题,比如写入 cgroup。

所以,OpenRC 的警告是有道理的。你确实在它不期望的环境里运行它。结果“不可预测”主要指的是:

  • 依赖完整系统启动环境(特定设备、挂载点)的服务可能失败。
  • 资源管理(通过 cgroups)可能失效。
  • 服务间的依赖关系处理可能不如预期。
  • 关机或重启流程可能不正常(容器里通常不关心这个)。

解决方案

既然硬性要求是必须在容器里跑这些服务,而且不能改应用代码,那就得找个相对稳妥的方式来管理它们。下面提供几种思路:

方案一:接受 softlevel,理解并缓解其影响

这是最直接的方法,既然它能用,那就继续用,但要清楚它的局限性,并尽量规避风险。

  • 原理与作用: 如前所述,softlevel 让 OpenRC 忽略启动环境检查。对于那些不严重依赖 cgroup 管理或特殊系统资源的服务,这种方式往往也能正常工作。那些 cgroup 写入失败的警告,如果服务本身不依赖这些特定的 cgroup 操作,可能只是噪音,并不会影响核心功能。
  • 实施步骤:
    • Dockerfile 中添加:
      RUN mkdir -p /run/openrc && touch /run/openrc/softlevel
      
    • 或者在容器启动脚本 (ENTRYPOINTCMD 调用的脚本) 的开头执行:
      #!/bin/sh
      mkdir -p /run/openrc
      touch /run/openrc/softlevel
      
      # 接下来启动你的服务,例如:
      # rc-service my-service start
      
      # 或者启动 OpenRC 默认运行级别的服务 (如果配置了的话)
      # openrc default # 这通常在容器里不是好的做法,最好明确启动所需服务
      
      # 最后 exec 你的主应用,或者保持脚本运行来管理服务
      exec your-main-application "$@"
      
  • 处理 cgroup 错误:
    • 评估影响: 首先确认你的服务是否真的因为 cgroup 写入失败而运行不正常。很多时候,这些只是 OpenRC 的例行操作,失败了也不影响服务。
    • 赋予权限(谨慎使用): 如果确认必须解决 cgroup 写入问题,可以考虑给容器增加权限。在 docker-compose.yml (Balena 通常使用这个) 中可以添加:
      services:
        my-alpine-service:
          image: your-alpine-image
          privileged: true # 非常不推荐!赋予容器几乎所有宿主机权限
          # 或者更精细地赋予所需能力 (更安全)
          # cap_add:
          #   - SYS_ADMIN # 允许执行一系列管理操作,包括 cgroup
          # volumes: # Docker Desktop 等可能需要挂载 cgroup
          #  - /sys/fs/cgroup:/sys/fs/cgroup:rw
      
      安全建议: 强烈反对使用 privileged: true 。这会打破容器的隔离性,带来严重的安全风险。如果必须,优先考虑使用 cap_add 精确添加最小所需权限,比如 SYS_ADMIN(即便如此,也要非常小心)。同时,确认 BalenaOS 是否允许以及如何安全地修改 cgroup 权限。可能需要调整 BalenaOS 本身的配置或容器的运行时选项。
    • 修改 OpenRC 脚本(不推荐): 技术上可以修改 /lib/rc/sh/openrc-run.sh 来抑制错误或跳过 cgroup 操作,但这会使镜像难以维护和更新。
  • 关于 ENTRYPOINT/CMD 覆盖: 如果 softlevel 的设置和服务的启动逻辑放在了 ENTRYPOINT 调用的脚本里,那么用户覆盖 CMD 是没问题的(CMD 的内容会作为参数传给 ENTRYPOINT 脚本)。但如果用户用 docker run --entrypoint ... 或在 docker-compose.yml 里指定了新的 entrypoint,那么原始的设置和服务启动逻辑就会被完全替换掉,服务自然也就起不来了。这是基于 ENTRYPOINT 设计的固有行为。使用 softlevel 方案时,需要文档说明或约定,如果需要自定义启动命令,应该基于原有的 entrypoint 脚本进行扩展,而不是完全替换。

方案二:使用进程管理器 (Supervisord, s6-overlay等)

这通常是在容器内管理多个进程("服务")的推荐做法 。用一个专为容器设计的轻量级进程管理器来替代 OpenRC。

  • 原理与作用: Supervisord 这类工具本身并不模拟一个完整的 init 系统,而是作为一个简单的进程监控器。你配置好需要运行的程序(你的"服务"),Supervisord 会负责启动它们、监控它们的状态,并在它们意外退出时尝试重启。它通常作为容器的 PID 1 (或者由一个极简的 init 脚本启动)。这样就完全绕开了 OpenRC 和它对启动环境的依赖。
  • 实施步骤 (以 Supervisord 为例):
    • 安装 Supervisord:Dockerfile 中添加:
      RUN apk add --no-cache supervisor
      
    • 创建配置文件: 创建一个 supervisord.conf 文件,例如:
      [supervisord]
      nodaemon=true ; 在前台运行,适合容器
      logfile=/dev/null ; 或者指定日志文件路径
      logfile_maxbytes=0
      pidfile=/var/run/supervisord.pid
      
      [program:my-first-service]
      command=/path/to/your/service1 --foreground ; 服务需要能运行在前台
      autostart=true
      autorestart=true
      stderr_logfile=/dev/stderr ; 将错误输出到容器日志
      stderr_logfile_maxbytes=0
      stdout_logfile=/dev/stdout ; 将标准输出到容器日志
      stdout_logfile_maxbytes=0
      
      [program:my-second-service]
      command=/usr/sbin/another-service -c /etc/another.conf
      autostart=true
      autorestart=true
      # ... 其他配置 ...
      
      注意: Supervisord 管理的程序通常需要能运行在前台 (foreground)。如果你的服务默认是后台运行的 (daemonize),需要找到让它在前台运行的参数,或者用一个简单的包装脚本来保持它在前台。
    • 拷贝配置并设置启动命令:Dockerfile 中添加:
      COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
      # 清理掉可能残留的 softlevel 文件 (如果之前用了)
      RUN rm -f /run/openrc/softlevel
      CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
      
  • 优点:
    • 专为容器场景设计,更轻量、更稳定。
    • 避免了 OpenRC 的环境依赖和 cgroup 问题。
    • 配置清晰,易于管理多个进程。
    • 通常不需要额外的系统权限。
    • 对于 ENTRYPOINT/CMD 的行为更可预测。默认情况下,supervisord 会成为主进程。
  • 安全建议: Supervisord 本身通常不需要特权。但它启动的子进程如果需要特殊权限,那么这些权限还是得通过 cap_add 或其他方式赋予容器。
  • 进阶使用: Supervisord 支持事件监听、进程组、XML-RPC 接口等高级功能,可以实现更复杂的进程管理逻辑。s6-overlay 是另一个更强大、更符合 Unix 哲学但配置也更复杂的替代方案,特别擅长处理进程依赖和信号传递。

方案三:自定义 Entrypoint 脚本启动服务

如果你不想引入 Supervisord 这样的额外依赖,或者需要更精细地控制启动流程,可以编写一个自定义的入口脚本。

  • 原理与作用: 创建一个 entrypoint.sh 脚本作为容器的 ENTRYPOINT。这个脚本负责执行所有必要的初始化步骤(比如创建 softlevel 文件,如果坚持用 OpenRC 的话),然后手动启动所需的服务,最后用 exec 切换到容器的主应用进程(或者保持脚本运行作为监控者)。
  • 实施步骤:
    • 创建 entrypoint.sh 脚本:
      #!/bin/sh
      set -e # 脚本出错即退出
      
      echo "容器启动脚本开始执行..."
      
      # 可选项:如果仍需使用 OpenRC 服务,则创建 softlevel
      # mkdir -p /run/openrc
      # touch /run/openrc/softlevel
      # echo "softlevel 已创建 (如果需要 OpenRC)"
      
      # 手动启动服务 (示例)
      # 方法一:如果坚持用 rc-service (需要 softlevel)
      # echo "启动 my-service 服务..."
      # rc-service my-service start
      # sleep 2 # 等待服务启动 (简单粗暴的方式)
      
      # 方法二:直接后台运行服务程序 (可能更干净)
      echo "直接启动后台服务 program1..."
      /path/to/program1 &
      PID1=$!
      echo "直接启动后台服务 program2..."
      /path/to/program2 &
      PID2=$!
      
      echo "服务已启动,准备执行主命令: $@"
      
      # 等待后台服务退出 (简单实现,仅示例)
      # 在实际应用中,可能需要更复杂的信号处理和监控逻辑
      # 或者直接让脚本结束,依赖 Docker 重启策略
      # wait $PID1
      # wait $PID2
      
      # 或者,如果你的主命令是一个长时间运行的应用,用 exec 替换当前 shell
      # 这样主应用就能直接接收来自 Docker 的信号 (如 SIGTERM)
      exec "$@"
      # 如果没有提供主命令 (CMD),脚本可以在这里保持运行
      # 例如,使用 tail -f /dev/null 或其他等待机制
      # echo "没有提供主命令,脚本将保持运行..."
      # tail -f /dev/null
      
    • Dockerfile 中使用脚本:
      COPY entrypoint.sh /usr/local/bin/
      RUN chmod +x /usr/local/bin/entrypoint.sh
      ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
      # CMD 可以是你的主应用命令
      CMD ["your-main-application", "--arg1"]
      
  • 优点:
    • 完全控制启动流程,灵活性高。
    • 可以不依赖 OpenRC 或 Supervisord。
    • 可以很好地处理 ENTRYPOINT/CMD 的关系:脚本总会先执行,CMD 作为参数传入。
  • 缺点:
    • 需要自己编写和维护脚本,处理进程监控、重启、信号传递等会比较麻烦。如果服务很多,脚本会变得复杂。
    • 直接后台运行服务,缺乏健壮的监控和自动重启机制(除非自己实现)。
  • 安全建议: 脚本本身无特殊权限需求,但它启动的服务可能需要。
  • 进阶技巧: 在脚本中使用 trap 捕获 SIGTERM, SIGINT 等信号,实现服务的优雅关闭。使用 exec "$@" 来执行 CMD 可以让主应用正确接收 Docker 发送的信号,这是很重要的实践。如果需要监控后台进程,可以考虑用 wait 或更复杂的循环检查逻辑。

怎么选?

  • 想快速解决,且服务不多、对 cgroup 依赖不强? 可以继续用 方案一 (softlevel) ,但要明白风险,观察服务运行情况。搞清楚 ENTRYPOINT/CMD 如何配合。
  • 需要管理多个服务,追求稳定可靠、符合容器最佳实践? 强烈推荐 方案二 (Supervisord 或 s6-overlay) 。这是目前在容器内管理多进程最标准、最成熟的方法。
  • 服务不多,不想加新依赖,且对启动流程有特殊要求? 可以考虑 方案三 (自定义 Entrypoint) ,但要有自己处理进程管理的心理准备。
  • Balena 环境特殊性: BalenaOS 可能提供了一些机制来简化服务管理或权限处理。查阅 Balena 的文档,看是否有推荐的模式来处理这种情况,可能会有比标准 Docker 更便捷的方法。例如,BalenaOS 本身可能就使用了某个 init 系统或进程管理器,可以与其集成。

总的来说,虽然 softlevel 能让 OpenRC 在容器里跑起来,但它更像是一个“兼容模式”下的 workaround。警告信息指出的潜在问题是真实存在的。从长远和稳定性考虑,采用 Supervisord 这类专为容器设计的进程管理器通常是更好的选择。它能让你更清晰、更可靠地管理容器内的服务,同时避开 OpenRC 在非标准环境下的水土不服。