Sidekiq runit 脚本过于脆弱:**discourse:www-data** **+ forced** **-L log/sidekiq.log** **导致 1 秒崩溃

大家好,

报告一下官方 Docker/runit 设置中存在的一种故障模式,它可以在不进行任何重建或升级的情况下静默终止 Sidekiq(进而终止 AI/后台作业)。

环境

  • 官方 Discourse Docker 安装(标准容器 + runit 服务)。
  • 问题开始前没有进行重建/升级。
  • Discourse AI 插件已启用,但 AI 停止回复。

症状

  • 管理界面中 AI 显示为启用,但没有 AI 回复出现。
  • 后台作业(AI/嵌入/自动回复)似乎卡住了。
  • sv status sidekiq 显示 Sidekiq 在启动后反复崩溃:
down: sidekiq: 1s, normally up, want up
  • 手动启动 Sidekiq 运行正常,说明应用程序本身没问题:
bundle exec sidekiq -C config/sidekiq.yml
# 保持运行,连接到 Redis,处理作业

我们发现的情况

默认的 runit 脚本是:

exec chpst -u discourse:www-data \
  bash -lc 'cd /var/www/discourse && ... bundle exec sidekiq -e production -L log/sidekiq.log'

两个脆弱点:

  1. 主用户组 www-data 在我的容器中,典型的可写路径归 discourse:discourse 所有。当 Sidekiq 在 www-data 权限下启动时,tmp/pids 或共享路径的任何权限漂移都可能导致 Sidekiq 在启动时退出,即使以 discourse 用户手动启动是正常的。
  2. 强制的 -L log/sidekiq.log 写入共享日志 该日志路径是指向 /shared/log/rails/sidekiq.log 的符号链接。如果该文件/目录以不同的所有权/权限重新创建,Sidekiq 可能会立即退出,而无法产生有用的日志。

相关触发因素:logrotate 每日失败

另外,logrotate 每天都会失败并报告:

error: skipping "...\log" because parent directory has insecure permissions
Set "su" directive in config file ...

原因是标准的 Debian/Ubuntu 权限设置:

  • /var/log 是 root:adm,权限为 0775(组可写)。
  • 除非设置全局 su 指令,否则 logrotate 会拒绝轮换。这是预期的上游行为。

在每日 logrotate 作业失败的时刻,它也重新创建了 /shared/log/rails/ 下的文件(包括 sidekiq.log),这很可能与强制的 -L 日志记录相互作用,并导致了 Sidekiq 的“1秒崩溃”循环。

修复方法(无需重建)

  1. 修复 logrotate,使其停止在失败状态下触碰共享日志 添加一个全局 su 指令:
# /etc/logrotate.conf (顶部)
su root adm

之后,logrotate -v 退出代码为 0,并且不再报告父目录权限不安全。

  1. 用更健壮的默认值替换 Sidekiq runit 脚本 切换到 discourse:discourse 和标准的 sidekiq.yml,并且不强制 -L log/sidekiq.log,使 Sidekiq 稳定:
#!/bin/bash
exec 2>&1
cd /var/www/discourse

mkdir -p tmp/pids
chown discourse:discourse tmp/pids || true

exec chpst -u discourse:discourse \
  bash -lc 'cd /var/www/discourse && rm -f tmp/pids/sidekiq*.pid; exec bundle exec sidekiq -C config/sidekiq.yml'

执行此操作后:

  • sv status sidekiq 保持运行状态:
  • AI/后台作业恢复。

请求/建议

我们能否考虑默认情况下使官方 Docker/runit Sidekiq 服务更加健壮?

例如:

  • 在 discourse:discourse 用户下运行 Sidekiq(与容器内典型所有权匹配)。
  • 优先使用 bundle exec sidekiq -C config/sidekiq.yml
  • 避免通过 -L log/sidekiq.log 强制指定共享日志文件,或者使其能够抵御 logrotate/共享卷权限漂移。

即使是添加一个文档说明(“如果 Sidekiq 显示 down: 1s 但手动启动正常,请检查 /etc/service/sidekiq/run 并避免强制共享日志记录”)也会对自托管用户有很大帮助。

如果需要更多日志,我很乐意提供。谢谢!

1 个赞

您在哪里找到这个的?Sidekiq 是通过 unicorn master 启动的,以节省内存。在 discourse_docker 中完全没有看到这段代码。看起来您可能使用的是非常旧的设置?

2 个赞

您好——我将严格根据官方 Docker 容器的运行时事实重述此事。

我在运行的容器中看到的情况(事实)

这是一个官方 Docker 安装,带有 runit(标准的 /var/discourse 启动器工作流程;事件发生前没有重建)。在容器内:

  1. 存在一个 runit Sidekiq 服务,并且是正在被监控的那个
ls -l /etc/service/sidekiq/run
sv status sidekiq

