Múltiples contenedores de aplicación para un solo sitio Discourse

Puedes alojar múltiples instalaciones independientes de Discourse en un solo servidor (contenedores separados / puertos separados / app.yml separados) sin utilizar la función «multisitio» de Discourse.

Es más manual que el modo multisitio, pero mantiene las instancias aisladas y facilita la migración de un sitio individual a su propio servidor más adelante.

Un patrón práctico es:

• Postgres externo (una sola instancia)
• Redis externo (una sola instancia)
• Múltiples contenedores web de Discourse
• Un nodo Sidekiq
• Proxy inverso con comprobaciones de estado (health checks)

Esto evita por completo el multisitio, permitiendo aún así ahorros de costes en configuraciones de bajo tráfico.



GUÍA DE OPERACIÓN PARA DISCOURSE MULTICONTENEDOR
Postgres externo + Redis + HAProxy + app1 / app2


  1. PAQUETES DEL HOST
Paso Comando
Actualizar sistema apt-get update
Instalar herramientas base apt-get install -y ca-certificates curl gnupg lsb-release
Instalar HAProxy + certbot + socat apt-get install -y haproxy certbot socat

  1. RED DE DOCKER (OBLIGATORIA)

Se requiere una red definida por el usuario en Docker para que los contenedores puedan resolverse por nombre.

Paso Comando
Crear red docker network create discourse-net
Verificar docker network ls | grep discourse-net

Esto permite que:

• DISCOURSE_DB_HOST=pg
• DISCOURSE_REDIS_HOST=redis

funcionen correctamente.


  1. SECRETOS
Propósito Comando
Superusuario de Postgres export PG_SUPERPASS='REPLACE_ME_super_strong'
Contraseña de la BD de Discourse export DISCOURSE_DBPASS='REPLACE_ME_discordb_strong'
Contraseña de Redis export REDIS_PASS='REPLACE_ME_redis_strong'
Clave secreta base export SECRET_KEY_BASE="$(openssl rand -hex 64)"

  1. CONTENEDOR DE POSTGRES
Paso Comando
Crear directorio mkdir -p /var/discourse/external/postgres
Ejecutar contenedor docker run -d --name pg --restart=always --network=discourse-net -e POSTGRES_PASSWORD="$PG_SUPERPASS" -v /var/discourse/external/postgres:/var/lib/postgresql/data postgres:15
Verificar docker ps | grep pg

  1. CREAR BASE DE DATOS
Paso Comando
Crear rol docker exec -it pg psql -U postgres -c "CREATE ROLE discourse LOGIN PASSWORD '$DISCOURSE_DBPASS';"
Crear BD docker exec -it pg psql -U postgres -c "CREATE DATABASE discourse OWNER discourse ENCODING 'UTF8' TEMPLATE template0;"
Búsqueda de texto docker exec -it pg psql -U postgres -d discourse -c "ALTER DATABASE discourse SET default_text_search_config = 'pg_catalog.english';"
Probar inicio de sesión docker exec -it pg psql -U discourse -d discourse -c "select 1;"

  1. EXTENSIÓN PGVECTOR

Requerida para versiones modernas de Discourse.

Paso Comando
Instalar docker exec -it pg bash -lc 'apt-get update && apt-get install -y postgresql-15-pgvector && rm -rf /var/lib/apt/lists/*'
Crear extensión docker exec -it pg psql -U postgres -d discourse -c "CREATE EXTENSION IF NOT EXISTS vector;"
Verificar docker exec -it pg psql -U postgres -d discourse -c "SELECT extname FROM pg_extension WHERE extname='vector';"

  1. CONTENEDOR DE REDIS
Paso Comando
Crear directorio mkdir -p /var/discourse/external/redis

Plantilla de configuración de Redis:

