Múltiplos contêineres de app para um único site Discourse

Você pode hospedar várias instalações independentes do Discourse em um único servidor (containers separados / portas separadas / app.yml separados), sem usar o recurso “multisite” do Discourse.

É mais manual do que o multisite, mas mantém as instâncias isoladas e facilita a migração de um site individual para seu próprio servidor mais tarde.

Um padrão prático é:

• PostgreSQL externo (instância única)
• Redis externo (instância única)
• múltiplos containers web do Discourse
• um nó Sidekiq
• proxy reverso com verificações de saúde (health checks)

Isso evita completamente o multisite, ao mesmo tempo que permite economia de custos em configurações de baixo tráfego.



ROTEIRO DE EXECUÇÃO MULTI-CONTAINER DO DISCOURSE
PostgreSQL Externo + Redis + HAProxy + app1 / app2


  1. PACOTES DO HOST
Etapa Comando
Atualizar sistema apt-get update
Instalar ferramentas básicas apt-get install -y ca-certificates curl gnupg lsb-release
Instalar HAProxy + certbot + socat apt-get install -y haproxy certbot socat

  1. REDE DOCKER (OBRIGATÓRIA)

Uma rede Docker definida pelo usuário é necessária para que os containers possam se resolver por nome.

Etapa Comando
Criar rede docker network create discourse-net
Verificar docker network ls | grep discourse-net

Isso permite que:

• DISCOURSE_DB_HOST=pg
• DISCOURSE_REDIS_HOST=redis

funcionem corretamente.


  1. SECRETS
Finalidade Comando
Superusuário PostgreSQL export PG_SUPERPASS='REPLACE_ME_super_strong'
Senha do banco Discourse export DISCOURSE_DBPASS='REPLACE_ME_discordb_strong'
Senha do Redis export REDIS_PASS='REPLACE_ME_redis_strong'
Chave secreta base export SECRET_KEY_BASE="$(openssl rand -hex 64)"

  1. CONTAINER POSTGRES
Etapa Comando
Criar diretório mkdir -p /var/discourse/external/postgres
Executar container 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. CRIAR BANCO DE DADOS
Etapa Comando
Criar role docker exec -it pg psql -U postgres -c "CREATE ROLE discourse LOGIN PASSWORD '$DISCOURSE_DBPASS';"
Criar DB docker exec -it pg psql -U postgres -c "CREATE DATABASE discourse OWNER discourse ENCODING 'UTF8' TEMPLATE template0;"
Pesquisa de texto docker exec -it pg psql -U postgres -d discourse -c "ALTER DATABASE discourse SET default_text_search_config = 'pg_catalog.english';"
Testar login docker exec -it pg psql -U discourse -d discourse -c "select 1;"

  1. EXTENSÃO PGVECTOR

Necessária para versões modernas do Discourse.

Etapa Comando
Instalar docker exec -it pg bash -lc 'apt-get update && apt-get install -y postgresql-15-pgvector && rm -rf /var/lib/apt/lists/*'
Criar extensão 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. CONTAINER REDIS
Etapa Comando
Criar diretório mkdir -p /var/discourse/external/redis

Template de configuração do Redis:

requirepass REPLACE_ME_REDIS
appendonly yes
save 900 1
save 300 10
save 60 10000
Etapa Comando
Escrever config tee /var/discourse/external/redis/redis.conf >/dev/null <<EOF
Inserir senha sed -i "s/REPLACE_ME_REDIS/$REDIS_PASS/" /var/discourse/external/redis/redis.conf
Executar 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
Testar autenticação docker exec -it redis redis-cli -a "$REDIS_PASS" ping

  1. ESTRUTURA DE DIRETÓRIOS DO DISCOURSE
Etapa Comando
Criar diretório base mkdir -p /var/discourse
Entrar cd /var/discourse
Clonar repositório git clone https://github.com/discourse/discourse_docker.git
Diretório containers mkdir -p /var/discourse/containers
Logs compartilhados mkdir -p /var/discourse/shared/web-only/log/var-log
Linkar containers ln -sfn /var/discourse/containers /var/discourse/discourse_docker/containers
Linkar launcher ln -sfn /var/discourse/discourse_docker/launcher /var/discourse/launcher

  1. CONTAINERS DE APLICAÇÃO

app1.yml
• web + sidekiq
• porta 8001

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

app2.yml
• apenas web
• porta 8002
• sidekiq desabilitado

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

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

  1. BOOTSTRAP