事件期间的输出:

down: sidekiq: 1s, normally up, want up
  1. 手动启动 Sidekiq 成功
cd /var/www/discourse
sudo -u discourse bundle exec sidekiq -C config/sidekiq.yml

这会保持运行,连接到 Redis,并处理作业。

  1. 仅修补 /etc/service/sidekiq/run (不重建)立即修复了崩溃循环 将 /etc/service/sidekiq/run 替换为:
#!/bin/bash
exec 2>&1
cd /var/www/discourse
mkdir -p tmp/pids
chown discourse:discourse tmp/pids || true
exec chpst -u discourse:discourse \
  bash -lc 'cd /var/www/discourse && rm -f tmp/pids/sidekiq*.pid; exec bundle exec sidekiq -C config/sidekiq.yml'

之后:

sv status sidekiq
run: sidekiq: (pid <PID>) <SECONDS>s

因此,在这个官方镜像中,Sidekiq 不是通过 Unicorn master 启动的;它是一个 runit 服务,其运行时脚本可能会陷入崩溃循环。

为什么您可能看不到确切的代码在

discourse_docker 中

我同意确切的文本可能不在仓库中,因为 /etc/service/sidekiq/run 是在镜像构建/启动期间生成/注入的运行时工件,不一定是 discourse_docker 中逐字对应的文件。但如上所示,它这个官方镜像中活动的受监控服务。

触发脆弱性的原因(事实 + 最少推断)

  • 我们还观察到由于标准的 Debian 权限设置(/var/log = root:adm 0775),logrotate 每天都会失败,因此在添加全局 su root adm 之前,logrotate 拒绝轮换。
  • 当 logrotate 失败时,它会在 /shared/log/rails/ 下重新创建文件,包括 sidekiq.log。
  • 此镜像中默认的 runit 脚本使用 discourse:www-data 并强制将 -L log/sidekiq.log 写入 /shared/log,这使得 Sidekiq 对共享卷权限漂移非常敏感,并可能在有有用日志之前立即退出。

请求/建议

鉴于以上情况,我们能否考虑加强默认的 Docker/runit Sidekiq 服务?

建议的默认设置:

  • 以 discourse:discourse 身份运行(与容器内的典型所有权匹配),
  • 通过 bundle exec sidekiq -C config/sidekiq.yml 启动,
  • 避免强制共享的 -L log/sidekiq.log(或使其具有弹性)。

这将防止导致所有后台/AI 作业停止的静默的 down: 1s 崩溃循环。

我很乐意测试您指向我的任何分支/提交。

我又…对你从哪里获取图片感到困惑:

image

这是官方图片。

这是在官方 discourse docker 中搜索单词 sidekiq 的结果。

https://github.com/search?q=repo%3Adiscourse%2Fdiscourse_docker%20sidekiq&type=code

有 3 个匹配项…但没有关于 runit 单元的内容。它由 unicorn 管理。

1 个赞

您好,感谢您的截图,它有助于阐明布局。

我同意,在当前的官方镜像中,Sidekiq 不是一个独立的 runit 服务(没有 /etc/service/sidekiq/)。它是从 unicorn runit 服务的启动链中启动的,这与您的 /etc/service 列表相符。

我的报告仍然是关于该 Sidekiq 启动路径的运行时故障模式,无论它存在于独立的 runit 单元中还是在 unicorn/run 内部:

我的 VPS 上的运行时事实(官方 Docker,事件发生前没有重建/升级):

  1. 后台作业停止,AI 回复停止。
  2. 当由容器的 Supervisor/启动链启动时,Sidekiq 进入立即崩溃循环(停机时间:1 秒)。
  3. discourse 身份手动启动 bundle exec sidekiq -C config/sidekiq.yml 保持运行并处理作业,因此应用程序/redis 没问题。
  4. 与此同时,logrotate 失败导致 /shared/log/rails/sidekiq.log(及相关路径)以不同的权限重新创建;在稳定 Sidekiq 启动命令(以 discourse:discourse 身份运行,使用 sidekiq.yml避免强制使用共享的 -L sidekiq.log)后,崩溃循环立即停止。

因此,字面上的文件 /etc/service/sidekiq/run 在此镜像中可能不存在——同意——但嵌入在 unicorn runit 服务中的 Sidekiq 启动步骤对共享卷权限/logrotate 漂移很脆弱,并且可以在没有重建的情况下静默地杀死 Sidekiq。这是核心问题。

建议: 请考虑在官方 unicorn runit 脚本(或生成它的位置)中加固 Sidekiq 启动:

  • discourse:discourse 身份运行 Sidekiq,

  • 优先使用 bundle exec sidekiq -C config/sidekiq.yml

  • 避免强制使用共享的 -L log/sidekiq.log(或使其具有弹性)。