Agregar una página offline para mostrar cuando Discourse se está reconstruyendo o iniciando

¿Hay alguna razón obvia por la que no puedo subir un .png de más de 500kb a mi foro después de seguir esta guía?

Veo que se habla de esta línea client_max_body_size 0; pero eso no debería ser el problema, ¿verdad?

EDITADO: Lo descubrí rápidamente después de publicar aquí. Necesitaba marcar :white_check_mark: forzar https en la configuración. Dejaré esta publicación aquí en caso de que futuras personas tengan un problema.

3 Me gusta

Tomo nota de esto:

nginx recomienda deshabilitar la cabecera Connection: close, así como establecer proxy_http_version 1.1, algo así:

    proxy_http_version 1.1;
    # Deshabilitar \"Connection: close\" por defecto
    proxy_set_header "Connection" "";

No encuentro documentación sobre si Connection: close tiene algún impacto en los sockets de dominio Unix, pero dado que esta documentación también es útil para ejecutar un proxy externo en un sistema separado y esperaría que eliminar la cabecera no perjudique, ¿podría tener sentido recomendar su eliminación aquí?

1 me gusta

Si ha implementado esto en un sistema con SELinux (enforcing), no podrá usar el socket de dominio Unix para que el host se comunique con el contenedor, porque incluso si se vuelve a etiquetar el socket de dominio Unix, se recreará sin la etiqueta cada vez que reinicie el contenedor. En su lugar, tendrá que realizar dos cambios.

Deberá permitir que nginx acceda a las páginas de error y cambiar de proxy a través de un socket de dominio Unix a un puerto. Esto implicará un par de microsegundos de latencia por solicitud, ya que el costo de ejecutar nginx con SELinux como una capa de seguridad no será perceptible para sus usuarios.

Primero, ejecute estos comandos para permitir que nginx realice conexiones de red y acceda a las páginas de error:

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

Luego, en su archivo app.yaml, comente o elimine - "templates/web.socketed.template.yml", exponga el puerto 80 como un puerto diferente en la máquina local y reconstruya el contenedor.

expose:
  - "8008:80"   # http

No use https aquí; ha terminado SSL en el nginx externo y la cabecera X-Forwarded-Proto le indica a Discourse que la solicitud llegó a través de https. Asegúrese de que el puerto 8008 (o cualquier otro puerto que haya elegido) no esté expuesto públicamente por la configuración de su firewall.

Luego, modifique la configuración de su nginx externo para que proxy a través de nginx.http.sock a http://127.0.0.1:8008 (o el puerto elegido) y elimine la cabecera predeterminada Connection: close, para que el nginx externo no tenga que establecer una nueva conexión IP para cada solicitud.

