Contenitori di app multipli per un singolo sito Discourse

Puoi ospitare più installazioni autonome di Discourse su un singolo server (container separati / porte separate / app.yml separati) senza utilizzare la funzione “multisito” di Discourse.

È più manuale rispetto al multisito, ma mantiene le istanze isolate e rende più semplice migrare un singolo sito su un server dedicato in seguito.

Un modello pratico è:

• Postgres esterno (istanza singola)
• Redis esterno (istanza singola)
• più container web di Discourse
• un nodo Sidekiq
• reverse proxy con controlli di integrità (health checks)

Questo evita completamente il multisito, consentendo comunque risparmi sui costi per configurazioni a basso traffico.



RUNBOOK PER L’ESECUZIONE MULTI-CONTAINER DI DISCOURSE
Postgres esterno + Redis + HAProxy + app1 / app2


  1. PACCHETTI HOST
Passaggio Comando
Aggiorna il sistema apt-get update
Installa strumenti di base apt-get install -y ca-certificates curl gnupg lsb-release
Installa HAProxy + certbot + socat apt-get install -y haproxy certbot socat

  1. RETE DOCKER (OBBLIGATORIA)

È necessaria una rete Docker definita dall’utente in modo che i container possano risolversi per nome.

Passaggio Comando
Crea la rete docker network create discourse-net
Verifica docker network ls | grep discourse-net

Questo permette:

• DISCOURSE_DB_HOST=pg
• DISCOURSE_REDIS_HOST=redis

di funzionare correttamente.


  1. SEGRETI
Scopo Comando
Superutente Postgres export PG_SUPERPASS='REPLACE_ME_super_strong'
Password DB Discourse export DISCOURSE_DBPASS='REPLACE_ME_discordb_strong'
Password Redis export REDIS_PASS='REPLACE_ME_redis_strong'
Chiave segreta base export SECRET_KEY_BASE="$(openssl rand -hex 64)"

  1. CONTAINER POSTGRES
Passaggio Comando
Crea directory mkdir -p /var/discourse/external/postgres
Esegui 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
Verifica docker ps | grep pg

  1. CREA DATABASE
Passaggio Comando
Crea ruolo docker exec -it pg psql -U postgres -c "CREATE ROLE discourse LOGIN PASSWORD '$DISCOURSE_DBPASS';"
Crea DB docker exec -it pg psql -U postgres -c "CREATE DATABASE discourse OWNER discourse ENCODING 'UTF8' TEMPLATE template0;"
Ricerca testuale docker exec -it pg psql -U postgres -d discourse -c "ALTER DATABASE discourse SET default_text_search_config = 'pg_catalog.english';"
Test accesso docker exec -it pg psql -U discourse -d discourse -c "select 1;"

  1. ESTENSIONE PGVECTOR

Necessaria per le versioni moderne di Discourse.

Passaggio Comando
Installa docker exec -it pg bash -lc 'apt-get update && apt-get install -y postgresql-15-pgvector && rm -rf /var/lib/apt/lists/*'
Crea estensione docker exec -it pg psql -U postgres -d discourse -c "CREATE EXTENSION IF NOT EXISTS vector;"
Verifica docker exec -it pg psql -U postgres -d discourse -c "SELECT extname FROM pg_extension WHERE extname='vector';"

  1. CONTAINER REDIS
Passaggio Comando
Crea directory mkdir -p /var/discourse/external/redis

Modello di configurazione Redis:

requirepass REPLACE_ME_REDIS
appendonly yes
save 900 1
save 300 10
save 60 10000
Passaggio Comando
Scrivi configurazione tee /var/discourse/external/redis/redis.conf >/dev/null <<EOF
Inserisci password sed -i "s/REPLACE_ME_REDIS/$REDIS_PASS/" /var/discourse/external/redis/redis.conf
Esegui 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
Test autenticazione docker exec -it redis redis-cli -a "$REDIS_PASS" ping

  1. STRUTTURA DELLE DIRECTORY DI DISCOURSE
Passaggio Comando
Crea directory base mkdir -p /var/discourse
Entra cd /var/discourse
Clona repository git clone https://github.com/discourse/discourse_docker.git
Directory container mkdir -p /var/discourse/containers
Log condivisi mkdir -p /var/discourse/shared/web-only/log/var-log
Collega container ln -sfn /var/discourse/containers /var/discourse/discourse_docker/containers
Collega launcher ln -sfn /var/discourse/discourse_docker/launcher /var/discourse/launcher

  1. CONTAINER APPLICAZIONE

app1.yml
• web + sidekiq
• porta 8001

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

app2.yml
• solo web
• porta 8002
• sidekiq disabilitato

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

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

  1. BOOTSTRAP
