Conteneurs d'applications multiples pour un seul site Discourse

Vous pouvez héberger plusieurs installations Discourse autonomes sur un seul serveur (conteneurs séparés / ports séparés / app.yml séparés), sans utiliser la fonctionnalité « multisite » de Discourse.

Cela demande plus de travail manuel que le multisite, mais cela maintient les instances isolées et facilite la migration ultérieure d’un site individuel vers son propre serveur.

Un modèle pratique consiste à utiliser :

• une base de données Postgres externe (instance unique)
• un cache Redis externe (instance unique)
• plusieurs conteneurs web Discourse
• un nœud Sidekiq
• un proxy inverse avec des vérifications de santé

Cela évite complètement le multisite tout en permettant des économies de coûts pour des configurations à faible trafic.



PROCÉDURE OPÉRATIONNELLE POUR L’EXÉCUTION MULTI-CONTENEUR DE DISCOURSE
Postgres externe + Redis + HAProxy + app1 / app2


  1. PAQUETS HÔTE
Étape Commande
Mettre à jour le système apt-get update
Installer les outils de base apt-get install -y ca-certificates curl gnupg lsb-release
Installer HAProxy + certbot + socat apt-get install -y haproxy certbot socat

  1. RÉSEAU DOCKER (REQUIS)

Un réseau Docker défini par l’utilisateur est requis afin que les conteneurs puissent se résoudre par nom.

Étape Commande
Créer le réseau docker network create discourse-net
Vérifier docker network ls | grep discourse-net

Cela permet :

• DISCOURSE_DB_HOST=pg
• DISCOURSE_REDIS_HOST=redis

de fonctionner correctement.


  1. SECRETS
Objectif Commande
Superutilisateur Postgres export PG_SUPERPASS='REPLACE_ME_super_strong'
Mot de passe de la base Discourse export DISCOURSE_DBPASS='REPLACE_ME_discordb_strong'
Mot de passe Redis export REDIS_PASS='REPLACE_ME_redis_strong'
Clé secrète de base export SECRET_KEY_BASE="$(openssl rand -hex 64)"

  1. CONTENEUR POSTGRES
Étape Commande
Créer le répertoire mkdir -p /var/discourse/external/postgres
Lancer le conteneur 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
Vérifier docker ps | grep pg

  1. CRÉATION DE LA BASE DE DONNÉES
Étape Commande
Créer le rôle docker exec -it pg psql -U postgres -c "CREATE ROLE discourse LOGIN PASSWORD '$DISCOURSE_DBPASS';"
Créer la base docker exec -it pg psql -U postgres -c "CREATE DATABASE discourse OWNER discourse ENCODING 'UTF8' TEMPLATE template0;"
Recherche textuelle docker exec -it pg psql -U postgres -d discourse -c "ALTER DATABASE discourse SET default_text_search_config = 'pg_catalog.english';"
Tester la connexion docker exec -it pg psql -U discourse -d discourse -c "select 1;"

  1. EXTENSION PGVECTOR

Requis pour les versions récentes de Discourse.

Étape Commande
Installer docker exec -it pg bash -lc 'apt-get update && apt-get install -y postgresql-15-pgvector && rm -rf /var/lib/apt/lists/*'
Créer l’extension docker exec -it pg psql -U postgres -d discourse -c "CREATE EXTENSION IF NOT EXISTS vector;"
Vérifier docker exec -it pg psql -U postgres -d discourse -c "SELECT extname FROM pg_extension WHERE extname='vector';"

  1. CONTENEUR REDIS
Étape Commande
Créer le répertoire mkdir -p /var/discourse/external/redis

Modèle de configuration Redis :

requirepass REPLACE_ME_REDIS
appendonly yes
save 900 1
save 300 10
save 60 10000
Étape Commande
Écrire la config tee /var/discourse/external/redis/redis.conf >/dev/null <<EOF
Insérer le mot de passe sed -i "s/REPLACE_ME_REDIS/$REDIS_PASS/" /var/discourse/external/redis/redis.conf
Lancer 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
Tester l’authentification docker exec -it redis redis-cli -a "$REDIS_PASS" ping

  1. STRUCTURE DES RÉPERTOIRES DISCOURSE
Étape Commande
Créer le répertoire de base mkdir -p /var/discourse
Entrer cd /var/discourse
Cloner le dépôt git clone https://github.com/discourse/discourse_docker.git
Répertoire des conteneurs mkdir -p /var/discourse/containers
Journaux partagés mkdir -p /var/discourse/shared/web-only/log/var-log
Lier les conteneurs ln -sfn /var/discourse/containers /var/discourse/discourse_docker/containers
Lancer le lanceur ln -sfn /var/discourse/discourse_docker/launcher /var/discourse/launcher

  1. CONTENEURS D’APPLICATION

app1.yml
• web + sidekiq
• port 8001

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

app2.yml
• web uniquement
• port 8002
• sidekiq désactivé

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

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

  1. INITIALISATION (BOOTSTRAP)