requirepass REPLACE_ME_REDIS
appendonly yes
save 900 1
save 300 10
save 60 10000
Paso Comando
Escribir config tee /var/discourse/external/redis/redis.conf >/dev/null <<EOF
Insertar contraseña sed -i "s/REPLACE_ME_REDIS/$REDIS_PASS/" /var/discourse/external/redis/redis.conf
Ejecutar redis docker run -d --name redis --restart=always --network=discourse-net -v /var/discourse/external/redis:/data -v /var/discourse/external/redis/redis.conf:/usr/local/etc/redis/redis.conf redis:7-alpine redis-server /usr/local/etc/redis/redis.conf
Probar autenticación docker exec -it redis redis-cli -a "$REDIS_PASS" ping

  1. ESTRUCTURA DE DIRECTORIOS DE DISCOURSE
Paso Comando
Crear directorio base mkdir -p /var/discourse
Entrar cd /var/discourse
Clonar repositorio git clone https://github.com/discourse/discourse_docker.git
Directorio de contenedores mkdir -p /var/discourse/containers
Logs compartidos mkdir -p /var/discourse/shared/web-only/log/var-log
Enlazar contenedores ln -sfn /var/discourse/containers /var/discourse/discourse_docker/containers
Enlazar lanzador ln -sfn /var/discourse/discourse_docker/launcher /var/discourse/launcher

  1. CONTENEDORES DE APLICACIÓN

app1.yml
• web + sidekiq
• puerto 8001

docker_args: "--network=discourse-net"
expose:
  - "8001:80"

app2.yml
• solo web
• puerto 8002
• sidekiq desactivado

docker_args: "--network=discourse-net"
expose:
  - "8002:80"

run:
  - exec: bash -lc 'mkdir -p /etc/service/sidekiq && touch /etc/service/sidekiq/down'

  1. INICIALIZACIÓN (BOOTSTRAP)
Paso Comando
Entrar cd /var/discourse/discourse_docker
Inicializar app1 ./launcher bootstrap app1
Iniciar app1 ./launcher start app1
Inicializar app2 ./launcher bootstrap app2
Iniciar app2 ./launcher start app2

  1. COMPROBACIONES DE ESTADO (HEALTH CHECKS)
