Мне удалось это исправить. Я подождал достаточно долго, чтобы убедиться, что проблема действительно решена, и, похоже, так оно и есть.
Настройка
Discourse работает в Docker и предоставляет nginx через Unix-сокет (/var/discourse/shared/standalone/nginx.http.sock). Caddy стоит перед ним в качестве обратного прокси и передает реальный IP клиента через заголовок X-Real-IP.
Пришлось изменить две вещи: конфигурацию nginx внутри контейнера и способ запуска пересборки.
1. app.yml
## Плагины добавляются сюда
## см. https://meta.discourse.org/t/19157 для подробностей
hooks:
# Этот блок after_code — просто мой собственный список плагинов, он не имеет
# отношения к исправлению. Оставьте здесь то, что у вас уже есть.
after_code:
- exec:
[...]
# Вот важная часть.
# 1. Записывает http-level map, который превращает литерал "unix:" в 127.0.0.1.
# Префикс 00- заставляет nginx загружать этот файл перед discourse.conf.
# 2. Перезаписывает discourse.conf так, чтобы X-Forwarded-For использовал
# смэппедную переменную вместо сырого $remote_addr.
# 3. nginx -t прерывает сборку, если результат недействителен.
after_web_config:
- exec: >-
printf 'map $remote_addr $safe_remote_addr {\n "unix:" 127.0.0.1;\n default $remote_addr;\n}\nreal_ip_header X-Real-IP;\n'
> /etc/nginx/conf.d/00-safe-remote-addr.conf
- exec: >-
sed -i 's/X-Forwarded-For \$remote_addr;/X-Forwarded-For $safe_remote_addr;/g'
/etc/nginx/conf.d/discourse.conf
- exec: nginx -t
Критически важным кодом является то, что находится внутри тела after_web_config.
2. Блок Caddyfile
Скрипт ниже управляет режимом обслуживания с помощью флага-файла, поэтому вашему сайту-блоку нужно знать о нем:
forum.example.com {
request_header X-Real-IP {remote_host}
root * /var/caddy/flags
@maintenance file maintenance.flag
handle @maintenance {
respond "Идут технические работы. Мы вернемся через несколько минут." 503
}
handle {
reverse_proxy unix//var/discourse/shared/standalone/nginx.http.sock
}
}
Путь к флагу в скрипте и файл, который ищет матчер @maintenance, должны быть одним и тем же файлом. Если вы переименуете одно, переименуйте и другое, иначе режим обслуживания молча никогда не включится.
Если тот же экземпляр Caddy обслуживает и другие приложения, используйте флаг, который соответствует только блоку форума, иначе пересборка Discourse выведет из строя и эти другие приложения.
3. Скрипт пересборки
Я больше не использую ./launcher rebuild app напрямую. Я использую discourse-rebuild.sh:
#!/bin/bash
#
# discourse-rebuild.sh — Пересобирает контейнер Discourse, не оставляя за собой
# осиротевших запросов и без ошибки `invalid input syntax for type inet: "unix:"`.
#
# КОНТЕКСТ
# Discourse работает в Docker и предоставляет nginx через Unix-сокет
# (/var/discourse/shared/standalone/nginx.http.sock). Caddy действует как
# обратный прокси перед ним и передает реальный IP клиента через X-Real-IP.
#
# Команда `rebuild` уничтожает контейнер и пересоздает сокет с новым inode.
# Во время этого перехода есть два проблемных временных окна:
#
# 1) Caddy сохраняет состояние от предыдущего сокета, пока его не перезагрузят.
# 2) nginx начинает принимать соединения сразу после запуска, но Unicorn
# требуется еще ~15 секунд, чтобы начать их обслуживать.
#
# Запрос, попавший в любое из этих окон, может прийти без X-Real-IP. Тогда
# $remote_addr остается с литералом "unix:", который PostgreSQL отклоняет при
# вставке в inet-колонку -> HTTP 500.
#
# ЧТО ДЕЛАЕТ СКРИПТ
# 1. Создает флаг-файл, который переводит сайт в режим 503 (Caddy проверяет его
# при каждом запросе, поэтому это вступает в силу мгновенно и без перезагрузки).
# 2. Ждет, пока ничего, кроме самого nginx, не будет удерживать сокет открытым.
# 3. Пересобирает контейнер.
# 4. Опрашивает /srv/status через сокет, пока Unicorn не ответит 200.
# 5. Перезагружает Caddy, чтобы он подхватил новый сокет, и удаляет флаг.
# 6. Запускает наблюдателя, который логирует любые оставшиеся хиты "unix:".
#
# Если пересборка не удалась, или Discourse так и не ответил в течение окна
# опроса, флаг НЕ удаляется: сайт намеренно остается в режиме обслуживания, чтобы
# сломанный контейнер никогда не был доступен. Верните его в работу вручную:
# rm -f /var/caddy/flags/maintenance.flag
#
# ТРЕБОВАНИЯ
# - Установлен lsof и права root.
# - FLAG ниже должен указывать на тот же самый файл, который ищет матчер
# @maintenance в Caddyfile.
# - request_header X-Real-IP {remote_host} в том же блоке Caddyfile.
#
# ИСПОЛЬЗОВАНИЕ
# ./discourse-rebuild.sh
#
# ЧТОБЫ УВИДЕТЬ, ЧТО ЗАХВАТИЛ НАБЮДАТЕЛЬ
# cat /var/log/unixip-hits.log
#
set -e
FLAG=/var/caddy/flags/maintenance.flag
SOCK=/var/discourse/shared/standalone/nginx.http.sock
cleanup() {
local code=$?
systemctl reload caddy
if [ $code -eq 0 ]; then
rm -f "$FLAG"
echo "✅ Пересборка завершена. Сайт в сети."
else
echo "⚠️ Пересборка не удалась (код выхода $code). Сайт все еще в режиме обслуживания."
echo " Проверьте его, и когда будет готово: rm -f $FLAG"
fi
}
trap cleanup EXIT
mkdir -p "$(dirname "$FLAG")"
touch "$FLAG"
echo "🔧 Режим обслуживания включен. Ожидание активных запросов..."
# Грубый эвристический метод: подсчитать процессы, удерживающие сокет открытым,
# и ждать, пока останется только слушатель. До 30 секунд.
for i in $(seq 30); do
n=$(lsof -t "$SOCK" 2>/dev/null | wc -l || echo 0)
[ "$n" -le 1 ] && break
sleep 1
done
/var/discourse/launcher rebuild app
# До 60 попыток: около 2 минут сна, больше, если какой-либо curl достигнет своего
# таймаута в 5 секунд.
echo "⏳ Ожидание ответа от Discourse..."
status=000
for i in $(seq 60); do
status=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 \
--unix-socket "$SOCK" http://localhost/srv/status 2>/dev/null || echo 000)
[ "$status" = "200" ] && break
sleep 2
done
if [ "$status" != "200" ]; then
echo "⚠️ Discourse так и не ответил в течение окна опроса (последний код статуса: $status)."
exit 1
fi
echo "✅ Discourse готов примерно через $((i*2))с."
systemd-run --unit=unixip-watch --collect \
/bin/bash -c "docker exec app tail -F /var/log/nginx/access.log | grep --line-buffered 'unix:' >> /var/log/unixip-hits.log"
Что на самом деле делает скрипт
- Переводит сайт в режим обслуживания через флаг-файл Caddy.
- Ждет, пока сокет не станет «тихим», затем запускает
./launcher rebuild app. - Опрашивает
/srv/statusчерез сокет, пока Unicorn не ответит200. Это критически важная часть: именно она не дает запросам доходить до nginx, пока Unicorn еще запускается. - Перезагружает Caddy (на всякий случай, хотя оказалось, что это не критично).
- Удаляет флаг обслуживания только если этот опрос был успешен.
Спустя несколько недель и несколько пересборок я больше не видел этой ошибки.
P.S.: Я и правда не знаю, что я там наделал.