Passaggio Comando
Entra cd /var/discourse/discourse_docker
Bootstrap app1 ./launcher bootstrap app1
Avvia app1 ./launcher start app1
Bootstrap app2 ./launcher bootstrap app2
Avvia app2 ./launcher start app2

  1. CONTROLLI DI INTEGRITÀ (HEALTH CHECKS)
Passaggio 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. CERTIFICATO TLS
Passaggio Comando
Ferma proxy systemctl stop haproxy
Emetti certificato certbot certonly --standalone -d example.com --agree-tos -m you@example.com --non-interactive
Avvia proxy systemctl start haproxy

  1. LOGICA 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. RICOSTRUZIONI SENZA TEMPO DI INATTIVITÀ (ZERO-DOWNTIME)
Passaggio Comando
Disabilita app1 echo "disable server be_discourse/app1" | socat stdio /run/haproxy/admin.sock
Ricrea app1 ./launcher rebuild app1
Abilita app1 echo "enable server be_discourse/app1" | socat stdio /run/haproxy/admin.sock
Passaggio Comando
Disabilita app2 echo "disable server be_discourse/app2" | socat stdio /run/haproxy/admin.sock
Ricrea app2 ./launcher rebuild app2
Abilita app2 echo "enable server be_discourse/app2" | socat stdio /run/haproxy/admin.sock

FINE

Rete Docker richiesta
Postgres e Redis esterni
pgvector installato
Sidekiq isolato su app1
Controlli di integrità HAProxy abilitati
Fallback per manutenzione attivo
Ricostruzioni rotative supportate

Migrare un sito su un server dedicato in seguito

Un vantaggio dell’eseguire installazioni completamente autonome di Discourse (invece del multisito) è che la migrazione è semplice e a basso rischio.

Ogni istanza di Discourse ha già:

• il proprio container
• i propri upload
• il proprio database
• il proprio utilizzo di Redis
• il proprio app.yml

Non è necessario alcun disaccoppiamento del multisito.


Passaggi generali per la migrazione

  1. Provisiona un nuovo VPS

Installa Docker e Discourse normalmente sul nuovo server.
Non configurare il multisito.


  1. Crea un backup completo

Dal sito di origine:

Admin → Backups → Crea Backup

Scarica il file di backup.

Questo include:

• database
• upload
• utenti
• impostazioni
• temi


  1. Ripristina sul nuovo server

Sul nuovo server:

• completa la configurazione iniziale
• accedi come amministratore
• carica il backup
• ripristina

Discourse gestisce automaticamente la compatibilità dello schema.


  1. Cambio DNS

Aggiorna il record A del dominio per puntare al nuovo IP del server.

Una volta propagato il DNS, gli utenti verranno spostati in modo trasparente.


  1. Smantella il vecchio container

Sul server originale:

• ferma il vecchio container
• rimuovilo quando sei sicuro

Le altre installazioni di Discourse sullo stesso host non sono influenzate.


Perché è più semplice del multisito

Nei setup multisito, la migrazione richiede spesso:

• separazione dei database
• estrazione dei dati specifici del sito
• aggiustamento di multisite.yml
• riprogettazione di Sidekiq
• riconfigurazione di upload ed email

Con installazioni autonome, nulla di tutto ciò è necessario.

Ogni sito è già indipendente.


Riepilogo

Questo approccio sacrifica un po’ di complessità operativa all’inizio
per una separazione molto semplice in seguito.

Funziona particolarmente bene durante le sperimentazioni
o la costruzione di comunità nelle fasi iniziali.


Quando questo approccio probabilmente non è adatto

Questa configurazione di solito non è una buona idea se:

• i siti si aspettano traffico moderato o elevato fin dall’inizio
• fai molto affidamento sul supporto ufficiale di Discourse
• non ti senti a tuo agio nel debug di Docker, reti o reverse proxy
• i requisiti di uptime sono rigorosi o critici per il business
• più siti sono strettamente accoppiati a livello operativo
• prevedi frequenti sperimentazioni con plugin su tutte le istanze

In questi casi, o:

• un setup multisito supportato
o
• un’installazione di Discourse per server

porterà generalmente a meno sorprese operative.


Nota importante

Questo approccio aumenta la flessibilità dell’infrastruttura,
ma aumenta anche la responsabilità dell’amministratore.

Funziona meglio quando la persona che lo gestisce è a proprio agio nel gestire l’intera stack
e considera eventuali guasti occasionali come parte del processo di apprendimento.

Se stabilità e supporto sono gli obiettivi principali,
un configurazione supportata è quasi sempre la scelta migliore.

un’ulteriore nota che si collega direttamente alla parte HAProxy della configurazione sopra.