Etapa Comando
Entrar cd /var/discourse/discourse_docker
Bootstrap app1 ./launcher bootstrap app1
Iniciar app1 ./launcher start app1
Bootstrap app2 ./launcher bootstrap app2
Iniciar app2 ./launcher start app2

  1. VERIFICAÇÕES DE SAÚDE (HEALTH CHECKS)
Etapa 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
Etapa Comando
Parar 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 DO 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>Maintenance</h1>"

  1. RECONSTRUÇÕES SEM TEMPO DE INATIVIDADE (ZERO-DOWNTIME)
Etapa Comando
Desabilitar app1 echo "disable server be_discourse/app1" | socat stdio /run/haproxy/admin.sock
Reconstruir app1 ./launcher rebuild app1
Habilitar app1 echo "enable server be_discourse/app1" | socat stdio /run/haproxy/admin.sock
Etapa Comando
Desabilitar app2 echo "disable server be_discourse/app2" | socat stdio /run/haproxy/admin.sock
Reconstruir app2 ./launcher rebuild app2
Habilitar app2 echo "enable server be_discourse/app2" | socat stdio /run/haproxy/admin.sock

FIM

Rede Docker necessária
PostgreSQL e Redis externos
pgvector instalado
Sidekiq isolado no app1
Verificações de saúde do HAProxy habilitadas
Fallback de manutenção ativo
Reconstruções em rotação suportadas

Migrando um site para seu próprio servidor mais tarde

Uma vantagem de executar instalações totalmente independentes do Discourse (em vez de multisite) é que a migração é direta e de baixo risco.

Cada instância do Discourse já possui:

• seu próprio container
• seus próprios uploads
• seu próprio banco de dados
• seu próprio uso do Redis
• seu próprio app.yml

Não é necessário desvendar o multisite.


Passos gerais de migração

  1. Provisionar um novo VPS

Instale o Docker e o Discourse normalmente no novo servidor.
Não configure multisite.


  1. Criar um backup completo

No site de origem:

Admin → Backups → Criar Backup

Baixe o arquivo de backup.

Isso inclui:

• banco de dados
• uploads
• usuários
• configurações
• temas


  1. Restaurar no novo servidor

No novo servidor:

• conclua a configuração inicial
• faça login como admin
• faça upload do backup
• restaure

O Discourse lida automaticamente com a compatibilidade do esquema.


  1. Migração de DNS

Atualize o registro A do domínio para apontar para o IP do novo servidor.

Assim que o DNS propagar, os usuários serão movidos de forma transparente.


  1. Descomissionar o container antigo

No servidor original:

• pare o container antigo
• remova-o quando tiver certeza

Outras instalações do Discourse no mesmo host não são afetadas.


Por que isso é mais simples que o multisite

Em configurações multisite, a migração frequentemente requer:

• separação de bancos de dados
• extração de dados específicos do site
• ajuste do multisite.yml
• reestruturação do Sidekiq
• reconfiguração de uploads e e-mail

Com instalações independentes, nada disso é necessário.

Cada site já é independente.


Resumo

Esta abordagem troca um pouco de complexidade operacional no início
por uma separação muito simples mais tarde.

Funciona particularmente bem durante experimentação
ou construção de comunidades em estágio inicial.


Quando esta abordagem provavelmente não é adequada

Esta configuração geralmente não é uma boa ideia se:

• os sites esperam tráfego moderado ou alto desde o início
• você depende fortemente do suporte oficial do Discourse
• você não se sente confortável depurando Docker, redes ou proxies reversos
• os requisitos de uptime são rigorosos ou críticos para o negócio
• múltiplos sites estão operacionalmente fortemente acoplados
• você espera experimentação frequente de plugins em todas as instâncias

Nesses casos, ou:

• uma configuração multisite suportada
ou
• uma instalação do Discourse por servidor

geralmente resultará em menos surpresas operacionais.


Nota importante

Esta abordagem aumenta a flexibilidade da infraestrutura,
mas também aumenta a responsabilidade do administrador.

Funciona melhor quando a pessoa que a executa está confortável em assumir toda a pilha (full stack)
e tratar falhas ocasionais como parte do processo de aprendizado.

Se estabilidade e suporteabilidade são os objetivos principais,
uma configuração suportada é quase sempre a melhor escolha.

uma nota adicional que se relaciona diretamente com a parte do HAProxy da configuração acima.

