Mutisite et objets Cloudflare R2

Utiliser Cloudflare R2 sur un seul site dans une installation multisite de Discourse

Si vous exécutez une installation multisite de Discourse et que vous souhaitez n’utiliser Cloudflare R2 (téléversements, sauvegardes et éventuellement les assets statiques) que pour un seul de vos sites, le guide standard Configurer un fournisseur de stockage d’objets compatible S3 pour les téléversements ne couvre pas tout à fait la particularité du multisite. Cet article détaille ce qui se passe réellement, ce qui reste spécifique à chaque site par rapport à ce qui est global au cluster, et quelques pièges que j’ai rencontrés et qui m’ont coûté du temps à résoudre.

Le point fondamental à comprendre : GlobalSetting vs SiteSetting

Tout dans cette configuration repose sur une distinction :

  • Les variables d’environnement app.yml (DISCOURSE_*) deviennent des GlobalSetting — lues une seule fois au démarrage du conteneur, depuis l’environnement du processus, et partagées par tous les sites du cluster. RAILS_DB n’a aucun effet sur elles.
  • Les champs de l’interface d’administration sont de simples SiteSetting — stockés par site dans la base de données respective de chaque site, véritablement limités à ce site précis.

Si un GlobalSetting existe pour un paramètre, il écrase silencieusement le SiteSetting correspondant et masque le champ dans l’interface d’administration. Cela signifie : tout ce que vous mettez dans app.yml s’applique à tous les sites, sans exception, sans contournement via RAILS_DB.

Partie 1 — Téléversements et sauvegardes (véritablement par site, facile)

Cette partie fonctionne exactement comme on le souhaite. enable_s3_uploads, s3_upload_bucket, backup_location, s3_backup_bucket et les champs de credentials sont tous des paramètres de site ordinaires. Configurez-les uniquement via Administration → Paramètres → recherchez “S3”, connecté au site spécifique que vous souhaitez sur R2, et laissez app.yml inchangé. Les autres sites du cluster continuent de stocker localement.

Exemple de valeurs pour R2 :