Esiste un comportamento comune con HAProxy + Discourse per cui la ricostruzione di un container web (ad esempio con ./launcher rebuild app1) restituirà brevemente risposte 503 Service Unavailable perché HAProxy sta ancora inviando traffico a quel backend mentre si sta riavviando. Questo non è un errore in Discourse stesso: accade perché il backend è momentaneamente non disponibile durante la ricostruzione.

La soluzione consigliata consiste nell’utilizzare il socket di amministrazione di HAProxy per:

  1. disabilitare il server in HAProxy prima della ricostruzione, e
  2. riabilitarlo dopo che la ricostruzione è terminata

Ciò impedisce quei 503 transitori.

Esiste una discussione esistente su Meta che documenta questo comportamento e la spiegazione della soluzione alternativa:

Se qualcuno qui utilizza HAProxy per le ricostruzioni progressive (rolling rebuilds), quel thread fornisce un contesto utile per spiegare perché i comandi del socket di amministrazione sono inclusi nel runbook.

[quote=“Ethsim2, post:51, topic:392692”]È possibile ospitare più installazioni Discourse autonome su un singolo server (container separati / porte separate / app.yml separato), senza utilizzare il “multisito” di Discourse.
[/quote]

Faccio qualcosa di simile, con un container in stile solo web per sito e traefik (anche se ho anche una configurazione che utilizza nginx-proxy) come proxy inverso. Ho provato HAproxy per un po’ (è quello che usa CDCK, per quanto ne so), ma l’ho trovato macchinoso.

[quote=“Ethsim2, post:51, topic:392692”]• Redis esterno (istanza singola)
[/quote]

Sono abbastanza sicuro che sia necessario un redis per ogni server Discourse.

[quote=“david, post:2, topic:219318”]Ogni Discourse ha bisogno del proprio server redis totalmente separato perché message-bus utilizza Redis Pub/Sub, che è condiviso tra tutti i database su un server Redis.
[/quote]

Penso che ci sia una piccola discrepanza terminologica qui.

Quando dici “un Redis per server Discourse”, sono d’accordo se per server intendiamo un sito Discourse logico.

Nel mio caso:

  • HAProxy viene utilizzato solo per il failover / fronting
  • Non c’è configurazione multisito
  • C’è solo un sito Discourse (singolo hostname, singolo database Postgres)
  • Ci sono semplicemente due container di app in grado di servire lo stesso sito

Quindi questo è più simile a un layout multi-web / HA, non a due installazioni Discourse indipendenti.

In questa configurazione, la condivisione di Redis è prevista e richiesta - altrimenti si perdono:

  • sessioni condivise
  • consegna MessageBus
  • limitazione della velocità (rate limiting)
  • coordinamento dei processi in background

Questo è lo stesso schema dell’esecuzione di più container web_only o della scalabilità orizzontale dei worker web:
più container di app → un Postgres + un Redis.

Dove Redis non deve essere condiviso è quando ci sono due siti Discourse separati (hostname/database diversi). In tal caso, ogni sito necessita del proprio DB Redis (o istanza) per evitare collisioni di chiavi.

Quindi penso che siamo allineati concettualmente - è solo:

  • :white_check_mark: un Redis per sito Discourse
  • :cross_mark: non un Redis per singolo container di app

Felice di chiarire ulteriormente se ho frainteso qualcosa - volevo solo spiegare la topologia più chiaramente.

Oh. Questo è l’opposto di ciò di cui pensavo stessimo parlando. Il titolo è “Un server per 2 comunità Discourse” Tu stai parlando di “due server per una comunità Discourse”.

Hai ragione: ho confuso due topologie diverse, e il titolo del thread è l’indizio.

Questo argomento riguarda “un server per 2 community” (due siti indipendenti).
Il mio precedente commento su “Redis esterno (istanza singola)” descriveva uno schema diverso: “due container applicativi per una community” (HA / multi-web per un singolo sito).

Quindi, per ribadire chiaramente:

A) Due siti Discourse indipendenti su un server (quello che chiede l’OP)

  • Trattali come due installazioni separate
  • Dovrebbero avere database Postgres separati e istanze Redis separate (o almeno un isolamento sufficiente per MessageBus Pub/Sub, che è l’insidia che hai citato)

B) Un sito Discourse con più container web/app (quello che stavo descrivendo)

  • Devono condividere lo stesso database Postgres e lo stesso Redis per quel sito (sessioni, limitazione della frequenza, MessageBus, ecc.)

Quindi: :white_check_mark: il tuo avviso su Redis si applica ad A (due community / due siti).
La mia nota su “Redis condiviso” si applica solo a B (una community scalata su più container).

Grazie per la correzione: manterrò i due casi esplicitamente separati in eventuali futuri runbook/post.