Add an offline page to display when Discourse is rebuilding or starting up

Есть ли очевидная причина, по которой я не могу загрузить .png файл размером более 500 КБ на свой форум после следования этому руководству?

Я видел обсуждения этой строки client_max_body_size 0;, но это не должно быть проблемой, верно?

РЕДАКТИРОВАНИЕ: Я быстро разобрался после публикации здесь. Нужно было проверить :white_check_mark: force https в настройках. Я оставлю этот пост здесь на случай, если у кого-то возникнет такая же проблема в будущем

3 лайка

Я заметил, что здесь:

nginx рекомендует отключать заголовок Connection: close, а также устанавливать proxy_http_version 1.1 — то есть что-то вроде этого:

    proxy_http_version 1.1;
    # Отключить заголовок "Connection: close" по умолчанию
    proxy_set_header "Connection" "";

Я не могу найти никакой документации о том, влияет ли Connection: close на сокеты домена Unix, но поскольку эта документация также полезна для запуска внешнего прокси на отдельной системе, и я ожидаю, что удаление заголовка не навредит, возможно, имеет смысл рекомендовать его удаление здесь?

1 лайк

Если вы развернули это на системе с SELinux (в режиме принуждения), вы не можете использовать unix-доменный сокет для общения хоста с контейнером, поскольку даже если вы переименуете unix-доменный сокет, он будет создан заново без метки при каждой перезагрузке контейнера. Вместо этого вам потребуется внести два изменения.

Вам нужно разрешить nginx доступ к страницам ошибок и переключиться с проксирования через unix-доменный сокет на порт. Это добавит несколько микросекунд задержки на каждый запрос — цена за запуск nginx с SELinux в качестве одного из уровней безопасности, но ваши пользователи этого не заметят.

Сначала выполните следующие команды, чтобы разрешить nginx устанавливать сетевые подключения и получать доступ к страницам ошибок:

setsebool -P httpd_can_network_connect 1
semanage fcontext -a -t httpd_sys_content_t /var/www
restorecon -R -v /var/www

Затем в вашем файле app.yaml закомментируйте или удалите строку - "templates/web.socketed.template.yml", откройте порт 80 как другой порт на локальной машине и пересоберите контейнер.

expose:
  - "8008:80"   # http

Не используйте https здесь — SSL уже завершён во внешнем nginx, а заголовок X-Forwarded-Proto сообщает Discourse, что запрос поступил по https. Убедитесь, что порт 8008 (или любой другой выбранный вами порт) не доступен публично через настройки брандмауэра.

Затем измените конфигурацию внешнего nginx: вместо проксирования через nginx.http.sock используйте http://127.0.0.1:8008 (или выбранный вами порт) и удалите заголовок Connection: close по умолчанию, чтобы внешний nginx не должен был устанавливать новое IP-соединение для каждого запроса.