Étape Commande
Entrer cd /var/discourse/discourse_docker
Initialiser app1 ./launcher bootstrap app1
Démarrer app1 ./launcher start app1
Initialiser app2 ./launcher bootstrap app2
Démarrer app2 ./launcher start app2

  1. VÉRIFICATIONS DE SANTÉ
Étape Commande
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. CERTIFICAT TLS
Étape Commande
Arrêter le proxy systemctl stop haproxy
Émettre le certificat certbot certonly --standalone -d example.com --agree-tos -m you@example.com --non-interactive
Démarrer le proxy systemctl start haproxy

  1. LOGIQUE 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. RECONSTRUCTIONS SANS INTERRUPTION DE SERVICE
Étape Commande
Désactiver app1 echo "disable server be_discourse/app1" | socat stdio /run/haproxy/admin.sock
Reconstruire app1 ./launcher rebuild app1
Activer app1 echo "enable server be_discourse/app1" | socat stdio /run/haproxy/admin.sock
Étape Commande
Désactiver app2 echo "disable server be_discourse/app2" | socat stdio /run/haproxy/admin.sock
Reconstruire app2 ./launcher rebuild app2
Activer app2 echo "enable server be_discourse/app2" | socat stdio /run/haproxy/admin.sock

FIN

Réseau Docker requis
Postgres et Redis externes
pgvector installé
Sidekiq isolé à app1
Vérifications de santé HAProxy activées
Redirection de maintenance active
Reconstructions progressives prises en charge

Migration d'un site vers son propre serveur ultérieurement

Un avantage de l’exécution d’installations Discourse entièrement autonomes (au lieu du multisite) est que la migration est simple et à faible risque.

Chaque instance Discourse dispose déjà de :

• son propre conteneur
• ses propres uploads
• sa propre base de données
• sa propre utilisation de Redis
• son propre app.yml

Aucune séparation du multisite n’est nécessaire.


Étapes de migration de haut niveau

  1. Provisionner un nouveau VPS

Installer Docker et Discourse normalement sur le nouveau serveur.
Ne pas configurer le multisite.


  1. Créer une sauvegarde complète

Depuis le site source :

Admin → Sauvegardes → Créer une sauvegarde

Télécharger le fichier de sauvegarde.

Cela inclut :

• la base de données
• les uploads
• les utilisateurs
• les paramètres
• les thèmes


  1. Restaurer sur le nouveau serveur

Sur le nouveau serveur :

• effectuer la configuration initiale complète
• se connecter en tant qu’administrateur
• télécharger la sauvegarde
• restaurer

Discourse gère automatiquement la compatibilité du schéma.


  1. Basculement DNS

Mettre à jour l’enregistrement A du domaine pour pointer vers l’adresse IP du nouveau serveur.

Une fois la propagation DNS terminée, les utilisateurs sont transférés de manière transparente.


  1. Désactiver l’ancien conteneur

Sur le serveur d’origine :

• arrêter l’ancien conteneur
• le supprimer une fois confiant

Les autres installations Discourse sur le même hôte ne sont pas affectées.


Pourquoi c’est plus simple que le multisite

Dans les configurations multisite, la migration nécessite souvent :

• la séparation des bases de données
• l’extraction des données spécifiques au site
• l’ajustement de multisite.yml
• la refonte de Sidekiq
• la reconfiguration des uploads et des e-mails

Avec des installations autonomes, rien de tout cela n’est nécessaire.

Chaque site est déjà indépendant.


Résumé

Cette approche échange un peu de complexité opérationnelle au début
pour une séparation très simple plus tard.

Elle fonctionne particulièrement bien pendant l’expérimentation
ou la construction précoce d’une communauté.


Quand cette approche n’est probablement pas adaptée

Cette configuration n’est généralement pas une bonne idée si :

• les sites s’attendent à un trafic modéré ou élevé dès le début
• vous dépendez fortement du support officiel de Discourse
• vous n’êtes pas à l’aise avec le débogage de Docker, du réseau ou des proxies inverses
• les exigences de disponibilité sont strictes ou critiques pour l’activité
• plusieurs sites sont étroitement couplés opérationnellement
• vous prévoyez des expérimentations fréquentes de plugins sur toutes les instances

Dans ces cas, soit :

• une configuration multisite prise en charge
ou
• une installation Discourse par serveur

entraînera généralement moins de surprises opérationnelles.


Note importante

Cette approche augmente la flexibilité de l’infrastructure,
mais augmente également la responsabilité de l’administrateur.

Elle fonctionne mieux lorsque la personne qui l’exécute est à l’aise de prendre en charge la pile complète
et de considérer les pannes occasionnelles comme faisant partie du processus d’apprentissage.

Si la stabilité et la possibilité de support sont les objectifs principaux,
un support configuré est presque toujours le meilleur choix.

une note supplémentaire qui est directement liée à la partie HAProxy de la configuration ci-dessus.