Existe um comportamento comum com HAProxy + Discourse em que a reconstrução de um contêiner web (por exemplo, com ./launcher rebuild app1) retornará brevemente respostas de 503 Service Unavailable, porque o HAProxy ainda está enviando tráfego para esse backend enquanto ele está reiniciando. Isso não é um erro no Discourse em si - acontece porque o backend fica momentaneamente indisponível durante a reconstrução.

A solução alternativa recomendada é usar o soquete de administração do HAProxy para:
\t1.\tdesabilitar o servidor no HAProxy antes da reconstrução, e
\t2.\treabilitá-lo após o término da reconstrução

Isso evita esses 503s transitórios.

Existe uma discussão existente no Meta documentando esse comportamento e a explicação da solução alternativa:

Se alguém aqui estiver usando HAProxy para reconstruções contínuas (rolling rebuilds), esse tópico fornece um contexto útil para o motivo pelo qual os comandos do soquete de administração estão incluídos no manual de execução (runbook).

[quote=“Ethsim2, post:51, topic:392692”]Você pode hospedar múltiplas instalações independentes do Discourse em um único servidor (contêineres separados / portas separadas / app.yml separado), sem usar o “multisite” do Discourse.
[/quote]

Eu faço algo semelhante, com um contêiner estilo apenas web por site e traefik (embora eu também tenha uma configuração usando nginx-proxy) como proxy reverso. Eu usei HAproxy por um tempo (é o que o CDCK usa, até onde eu sei), mas achei complicado.

Tenho quase certeza de que você precisa de um redis por servidor Discourse.

Acho que pode haver uma pequena divergência de terminologia aqui.

Quando você diz “um Redis por servidor Discourse”, concordo se por servidor quisermos dizer um site Discourse lógico.

No meu caso:

  • O HAProxy está sendo usado apenas para failover / fronting
  • Não há configuração multisite
  • Existe apenas um site Discourse (nome de host único, um único banco de dados Postgres)
  • Acontece de haver dois contêineres de aplicativo capazes de servir o mesmo site

Portanto, isso é mais próximo de uma configuração multi-web / HA, e não de duas instalações independentes do Discourse.

Nessa configuração, compartilhar o Redis é esperado e necessário - caso contrário, você perde:

  • sessões compartilhadas
  • entrega do MessageBus
  • limitação de taxa (rate limiting)
  • coordenação de trabalhos em segundo plano (background job coordination)

Este é o mesmo padrão de executar vários contêineres web_only ou dimensionar horizontalmente os workers web:
múltiplos contêineres de aplicativo → um Postgres + um Redis.

Onde o Redis não deve ser compartilhado é quando existem dois sites Discourse separados (nomes de host/bancos de dados diferentes). Nesse caso, cada site precisa de seu próprio DB Redis (ou instância) para evitar colisões de chaves.

Então, acho que estamos alinhados conceitualmente - é apenas:

  • :white_check_mark: um Redis por site Discourse
  • :cross_mark: não um Redis por contêiner de aplicativo individual

Fico feliz em esclarecer mais se eu tiver entendido mal alguma coisa - só queria explicar a topologia com mais clareza.

Ah. Isso é o oposto do que eu pensei que estávamos discutindo. O título é “Um servidor para 2 comunidades Discourse” Você está falando sobre “dois servidores para uma comunidade Discourse”.

Você está certo - eu confundi duas topologias diferentes, e o título do tópico é a pista.

Este tópico é sobre “um servidor para 2 comunidades” (dois sites independentes).
Meu comentário anterior sobre “Redis externo (instância única)” estava descrevendo um padrão diferente: “dois contêineres de aplicativo para uma comunidade” (HA / multi-web para um único site).

Então, para reafirmar claramente:

A) Dois sites Discourse independentes em um servidor (o que o OP está perguntando)

  • Trate-os como duas instalações separadas
  • Eles devem ter bancos de dados Postgres separados e instâncias Redis separadas (ou isolamento suficiente para o MessageBus Pub/Sub, que é o problema que você citou)

B) Um site Discourse com múltiplos contêineres web/app (o que eu estava descrevendo)

  • Eles devem compartilhar o mesmo banco de dados Postgres e o mesmo Redis para esse site
    (sessões, limitação de taxa, MessageBus, etc.)

Portanto: :white_check_mark: seu aviso sobre Redis se aplica a A (duas comunidades / dois sites).
Minha observação sobre “Redis compartilhado” só se aplica a B (uma comunidade dimensionada em vários contêineres).

Obrigado pela correção - manterei os dois casos explicitamente separados em quaisquer guias/posts futuros.