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
- 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 |
- 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.
- 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)" |
- 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 |
- 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;" |
- 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';" |
- 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 |
- 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 |
- 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'
- 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 |
- 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 |
- 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 |
- 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>"
- 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
- Aprovisionar un nuevo VPS
Instala Docker y Discourse normalmente en el nuevo servidor.
No configures multisitio.
- 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
- 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.
- 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.
- 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.