...
  location / {
    proxy_pass http://127.0.0.1:8008;
    proxy_set_header Host $http_host;
    proxy_http_version 1.1;
    # Deshabilitar \"Connection: close\" predeterminado
    proxy_set_header "Connection" "";
...
1 me gusta

Hola @sam (y quizás @falco). Tengo la tarea de limpiar algunos de estos documentos de #documentation:sysadmin. Este tiene muchas lecturas y creo que es uno de los menos útiles.

¿Crees que tiene sentido escribir un reemplazo que inicie haproxy - Official Image | Docker Hub y nginx - Official Image | Docker Hub, quizás con docker compose, haciendo que el contenedor nginx monte los certificados del contenedor discourse y tenga haproxy en modo tcp para hacer algo como esto (estoy seguro de que esto no funcionará, pero supongo que podré averiguar qué funcionará):

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

Creo que esa podría ser una solución viable que sea más fácil de seguir que este tema. Luego dejaría este para fines históricos (¿y quizás lo cerraría?) pero enlazaría al descrito anteriormente.

3 Me gusta

Tenga en cuenta que este tema se solapa significativamente con aquel:

Ese tema ha recibido actualizaciones significativas últimamente (lo que aumentó aún más el solapamiento). Quizás tenga sentido fusionar la parte de la página sin conexión de aquí a allí como una nota (ya que es fácil de añadir si ya ejecuta una instancia separada de Nginx), y luego marcar este como obsoleto (enlazando a las alternativas).

Su tema sugerido de HAProxy seguiría teniendo sentido además, como la forma predeterminada para las personas que no desean instalar un Nginx frontal por otras razones.

2 Me gusta

¿Cómo se suponía que lo iba a saber?

Oh.

¡Pero en serio, me gusta más tu solución que la mía!

Y ese tema dice en letras grandes que es un tema avanzado.

Pero tal vez tampoco sea necesario, ya que la mayoría de la gente entiende mejor nginx de todos modos. Sin embargo, he empezado a pensar en ello, así que la parte difícil será conseguir que me detenga. :slight_smile:

3 Me gusta

Siempre es bueno si hay muchas alternativas. Pero esta es una de las más fáciles (bastantes…), y familiar para muchos.

Así que por favor, no toques esta.

Y dejar esto como está tiene otro punto: los resultados de búsqueda. Debido al alto tráfico (y al uso muy limitado de etiquetas…) intentar encontrar algo específico aquí es bastante difícil hoy en día. Pero esta es bastante fácil de encontrar y tiene un propósito muy específico. Si este tema se traslada a otro, encontrarlo será más difícil.

Hay una razón por la que esto es tan popular… no a muchos se les ocurre usar docker o haproxy.

2 Me gusta

Suspiro. Bueno, supongo que eso también es cierto, pero tiene al menos 4 años de antigüedad. No lo he hecho últimamente, pero ya no necesitas modificar los archivos manualmente, ya que acme (¿o algo parecido?) lo hará por ti.

Lo que realmente creo es que tiene mucho más sentido usar una instalación de dos contenedores, que tiene poco tiempo de inactividad en lugar de pasar por estos aros para poner una página en línea mientras reconstruyes, pero tampoco puedo convencer a la gente de eso.

Así que tal vez lo que hay que hacer es reescribir esto para cómo funcionan las cosas hoy en día.

1 me gusta

Y es mucho más difícil. Creo que es más fácil arreglar las instrucciones sobre cómo instalar y usar certbot que empezar a enseñar cómo, cuándo y dónde actualizar el lado sql, etc., los contenedores.

Además, hay otro punto en contra de docker (incluso Discourse funciona de esa manera…): podemos encontrar muchas preguntas de nivel totalmente básico como cómo usar docker en primer lugar. O cómo evitar errores tipográficos en los archivos yml :wink:

Y aún así funciona (excepto que la sección SSL es un poco confusa, pero ya lo era hace 4 años :wink: )

No. No estoy en contra de otras soluciones. Soy muy reacio a que se muevan enlaces y textos antiguos a nuevas ubicaciones sin razones muy sólidas.

2 Me gusta

Tendremos que estar en desacuerdo ahí. Pero creo que es probable que haya mucha gente que esté de acuerdo contigo. (Quizás esta solución sea una solución de “configurar y olvidar”, y la solución de dos contenedores requiere prestar atención a cuándo hay una actualización de Postgres, que ocurre aproximadamente cada 2 años).

¡OK. En eso estamos de acuerdo! Así que creo que el camino a seguir es ver qué puedo hacer para limpiar esa parte y dejar en suspenso la solución de haproxy.

1 me gusta

¡Me encantaría ver esto hecho como parte de Discourse, pero gracias @fefrei por esto! ¡Increíble trabajo! Lo usaré con Apache, pero al menos los pasos básicos deberían ser los mismos.

1 me gusta

¡OK, solo me tomó 2 horas de retoques para hacerlo como quería!

Página de Mantenimiento de Discourse con Apache2

Como root

cd /var/discourse
nano containers/app.yml

Comenta estas líneas:

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

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

Añade al FINAL de la sección de plantillas (debe ser la última):

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

Nota: Esto hará que Discourse escuche solo en la IP interna y apache2 se encargará de los puertos 80/443 y la terminación SSL.

Nota: Discourse debe ser reconstruido para que esto tenga efecto:

cd /var/discourse
./launcher rebuild app

Instalar apache2 y certbot

apt install -y apache2 certbot python3-certbot-apache

Crear un directorio para la página html:

mkdir /var/www/discourse_maintenance

Página 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>We are currently upgrading the site, or performing scheduled maintenance.</p>
                <p>You'll automatically be redirected to the site once it's available.</p>
            </div>
        </div>
    </body>
</html>

Habilitar Módulo Proxy:

a2enmod proxy
a2enmod proxy_http
a2enmod headers

Archivo vhost de Apache:

<IfModule mod_ssl.c>
<VirtualHost *:443>
  ServerName your.discourse.domain
  ServerAdmin your@email.com
  DocumentRoot /var/www/discourse_maintenance

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

  # Maintenance Mode
  RewriteEngine On
  RewriteCond /var/www/under_maintenance -f
  # safety check to prevent redirect loops 
  RewriteCond %{REQUEST_URI} !/discourse_maintenance.html$
  # redirect internally all requests to 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/your.discourse.domain/fullchain.pem
  SSLCertificateKeyFile /etc/letsencrypt/live/your.discourse.domain/privkey.pem
  Include /etc/letsencrypt/options-ssl-apache.conf

</VirtualHost>
</IfModule>

Para habilitar el mantenimiento ejecuta touch /var/www/under_maintenance

Para deshabilitar el mantenimiento ejecuta touch /var/www/under_maintenance

Créditos: Add an offline page to display when Discourse is rebuilding or starting up por la idea inicial, la página html (recortada/editada a mi gusto) y la configuración de nginx en la que basé la configuración de Apache.

Edición: Se aceptan sugerencias para hacerlo automático cuando la respuesta es 502/503. Lo intenté pero no pude conseguir que funcionara como quería, así que opté por un método conocido que uso en otros servidores web cuando la aplicación backend está caída por mantenimiento, etc.

2 Me gusta

En el reinicio del sistema, esto retrasará la página de error/mantenimiento hasta que docker se inicie, lo que tarda bastante más que arrancar el sistema. Tampoco ofrece la opción de protecciones SELinux proporcionadas por el sistema para el nginx del sistema. Usar nginx del sistema puede, al menos en un sistema gestionado por systemd con arranque rápido, mostrarte la página de error a los pocos segundos de arrancar. Para mí, esto significa que mis sistemas responden con la página de mantenimiento muy rápidamente durante las actualizaciones del sistema que requieren reinicio. (Estoy ejecutando AlmaLinux 9 en el host, y arranca a nginx muy rápido).

Podría tener sentido documentar una alternativa de haproxy y comparar experiencias, pero haproxy en docker no es un reemplazo directo para nginx externo, y cerrar este tema sería un error.

No se trata solo de disponibilidad.

Usar docker para tráfico externo a través de IPv4 oculta las direcciones IPv6 externas del nginx y Discourse internos. Tendrás el mismo problema con haproxy. Mira tus registros de direcciones IP de espacio local solo para 127.0.0.1 o 172.* RFC1918. No usar un proxy externo significa que todo el tráfico IPv6 aparece como la misma IP, lo que rompe la limitación de tasa de zona interna de nginx, considerando todo el tráfico IPv6 como una sola zona.

IPv6 importa cada vez más.

2 Me gusta

Descubrí por accidente esta mañana que este paso no solo evita aplicar el socket unix, sino que también elimina el uso del módulo real_ip, de modo que el límite de velocidad se aplica en función de todas las conexiones juntas, en lugar de todas las conexiones por IP. Probablemente debería contribuir con una nueva plantilla con variables, pero ahora mismo acabo de añadir esto a mi archivo YAML del contenedor de la aplicación:

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;

No sé si tiene sentido tener, por ejemplo, un archivo templates/web.httpratelimit.yml con algo así con una variable para la dirección pero sin usar sockets de dominio unix. ¿Opiniones al respecto?

2 Me gusta
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/;
  }
}