Paso Comando
app1 curl -sSf http://127.0.0.1:8001/srv/status
app2 curl -sSf http://127.0.0.1:8002/srv/status
sidekiq app1 docker exec -it app1 pgrep -fa sidekiq
sidekiq app2 `docker exec -it app2 pgrep -fa sidekiq

  1. CERTIFICADO TLS
Paso Comando
Detener proxy systemctl stop haproxy
Emitir certificado certbot certonly --standalone -d example.com --agree-tos -m you@example.com --non-interactive
Iniciar proxy systemctl start haproxy

  1. LÓGICA DE HAPROXY
frontend fe_discourse
    bind :80
    bind :443 ssl crt /etc/letsencrypt/live/example.com/haproxy.pem

    http-request set-header X-Forwarded-Proto https if { ssl_fc }
    http-request set-header X-Forwarded-Proto http if !{ ssl_fc }

    redirect scheme https code 301 if !{ ssl_fc }

    use_backend be_discourse if { nbsrv(be_discourse) gt 0 }
    default_backend be_maint
backend be_discourse
    balance roundrobin
    option httpchk GET /srv/status
    server app1 127.0.0.1:8001 check
    server app2 127.0.0.1:8002 check
backend be_maint
    http-request return status 503 content-type text/html string "<h1>Mantenimiento</h1>"

  1. RECONSTRUCCIONES SIN TIEMPO DE INACTIVIDAD (ZERO-DOWNTIME)
Paso Comando
Desactivar app1 echo "disable server be_discourse/app1" | socat stdio /run/haproxy/admin.sock
Reconstruir app1 ./launcher rebuild app1
Activar app1 echo "enable server be_discourse/app1" | socat stdio /run/haproxy/admin.sock
Paso Comando
Desactivar app2 echo "disable server be_discourse/app2" | socat stdio /run/haproxy/admin.sock
Reconstruir app2 ./launcher rebuild app2
Activar app2 echo "enable server be_discourse/app2" | socat stdio /run/haproxy/admin.sock

FIN

Red de Docker requerida
Postgres y Redis externos
pgvector instalado
Sidekiq aislado en app1
Comprobaciones de estado de HAProxy activadas
Respuesta de mantenimiento activa
Reconstrucciones progresivas soportadas

Migrar un sitio a su propio servidor más adelante

Una ventaja de ejecutar instalaciones completamente independientes de Discourse (en lugar de multisitio) es que la migración es directa y de bajo riesgo.

Cada instancia de Discourse ya cuenta con:

• su propio contenedor
• sus propias subidas
• su propia base de datos
• su propio uso de Redis
• su propio app.yml

No es necesario desentrelazar un multisitio.


Pasos generales de migración

  1. Aprovisionar un nuevo VPS

Instala Docker y Discourse normalmente en el nuevo servidor.
No configures multisitio.


  1. Crear una copia de seguridad completa

Desde el sitio de origen:

Administración → Copias de seguridad → Crear copia de seguridad

Descarga el archivo de copia de seguridad.

Esto incluye:

• base de datos
• subidas
• usuarios
• configuraciones
• temas


  1. Restaurar en el nuevo servidor

En el nuevo servidor:

• completa la configuración inicial
• inicia sesión como administrador
• sube la copia de seguridad
• restaura

Discourse maneja automáticamente la compatibilidad del esquema.


  1. Cambio de DNS

Actualiza el registro A del dominio para que apunte a la IP del nuevo servidor.

Una vez que el DNS se propague, los usuarios se moverán de forma transparente.


  1. Desmantelar el contenedor antiguo

En el servidor original:

• detén el contenedor antiguo
• elimínalo cuando estés seguro

Las otras instalaciones de Discourse en el mismo host no se ven afectadas.


Por qué esto es más sencillo que el multisitio

En configuraciones de multisitio, la migración a menudo requiere:

• separar bases de datos
• extraer datos específicos del sitio
• ajustar multisite.yml
• reestructurar Sidekiq
• reconfigurar subidas y correo electrónico

Con instalaciones independientes, nada de eso es necesario.

Cada sitio ya es independiente.


Resumen

Este enfoque sacrifica un poco de complejidad operativa al principio
a cambio de una separación muy sencilla más adelante.

Funciona particularmente bien durante la experimentación
o la construcción de comunidades en etapas iniciales.


Cuando este enfoque probablemente no sea adecuado

Esta configuración generalmente no es una buena idea si:

• los sitios esperan tráfico moderado o alto desde el principio
• dependes en gran medida del soporte oficial de Discourse
• no te sientes cómodo depurando Docker, redes o proxies inversos
• los requisitos de tiempo de actividad son estrictos o críticos para el negocio
• múltiples sitios están estrechamente acoplados operativamente
• esperas experimentación frecuente con plugins en todas las instancias

En estos casos, ya sea:

• una configuración de multisitio soportada
o
• una instalación de Discourse por servidor

generalmente resultará en menos sorpresas operativas.


Nota importante

Este enfoque aumenta la flexibilidad de la infraestructura,
pero también incrementa la responsabilidad del administrador.

Funciona mejor cuando la persona que lo ejecuta se siente cómoda asumiendo todo el stack
y tratando las fallas ocasionales como parte del proceso de aprendizaje.

Si la estabilidad y la capacidad de soporte son los objetivos principales,
un configuración soportada es casi siempre la mejor opción.

una nota adicional que se relaciona directamente con la parte de HAProxy de la configuración anterior.

Existe un comportamiento común con HAProxy + Discourse en el que reconstruir un contenedor web (por ejemplo, con ./launcher rebuild app1) devolverá brevemente respuestas de 503 Service Unavailable porque HAProxy sigue enviando tráfico a ese backend mientras se está reiniciando. Esto no es un error en Discourse en sí, sino que ocurre porque el backend no está disponible momentáneamente durante la reconstrucción.

La solución recomendada es utilizar el socket de administración de HAProxy para:

  1. deshabilitar el servidor en HAProxy antes de la reconstrucción, y
  2. volver a habilitarlo después de que finalice la reconstrucción

Esto previene esos 503 transitorios.

Existe una discusión existente en Meta que documenta este comportamiento y la explicación de la solución alternativa:

Si alguien aquí está utilizando HAProxy para reconstrucciones continuas (rolling rebuilds), ese hilo proporciona un contexto útil sobre por qué se incluyen los comandos del socket de administración en el manual de procedimientos (runbook).

[quote=“Ethsim2, post:51, topic:392692”]Puedes alojar múltiples instalaciones independientes de Discourse en un solo servidor (contenedores separados / puertos separados / app.yml separado), sin usar “multisitio” de Discourse.
[/quote]

Yo hago algo similar, con un contenedor de estilo solo web por sitio y traefik (aunque también tengo una configuración que usa nginx-proxy) como proxy inverso. Probé HAproxy por un tiempo (es lo que usa CDCK, hasta donde sé), pero me resultó engorroso.

Estoy bastante seguro de que necesitas un redis por servidor Discourse.

Creo que puede haber una pequeña discrepancia terminológica aquí.

Cuando dices “un Redis por servidor de Discourse”, estoy de acuerdo si por servidor nos referimos a un sitio lógico de Discourse.

En mi caso:

  • HAProxy solo se utiliza para conmutación por error / fronting
  • No hay configuración multisitio
  • Solo hay un sitio de Discourse (un solo nombre de host, una sola base de datos Postgres)
  • Simplemente hay dos contenedores de aplicaciones capaces de servir ese mismo sitio

Así que esto se acerca más a un diseño multi-web / HA, no a dos instalaciones independientes de Discourse.

En esa configuración, compartir Redis es lo esperado y necesario; de lo contrario, se pierde:

  • sesiones compartidas
  • entrega de MessageBus
  • limitación de velocidad
  • coordinación de trabajos en segundo plano

Este es el mismo patrón que ejecutar múltiples contenedores web_only o escalar horizontalmente los trabajadores web:
múltiples contenedores de aplicaciones → una Postgres + una Redis.

Donde Redis no debe compartirse es cuando hay dos sitios de Discourse separados (diferentes nombres de host/bases de datos). En ese caso, cada sitio necesita su propia base de datos (o instancia) de Redis para evitar colisiones de claves.

Así que creo que estamos alineados conceptualmente: es solo:

  • :white_check_mark: una Redis por sitio de Discourse
  • :cross_mark: no una Redis por contenedor de aplicación individual

Estaré encantado de aclarar más si he entendido mal algo; solo quería explicar la topología con más claridad.

Oh. Eso es lo opuesto de lo que pensé que estábamos discutiendo. El título es “Un servidor para 2 comunidades de Discourse” Usted está hablando de “dos servidores para una comunidad de Discourse”.

Tiene razón: confundí dos topologías diferentes, y el título del hilo es la pista.

Este tema trata sobre “un servidor para 2 comunidades” (dos sitios independientes).
Mi comentario anterior sobre “Redis externo (instancia única)” describía un patrón diferente: “dos contenedores de aplicaciones para una comunidad” (HA / multi-web para un solo sitio).

Así que, para reiterar claramente:

A) Dos sitios Discourse independientes en un servidor (lo que pregunta el OP)

  • Trátelos como dos instalaciones separadas
  • Deben tener bases de datos Postgres separadas e instancias de Redis separadas (o al menos un aislamiento suficiente para MessageBus Pub/Sub, que es el problema que citó)

B) Un sitio Discourse con múltiples contenedores web/aplicación (lo que yo estaba describiendo)

  • Deben compartir la misma base de datos Postgres y el mismo Redis para ese sitio
    (sesiones, limitación de velocidad, MessageBus, etc.)

Entonces: :white_check_mark: su advertencia sobre Redis se aplica a A (dos comunidades / dos sitios).
Mi nota sobre “Redis compartido” solo se aplica a B (una comunidad escalada a través de múltiples contenedores).

Gracias por la corrección; mantendré los dos casos explícitamente separados en cualquier guía/publicación de seguimiento.