Il existe un comportement courant avec HAProxy + Discourse où la reconstruction d’un conteneur web (par exemple avec ./launcher rebuild app1) renverra brièvement des réponses 503 Service Unavailable car HAProxy continue d’envoyer du trafic au backend pendant qu’il redémarre. Ce n’est pas une erreur dans Discourse lui-même - cela se produit parce que le backend est momentanément indisponible pendant la reconstruction.

La solution de contournement recommandée consiste à utiliser la socket d’administration HAProxy pour :
\t1.\tdésactiver le serveur dans HAProxy avant la reconstruction, et
\t2.\tre-l’activer après la fin de la reconstruction

Cela empêche ces 503 transitoires.

Il existe une discussion Meta existante documentant ce comportement et l’explication de la solution de contournement :

Si quelqu’un ici utilise HAProxy pour des reconstructions progressives, ce fil de discussion fournit un contexte utile pour expliquer pourquoi les commandes de socket d’administration sont incluses dans le carnet d’exécution (runbook).

[quote=“Ethsim2, post:51, topic:392692”]Vous pouvez héberger plusieurs installations Discourse autonomes sur un seul serveur (conteneurs séparés / ports séparés / app.yml séparé), sans utiliser le « multisite » de Discourse.
[/quote]

Je fais quelque chose de similaire, avec un conteneur de style web-only par site et traefik (bien que j’aie aussi une configuration utilisant nginx-proxy) comme proxy inverse. J’ai essayé HAproxy pendant un moment (c’est ce qu’utilise CDCK, d’après mes dernières nouvelles), mais je l’ai trouvé lourd.

Je suis presque certain qu’il vous faut un redis par serveur Discourse.

[quote=“david, post:2, topic:219318”]Chaque Discourse a besoin de son propre serveur redis totalement séparé car message-bus utilise Redis Pub/Sub, qui est partagé entre toutes les bases de données sur un serveur Redis.
[/quote]

Je pense qu’il y a une petite divergence terminologique ici.

Lorsque vous dites « un Redis par serveur Discourse », je suis d’accord si par serveur, nous entendons un site Discourse logique unique.

Dans mon cas :

  • HAProxy est uniquement utilisé pour le basculement / la façade
  • Il n’y a pas de configuration multi-site
  • Il n’y a qu’un seul site Discourse (nom d’hôte unique, base de données Postgres unique)
  • Il se trouve qu’il y a deux conteneurs d’application capables de servir ce même site

Ceci est donc plus proche d’une configuration multi-web / HA, et non de deux installations Discourse indépendantes.

Dans cette configuration, le partage de Redis est attendu et requis - sinon vous perdez :

  • les sessions partagées
  • la livraison MessageBus
  • la limitation de débit (rate limiting)
  • la coordination des tâches de fond (background job coordination)

C’est le même modèle que l’exécution de plusieurs conteneurs web_only ou la mise à l’échelle horizontale des workers web :
plusieurs conteneurs d’application → une seule Postgres + un seul Redis.

Là où Redis ne doit pas être partagé, c’est lorsqu’il y a deux sites Discourse distincts (noms d’hôte / bases de données différents). Dans ce cas, chaque site a besoin de sa propre base de données Redis (ou instance) pour éviter les collisions de clés.

Donc, je pense que nous sommes alignés conceptuellement - c’est juste :

  • :white_check_mark: un Redis par site Discourse
  • :cross_mark: pas un Redis par conteneur d’application individuel

Heureux de clarifier davantage si j’ai mal compris quelque chose - je voulais juste expliquer la topologie plus clairement.

Oh. C’est le contraire de ce dont je pensais que nous parlions. Le titre est « Un serveur pour 2 communautés Discourse » Vous parlez de « deux serveurs pour une communauté Discourse »

Vous avez raison - j’ai confondu deux topologies différentes, et le titre du fil de discussion est l’indice.

Ce sujet concerne « un serveur pour 2 communautés » (deux sites indépendants).
Mon commentaire précédent sur « Redis externe (instance unique) » décrivait un modèle différent : « deux conteneurs d’application pour une communauté » (HA / multi-web pour un seul site).

Donc, pour récapituler clairement :

A) Deux sites Discourse indépendants sur un seul serveur (ce que demande l’OP)

  • Traitez-les comme deux installations distinctes
  • Ils devraient avoir des bases de données Postgres séparées et des instances Redis séparées (ou au moins une isolation suffisante pour le Pub/Sub de MessageBus, ce qui est le problème que vous avez cité)

B) Un site Discourse avec plusieurs conteneurs web/app (ce que je décrivais)

  • Ils doivent partager la même base de données Postgres et le même Redis pour ce site (sessions, limitation de débit, MessageBus, etc.)

Donc : :white_check_mark: votre avertissement concernant Redis s’applique à A (deux communautés / deux sites).
Ma note sur « Redis partagé » ne s’applique qu’à B (une communauté mise à l’échelle sur plusieurs conteneurs).

Merci pour la correction - je garderai les deux cas explicitement séparés dans les futurs guides/publications.