Скрипт Sidekiq для runit слишком ненадёжен

Привет, команда,

Сообщаю о сбое в официальной сборке 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. Любое расхождение во владении tmp/pids или общих путях может привести к выходу Sidekiq при запуске под пользователем www-data, хотя ручной запуск от имени 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 (доступна для записи группе).
  • logrotate отказывается выполнять ротацию, если не задана глобальная директива su. Это ожидаемое поведение с точки зрения разработчиков.

В момент, когда ежедневная задача logrotate завершалась с ошибкой, она также пересоздавала файлы в /shared/log/rails/ (включая sidekiq.log), что, вероятно, взаимодействовало с принудительным логированием через -L и способствовало циклу падений Sidekiq («1s crash»).

Исправление (пересборка не требуется)

  1. Исправить logrotate, чтобы он перестал трогать общие логи в случае сбоя: Добавить глобальную директиву su:
# /etc/logrotate.conf (в начале файла)
su root adm

После этого logrotate -v завершается с кодом 0 и больше не сообщает об небезопасных правах родительского каталога.

  1. Заменить скрипт runit для Sidekiq на более надёжный вариант по умолчанию
    Переход на пользователя 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 показывает run.
  • AI и фоновые задачи возобновляют работу.

Запрос / предложение

Можно ли рассмотреть возможность повышения надёжности официального сервиса Sidekiq в сборке Docker/runit по умолчанию?

Например:

  • Запускать Sidekiq под пользователем discourse:discourse (что соответствует типичному владению внутри контейнера).
  • Использовать bundle exec sidekiq -C config/sidekiq.yml.
  • Избегать принудительной записи в общий файл логов через -L log/sidekiq.log или сделать это устойчивым к изменениям прав доступа при ротации логов или работе с общими томами.

Даже примечание в документации («если Sidekiq показывает down: 1s, но ручной запуск работает, проверьте /etc/service/sidekiq/run и избегайте принудительного логирования в общие файлы») очень помогло бы тем, кто разворачивает систему самостоятельно.

Готов предоставить дополнительные логи при необходимости. Спасибо!

Где вы это нашли? Sidekiq запускается через мастер unicorn для экономии памяти. В discourse_docker этого кода совершенно не видно. Похоже, вы используете очень старую настройку?

Привет — давайте переформулирую это строго на основе фактов времени выполнения из официального контейнера Docker.

Что я наблюдаю в работающем контейнере (факты)

Это официальная установка Docker с runit (стандартный рабочий процесс запуска /var/discourse; перед инцидентом пересборка не выполнялась). Внутри контейнера:

  1. Существует служба Sidekiq под управлением runit, и именно она находится под наблюдением
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 в этом образе; это служба runit, скрипт времени выполнения которой может попадать в цикл аварийных перезапусков.

Почему вы можете не увидеть точный код в

discourse_docker

Я согласен, что дословный текст может отсутствовать в репозитории, поскольку /etc/service/sidekiq/run — это артефакт времени выполнения, генерируемый/внедряемый во время сборки или запуска образа, а не обязательно файл, идентичный файлу в discourse_docker. Однако, как показано выше, это именно активная служба под наблюдением в данном официальном образе.

Что вызвало уязвимость (факты + минимальные выводы)

  • Мы также наблюдали ежедневные сбои logrotate из-за стандартных прав доступа Debian: /var/log = root:adm 0775, поэтому logrotate отказывался выполнять ротацию, пока не было добавлено глобальное su root adm.
  • При сбоях logrotate он создавал файлы заново в /shared/log/rails/, включая sidekiq.log.
  • Скрипт runit по умолчанию в этом образе использовал пользователя discourse:www-data и принудительно перенаправлял лог -L log/sidekiq.log в /shared/log, что делает Sidekiq очень чувствительным к дрейфу прав доступа в общих томах и может привести к немедленному завершению работы до появления полезных логов.

Запрос / предложение

Учитывая вышесказанное, можем ли мы рассмотреть возможность усиления защиты службы Sidekiq по умолчанию в Docker/runit?

Предлагаемые настройки по умолчанию:

  • запуск от имени пользователя discourse:discourse (соответствует типичным правам владения внутри контейнера),
  • запуск через bundle exec sidekiq -C config/sidekiq.yml,
  • избегать принудительного использования общего лога -L log/sidekiq.log (или сделать его устойчивым к ошибкам).

Это предотвратит тихий цикл аварийных перезапусков (down: 1s), который останавливает все фоновые задачи и задачи ИИ.

Готов протестировать любую ветку или коммит, на который вы укажете.

Опять… я не понимаю, откуда вы берёте своё изображение:

image

Это официальное изображение.

Это поиск по слову sidekiq в официальном Docker-образе Discourse.

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

Найдено 3 совпадения… ничего о unit-файле runit. Управление осуществляется через unicorn.

Привет — спасибо, скриншот проясняет структуру.

Я согласен, что в текущем официальном образе Sidekiq не является отдельной службой runit (нет /etc/service/sidekiq/). Он запускается через цепочку запуска службы runit для unicorn, что соответствует вашему списку в /etc/service.

Мой отчёт касается сценария сбоя во время выполнения этого пути запуска Sidekiq, независимо от того, находится ли он в отдельном модуле runit или внутри unicorn/run:

Факты из работы на моём VPS (официальный Docker, без пересборки/обновления непосредственно перед инцидентом):

  1. Фоновые задачи остановились, и ответы ИИ прекратились.

  2. Sidekiq вошёл в немедленный цикл аварийных перезапусков (down: 1s) при запуске через цепочку супервизора/запуска контейнера.

  3. Ручной запуск от имени пользователя discourse через bundle exec sidekiq -C config/sidekiq.yml работал стабильно и обрабатывал задачи, что означает, что приложение и Redis в порядке.

  4. В то же время сбои logrotate привели к пересозданию файла /shared/log/rails/sidekiq.log (и связанных путей) с другими правами доступа; после стабилизации команды запуска Sidekiq (запуск от имени discourse:discourse, использование sidekiq.yml, отказ от принудительного указания shared--L sidekiq.log) цикл аварийных перезапусков немедленно прекратился.

Таким образом, файл /etc/service/sidekiq/run может буквально отсутствовать в этом образе — согласен, — но шаг запуска Sidekiq, встроенный в службу runit для unicorn, уязвим к изменениям прав доступа в общих томах и сбоям logrotate и может незаметно остановить Sidekiq без пересборки. Это основная проблема.

Предложение: пожалуйста, рассмотрите возможность повышения надёжности запуска Sidekiq в официальном скрипте runit для unicorn (или там, где он генерируется):

  • запускать Sidekiq от имени пользователя discourse:discourse,
  • использовать bundle exec sidekiq -C config/sidekiq.yml,
  • избегать принудительного указания общего файла лога -L log/sidekiq.log (или сделать его устойчивым к сбоям).