...
  location / {
    proxy_pass http://127.0.0.1:8008;
    proxy_set_header Host $http_host;
    proxy_http_version 1.1;
    # Отключаем заголовок по умолчанию "Connection: close"
    proxy_set_header "Connection" "";
...
1 лайк

Привет @sam (и, возможно, @falco). Мне поручено привести в порядок некоторые из этих документов в категории #documentation:sysadmin. Этот документ имеет очень высокий процент прочтений, но я считаю, что он один из наименее полезных.

Как вы думаете, имеет смысл написать замену, которая разворачивает haproxy - Official Image | Docker Hub и nginx - Official Image | Docker Hub, возможно, с помощью Docker Compose? При этом контейнер nginx должен монтировать сертификаты из контейнера Discourse, а HAProxy в режиме TCP должен выполнять что-то вроде следующего (я уверен, что это не сработает, но предполагаю, что смогу разобраться, что именно нужно):

backend my_app_be
	balance roundrobin
	option httpchk HEAD /srv/status
        server discourse app:443 check
	server fallback nginx:80 check backup

Мне кажется, это могло бы стать рабочим решением, которое проще понять, чем эта тема. Затем я оставил бы этот пост в исторических целях (и, возможно, закрыл бы его?), но добавил ссылку на описанное выше решение.

3 лайка

Обратите внимание, что эта тема имеет значительное пересечение с той:

В той теме недавно были внесены существенные обновления (что ещё больше увеличило пересечение). Возможно, имеет смысл перенести часть про офлайн-страницу отсюда туда в виде примечания (поскольку это легко добавить, если у вас уже запущен отдельный экземпляр Nginx), а затем пометить эту тему как устаревшую (ссылкой на альтернативы)?

Ваша предложенная тема про HAProxy по-прежнему будет актуальна как дополнительный вариант, так как это стандартный путь для тех, кто не хочет устанавливать фронтенд Nginx по другим причинам.

2 лайка

Откуда мне было знать?

О.

Но если серьёзно, мне нравится ваше решение больше, чем моё!

И в той теме крупными буквами написано, что это продвинутая тема.

Но, возможно, это тоже не обязательно, поскольку большинство людей всё равно лучше понимают Nginx. Правда, я уже начал об этом думать, так что самая сложная часть будет — заставить меня остановиться. :slight_smile:

3 лайка

Всегда хорошо, когда есть множество альтернатив. Но этот вариант — один из самых простых (их довольно много…) и знакомый многим.

Поэтому, пожалуйста, не трогайте его.

К тому же, оставить всё как есть имеет ещё один плюс: результаты поиска. Из-за высокого трафика (и очень редкого использования тегов…) сейчас довольно трудно найти что-то конкретное здесь. Но этот материал легко найти, и у него очень чёткая цель. Если эту тему переместить в другое место, найти её станет ещё сложнее.

Есть причина, почему это так популярно… не у всех хватает вдохновения использовать Docker или HAProxy.

2 лайка

Вздох. Ну, я думаю, это тоже верно, но информация устарела уже как минимум на 4 года. Я не занимался этим недавно, но больше нет необходимости вручную изменять файлы — это сделает за вас acme (или что-то подобное?).

Мне действительно кажется, что гораздо логичнее использовать установку с двумя контейнерами, которая обеспечивает минимальное время простоя, вместо того чтобы проходить через все эти круги, чтобы разместить страницу во время перестройки, но я тоже не могу никого в этом убедить.

Так что, возможно, стоит переписать это с учётом того, как всё работает сегодня.

1 лайк

И это намного сложнее. Я полагаю, проще исправить инструкции по установке и использованию certbot, чем начинать объяснять, как, когда и где обновлять контейнеры, работающие на стороне SQL и т. д.

Кроме того, есть ещё один аргумент против Docker (хотя даже Discourse работает таким образом…): мы можем найти множество вопросов самого базового уровня, например, как вообще использовать Docker. Или как избежать опечаток в yml-файлах :wink:

И всё же это работает (за исключением раздела SSL, который немного запутанный, но он был таковым и 4 года назад :wink: )

Нет. Я не против других решений. Я категорически против того, чтобы старые ссылки и тексты переносились в новые места без очень веских причин.

2 лайка

Здесь нам, видимо, придётся согласиться не соглашаться. Но я полагаю, что с вами согласны многие. (Возможно, это решение типа «настроил и забыл», тогда как вариант с двумя контейнерами требует внимания к моментам обновления Postgres, которое происходит примерно раз в два года.)

Хорошо. В этом мы согласны! Поэтому, думаю, лучший путь — посмотреть, что я могу сделать, чтобы упорядочить этот момент, и пока отложить решение с haproxy.

1 лайк

Мне бы очень хотелось увидеть это реализованным в рамках Discourse, но спасибо @fefrei за это! Потрясающая работа! Я буду использовать Apache для этого, но, по крайней мере, базовые шаги должны быть одинаковыми.

1 лайк

Окей, потребовалось всего 2 часа возни, чтобы сделать как мне нужно!

Страница обслуживания Discourse с Apache2

Как root:

cd /var/discourse
nano containers/app.yml

Закомментируйте эти строки:

  #- "templates/web.ssl.template.yml"
  #- "templates/web.letsencrypt.ssl.template.yml"

expose:
  #- "80:80"   # http
  #- "443:443" # https

В конце раздела templates добавьте (обязательно в самом конце):

  - "templates/web.socketed.template.yml"

Примечание: это заставит Discourse слушать только внутренний IP-адрес, а apache2 перехватит порты 80/443 и возьмёт на себя завершение SSL.

Примечание: для применения изменений Discourse необходимо пересобрать:

cd /var/discourse
./launcher rebuild app

Установите apache2 и certbot:

apt install -y apache2 certbot python3-certbot-apache

Создайте директорию для HTML-страницы:

mkdir /var/www/discourse_maintenance

HTML-страница:
/var/www/discourse_maintenance/discourse_maintenance.html

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="refresh" content="5">
        <title>Discourse Maintenance</title>
        <style>
            .center {
                display: flex;
                justify-content: center;
            }
            .container {
                max-width: 500px;
                padding: 50px 50px 30px 50px;
            }
            .title {
                padding-top: 20px;
            }
            h1, p {
                font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
            }
        </style>
    </head>
    <body>
        <div class="center">
            <div class="container">
                <h1 class="title">Discourse Maintenance&hellip;</h1>
                <p>В данный момент мы обновляем сайт или проводим плановое техническое обслуживание.</p>
                <p>Как только сайт будет доступен, вас автоматически перенаправит на него.</p>
            </div>
        </div>
    </body>
</html>

Включите модуль Proxy:

a2enmod proxy
a2enmod proxy_http
a2enmod headers

Файл vhost для Apache:

<IfModule mod_ssl.c>
<VirtualHost *:443>
  ServerName ваш.discourse.домен
  ServerAdmin ваш@email.com
  DocumentRoot /var/www/discourse_maintenance

  ErrorLog ${APACHE_LOG_DIR}/error.log
  CustomLog ${APACHE_LOG_DIR}/access.log combined

  # Режим обслуживания
  RewriteEngine On
  RewriteCond /var/www/under_maintenance -f
  # Проверка безопасности для предотвращения циклов перенаправления
  RewriteCond %{REQUEST_URI} !/discourse_maintenance.html$
  # Перенаправление всех запросов на maintenance.html внутри сервера
  RewriteRule ^.*$ /var/www/discourse_maintenance/discourse_maintenance.html

  ProxyPass / unix:///var/discourse/shared/standalone/nginx.http.sock|http://127.0.0.1/
  ProxyPassReverse / unix:///var/discourse/shared/standalone/nginx.http.sock|http://127.0.0.1/

  SSLCertificateFile /etc/letsencrypt/live/ваш.discourse.домен/fullchain.pem
  SSLCertificateKeyFile /etc/letsencrypt/live/ваш.discourse.домен/privkey.pem
  Include /etc/letsencrypt/options-ssl-apache.conf
</VirtualHost>
</IfModule>

Чтобы включить режим обслуживания, выполните touch /var/www/under_maintenance

Чтобы выключить режим обслуживания, выполните touch /var/www/under_maintenance

Благодарности: Add an offline page to display when Discourse is rebuilding or starting up — за первоначальную идею и HTML-страницу (сокращённую и отредактированную по моему вкусу), а также за конфигурацию nginx, на основе которой я построил конфигурацию Apache.

Редактирование: приветствуются предложения по автоматизации процесса, когда ответом является 502/503. Я пытался, но не смог заставить это работать так, как хотел, поэтому выбрал известный метод, который использую на других веб-серверах, когда бэкенд-приложение недоступно для обслуживания и т.п.

2 лайка

При перезагрузке системы это откладывает появление страницы ошибки/техобслуживания до тех пор, пока не запустится docker, что занимает значительно больше времени, чем сама загрузка системы. Кроме того, это лишает возможности использовать встроенные защиты SELinux для системного nginx. Использование системного nginx, по крайней мере на системе с управлением systemd и быстрой загрузкой, позволяет получить страницу ошибки в течение нескольких секунд после запуска. Для меня это означает, что мои системы очень быстро отвечают страницей техобслуживания во время обновлений, требующих перезагрузки. (Я использую AlmaLinux 9 на хосте, и он загружается до nginx очень быстро.)

Возможно, имеет смысл документировать альтернативу с haproxy и сравнивать опыт, но haproxy в docker не является прямым аналогом внешнего nginx, и закрытие этой темы было бы ошибкой.

Дел в том не только в доступности.

Использование docker для внешнего трафика через IPv4 скрывает внешние IPv6-адреса от внутреннего nginx и Discourse. С haproxy возникнет та же проблема. Посмотрите свои логи на наличие 127.0.0.1 или адресов из локального пространства RFC1918 172.*. Отсутствие внешнего прокси означает, что весь IPv6-трафик отображается как один и тот же IP, что ломает внутреннее ограничение скорости зон в nginx, рассматривая весь IPv6-трафик как одну зону.

IPv6 становится всё более важным.

2 лайка

Сегодня утром я случайно обнаружил, что этот шаг не только исключает использование unix-сокета, но и также отключает модуль real_ip, благодаря чему ограничение скорости применяется ко всем подключениям в совокупности, а не к подключениям с каждого IP-адреса в отдельности. Мне, вероятно, стоит внести вклад в создание нового шаблона с переменными, но пока я просто добавил это в свой YAML-файл конфигурации контейнера приложения:

run:
  - replace:
     filename: "/etc/nginx/conf.d/discourse.conf"
     from: /listen 80;/
     to: |
       listen unix:/shared/nginx.http.sock;
       set_real_ip_from 172.0.0.0/24;
  - replace:
     filename: "/etc/nginx/conf.d/discourse.conf"
     from: /listen 443 ssl http2;/
     to: |
       listen unix:/shared/nginx.https.sock ssl http2;
       set_real_ip_from 172.0.0.0/24;

Не уверен, имеет ли смысл создать, например, файл templates/web.httpratelimit.yml с подобной конфигурацией, где будет переменная для адреса, но без использования доменных сокетов Unix. Что вы об этом думаете?

2 лайка
server {
  listen 80; listen [::]:80; listen 443 ssl http2; listen [::]:443 ssl http2;
  server_name DOMAIN;
  ssl_certificate      /etc/letsencrypt/live/DOMAIN/fullchain.pem;
  ssl_certificate_key  /etc/letsencrypt/live/DOMAIN/privkey.pem;

  ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
  ssl_protocols TLSv1.2;
  ssl_prefer_server_ciphers on;
  ssl_session_cache shared:SSL:10m;

  add_header Strict-Transport-Security "max-age=63072000;";
  ssl_stapling on;
  ssl_stapling_verify on;

  client_max_body_size 0;

  location / {
    error_page 502 =502 /errorpages/offline.html;
    proxy_intercept_errors on;

    proxy_pass http://unix:/var/discourse/shared/standalone/nginx.http.sock:;
    proxy_set_header Host $http_host;
    proxy_http_version 1.1;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;
  }

  location /errorpages/ {
    alias /var/www/errorpages/;
  }
}

! # Измените домен и путь к файлу ошибок

У меня этот скрипт с SSL работает.

2 лайка

Я только что попробовал воспользоваться этим руководством, но у меня не получилось заставить его работать.

Я начал с использования Nginx-прокси для запуска двух сайтов в одном контейнере Discourse. Мне нужно было лишь добавить блок со страницами ошибок, поэтому я пропустил части, которые, казалось, дублировали информацию из Run other websites on the same machine as Discourse. Однако, должно быть, я упустил какой-то ключевой шаг. В итоге я получил то, что мне нужно, из этого руководства DigitalOcean. Настроить это вручную не сложно, но, кажется, должно существовать более простое решение.

Учитывая, что Docker является стандартным способом запуска Discourse, это звучит лучше. Я предполагаю, что это будет решение, которое вы настраиваете один раз и больше не возвращаетесь к нему.

3 лайка

Идея, обсуждаемая в этой теме, отлично подходит и для тех, кто использует Caddy в качестве обратного прокси, будь то в виде отдельного приложения или с использованием Cloudflare Tunnels.

discourse.example.org {
        reverse_proxy <host | ip>:port

        handle_errors 5xx {
                root * /path/to/error-pages
                rewrite * /error.html
                file_server {
                        status 404
                }
        }
}

Раздел status 404 важен только при использовании Cloudflare Tunnels. Если Caddy возвращает Cloudflare ошибку 5xx, Cloudflare Tunnel отобразит собственную ошибку отключения. Изменение статуса указывает Cloudflare, что существует активное соединение, которое сможет отдать страницу ошибки.

2 лайка

Возможно, я неправильно понимаю, как это работает, но разве обновление просто не перезагружает ту страницу, на которой вы уже находитесь? Как это возвращает вас на другой URL?

Переход на другой URL не требуется — суть в том, что страница ошибки отображается непосредственно по тому адресу, который пытался открыть пользователь (например, https://meta.discourse.org/t/add-an-offline-page-to-display-when-discourse-is-rebuilding-or-starting-up/45238/158), и именно этот адрес будет обновлён, показав либо ту же ошибку, либо нужную пользователю страницу :slight_smile:

А, понятно. Я не дочитал внимательно всю настройку, так как использую другой сервер для офлайн-страницы — на случай, если моя машина по какой-то причине полностью выйдет из строя. Хотя это логично. Сейчас я просто пытаюсь заставить свой JS-скрипт, который должен перенаправлять обратно на исходный URL, работать…