! # cambia tu dominio y la ruta del archivo de error

Tengo este script con ssl y funciona

2 Me gusta

Acabo de intentar usar esta guía y no pude hacer que funcionara.

Comencé con un proxy Nginx para ejecutar dos sitios desde un único contenedor de Discourse. Solo quería agregar la parte de la página de error, así que me salté las partes que parecían superponerse con Run other websites on the same machine as Discourse. Sin embargo, debo haberme saltado un paso clave. Al final, obtuve lo que necesitaba de este tutorial de DigitalOcean. No es difícil configurarlo manualmente, pero parece que debe haber una mejor manera.

Dado que Docker es la forma estándar de ejecutar Discourse, esto suena mejor. Supongo que sería el tipo de cosa que configurarías y olvidarías.

3 Me gusta

La idea dentro de este hilo también es genial para aquellos de nosotros que ejecutamos Caddy como proxy inverso, ya sea en una aplicación independiente o usando 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
                }
        }
}

La sección status 404 es importante solo si se usan Cloudflare Tunnels. Si Caddy devuelve 5xx a Cloudflare, Cloudflare Tunnel mostrará su propio error de desconexión. Cambiar el estado indica a Cloudflare que hay una conexión activa válida que servirá una página de error.

2 Me gusta

Quizás estoy entendiendo mal cómo funciona esto, pero ¿no es un refresco solo un refresco de la página en la que ya estás? ¿Cómo te devuelve a una URL diferente?

No es necesario ir a una URL diferente: el truco es que la página de error se sirve directamente en la URL a la que el usuario intentó acceder (por ejemplo, https://meta.discourse.org/t/add-an-offline-page-to-display-when-discourse-is-rebuilding-or-starting-up/45238/158), y esa es la URL exacta que se actualizará, mostrando de nuevo el mismo error o la página que el usuario quería :slight_smile:

Oh, ya veo. No había leído cuidadosamente toda la configuración, ya que estoy usando otro servidor para la página offline, por si mi máquina entera se apagara por alguna razón.
Tiene sentido, aunque. Ahora solo intento hacer que mi JS, que se supone que debe redirigir de regreso a la URL original, funcione…