Enable S3 uploads = true
Enable direct S3 uploads = true
S3 access key ID / secret access key = <votre jeton R2>
S3 region = auto
S3 upload bucket = <nom du bucket>
S3 endpoint = https://<account-id>.r2.cloudflarestorage.com
S3 CDN URL = https://uploads.votredomaine.com
S3 use ACLs = false   (R2 utilise des autorisations au niveau du bucket, pas des ACL d'objets)
S3 backup bucket = <nom du bucket de sauvegarde>
Backup location = S3

Configurez la politique CORS de votre bucket directement dans le tableau de bord Cloudflare (R2 n’a pas besoin de la tâche rake CORS de Discourse) :

[
  {
    "AllowedOrigins": ["https://votre-site.tld"],
    "AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],
    "AllowedHeaders": ["*"],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3000
  }
]

Partie 2 — Migration des téléversements locaux existants

rake uploads:migrate_to_s3 ne lit la configuration S3 que depuis les variables d’environnement — il n’a aucun repli vers les paramètres de site, quelle que soit la configuration dans l’interface d’administration. C’est un vrai manquement dans la tâche, pas une erreur de configuration. Passez les credentials en ligne pour une exécution ponctuelle plutôt que de toucher à app.yml :

./launcher enter app

RAILS_DB=default \
DISCOURSE_S3_REGION=auto \
DISCOURSE_S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com \
DISCOURSE_S3_BUCKET=<nom du bucket> \
DISCOURSE_S3_ACCESS_KEY_ID=<clé> \
DISCOURSE_S3_SECRET_ACCESS_KEY=<secret> \
rake uploads:migrate_to_s3

Ces variables d’environnement n’existent que pour ce processus shell — rien ne persiste une fois que vous quittez.

Erreur de checksum avec les versions récentes du SDK AWS

Si vous obtenez :

Aws::S3::Errors::InvalidRequest: You can only specify one non-default checksum at a time.

il s’agit d’une incompatibilité connue entre les versions récentes de aws-sdk-core (qui envoient par défaut un checksum CRC32) et R2. Corrigez en ajoutant deux variables d’environnement supplémentaires à la même commande :

export AWS_REQUEST_CHECKSUM_CALCULATION=when_required
export AWS_RESPONSE_CHECKSUM_VALIDATION=when_required

Enregistrements « non migrés » restants après une exécution principalement réussie

Si la tâche se termine avec un message comme 1 of 1291 uploads are not migrated, ne paniquez pas — tout le reste a déjà été migré et les URL de la BDD ont déjà été réécrites. Trouvez l’élément récalcitrant dans rails c :

base_url = File.join(SiteSetting.Upload.s3_base_url, "original/")
Upload.by_users.where("url NOT LIKE '#{base_url}%'").pluck(:id, :url, :original_filename)

Dans mon cas, il s’agissait d’un enregistrement Upload errant (un zip de journal de sauvegarde) utilisant l’URL brute de style endpoint de R2 au lieu du format d’URL CDN que la vérification attend — un faux positif, pas un vrai échec. Corrigez l’URL ou supprimez l’enregistrement s’il ne s’agit pas de contenu significatif.

Partie 3 — Assets statiques (JS/CSS) — la partie véritablement globale au cluster

C’est ici que l’objectif « pour un seul site » rencontre un mur. Les assets compilés sont partagés sur tout le cluster multisite — il y a un seul bundle JS/CSS compilé, pas un par site. Le fait que les assets soient servis depuis R2 ou localement est décidé une fois, au démarrage de Rails, via GlobalSetting.use_s3? — il n’y a pas de remplacement par site pour cela.

Si vous souhaitez déporter les assets sur R2, vous devez mettre les détails de connexion (et non le drapeau d’activation des téléversements) dans app.yml :

env:
  DISCOURSE_USE_S3: true
  DISCOURSE_S3_REGION: auto
  DISCOURSE_S3_ENDPOINT: https://<account-id>.r2.cloudflarestorage.com
  DISCOURSE_S3_ACCESS_KEY_ID: "xxx"
  DISCOURSE_S3_SECRET_ACCESS_KEY: "xxx"
  DISCOURSE_S3_BUCKET: <nom du bucket>
  DISCOURSE_S3_CDN_URL: https://uploads.votredomaine.com
  AWS_REQUEST_CHECKSUM_CALCULATION: when_required
  AWS_RESPONSE_CHECKSUM_VALIDATION: when_required

hooks:
  after_assets_precompile:
    - exec:
        cd: $home
        cmd:
          - sudo -E -H -u discourse bundle exec rake s3:upload_assets
          - sudo -E -H -u discourse bundle exec rake s3:expire_missing_assets

Notes :

  • Ne définissez pas DISCOURSE_CDN_URL. Seulement DISCOURSE_S3_CDN_URL. Définir les deux, avec votre domaine principal proxifié via Cloudflare, provoque des boucles de redirection selon l’avertissement du guide S3 principal lui-même.
  • Utilisez bundle exec rake, pas bundle rake (faute de frappe facile) — et utilisez sudo -E -H -u discourse (le -H définit correctement HOME pour l’utilisateur discourse ; sans cela, Bundler revient à un répertoire temporaire à chaque exécution).
  • Le déport des assets affecte les deux sites. Les balises <script>/<link> de votre deuxième site commenceront également à résoudre vers l’URL CDN de R2, car c’est le même bundle compilé. Assurez-vous que AllowedOrigins de la CORS de votre bucket inclut le domaine de chaque site.
  • Cela ne force pas les téléversements réels de votre deuxième site sur S3 — enable_s3_uploads reste un paramètre par site véritablement indépendant du GlobalSetting de service des assets. Vérifiez avec SiteSetting.Upload.enable_s3_uploads dans rails c pour la BDD de ce site après reconstruction.

USE_DB_S3_CONFIG — ce qu’il fait réellement (et ne fait pas)

Vous verrez USE_DB_S3_CONFIG=true référencé dans certaines configurations communautaires (par exemple le chart Bitnami) comme un moyen de faire lire les credentials à s3:upload_assets depuis les paramètres de site au lieu des variables d’environnement. Cela fonctionne pour la tâche de téléversement elle-même — mais cela ne change pas GlobalSetting.use_s3?, qui est le drapeau qui contrôle réellement si les URL des assets sont réécrites vers le CDN au moment du rendu. Vous pouvez donc pousser des fichiers vers R2 avec succès avec USE_DB_S3_CONFIG et voir votre site servir les assets localement, car la vérification de rendu de la page ne voit jamais que « S3 est activé ». Si vous voulez que les assets soient réellement servis depuis R2, vous avez besoin des vraies variables d’environnement DISCOURSE_USE_S3: true + connexion dans app.yml, pas seulement du contournement de configuration BDD.

Partie 4 — Ce qui ne sera toujours pas sur R2, et pourquoi

Même avec le hook fonctionnel, s3:upload_assets ne téléverse que ce qui est dans Rails.application.assets.load_path — le manifeste Sprockets de Rails. Trois catégories sont générées en dehors de ce pipeline et n’apparaissent jamais dans cette liste, donc elles restent sur le disque local quoi qu’il arrive :

  • CSS des thèmes — compilé dynamiquement par thème/schéma de couleur par Stylesheet::Manager de Discourse, pas via Sprockets.
  • theme-javascripts — JS compilé par thème par ThemeJavascriptCompiler.
  • Fichiers JS extra-locale / de localisation — générés par JsLocaleHelper.

Ce n’est pas un problème de configuration — ce n’étaient jamais des assets Sprockets au départ, donc il n’y a pas de variable d’environnement qui les intègre. En pratique, cela signifie : bundle JS Ember/vendor principal → déporté sur R2 avec succès ; CSS/JS des thèmes et localisations → restent locaux, servis directement par l’application. C’est un état normal et fonctionnel, pas un état cassé.

Résumé : ce qu’il faut mettre où

Élément Emplacement Portée
enable_s3_uploads, s3_upload_bucket, backup_location, s3_backup_bucket Interface d’administration, par site Par site
Credentials + DISCOURSE_S3_REGION/ENDPOINT/BUCKET/CDN_URL + DISCOURSE_USE_S3 app.yml, uniquement si vous souhaitez le déport CDN des assets Globale au cluster (inévitable)
Hook after_assets_precompile app.yml Globale au cluster
AWS_REQUEST_CHECKSUM_CALCULATION / AWS_RESPONSE_CHECKSUM_VALIDATION app.yml Globale au cluster (drapeau de comportement SDK inoffensif)
CORS sur le bucket Tableau de bord Cloudflare Doit inclure le domaine de chaque site si les assets sont partagés

Si vous n’avez pas besoin du déport CDN des assets, sautez la Partie 3 entièrement — vous pouvez exécuter une configuration R2 pleinement fonctionnelle, véritablement par site (téléversements + sauvegardes uniquement) sans jamais toucher à app.yml.

1 « J'aime »