Cloudflare R2 auf nur einer Site in einer Discourse-Multisite-Installation verwenden
Wenn du eine Multisite-Discourse-Installation betreibst und nur eine deiner Sites auf Cloudflare R2 hosten möchtest (Uploads, Backups und optional statische Assets), deckt der Standardleitfaden S3-kompatiblen Object-Storage-Provider für Uploads konfigurieren die Multisite-Spezifik nicht vollständig ab. Dieser Beitrag erklärt, was tatsächlich passiert, was pro Site und was clusterweit gilt, sowie einige Tücken, auf die ich gestoßen bin und die Zeit kosteten, um sie zu verstehen.
Der Kernpunkt: GlobalSetting vs. SiteSetting
Bei dieser Einrichtung kommt alles auf eine Unterscheidung an:
- Umgebungsvariablen in
app.yml(DISCOURSE_*) werden zuGlobalSettings — sie werden beim Start des Containers einmalig aus der Prozessumgebung gelesen und von jeder Site im Cluster geteilt.RAILS_DBhat keinen Einfluss darauf. - Felder in der Admin-Oberfläche sind gewöhnliche
SiteSettings — sie werden pro Site in der eigenen Datenbank der jeweiligen Site gespeichert und sind tatsächlich auf diese eine Site beschränkt.
Wenn es eine GlobalSetting für etwas gibt, überschreibt sie die entsprechende SiteSetting-Einstellung in der Admin-Oberfläche stillschweigend und blendet das Feld aus. Das bedeutet: Was auch immer du in app.yml einträgst, gilt für jede Site, ohne Ausnahmen und ohne RAILS_DB-Workaround.
Teil 1 — Uploads und Backups (tatsächlich pro Site, einfach)
Dieser Teil funktioniert genau wie erhofft. enable_s3_uploads, s3_upload_bucket, backup_location, s3_backup_bucket und die Felder für die Zugangsdaten sind alle gewöhnliche Site-Einstellungen. Konfiguriere sie nur über Admin → Einstellungen → Suche „S3“, während du bei der spezifischen Site angemeldet bist, die auf R2 laufen soll, und lass app.yml unverändert. Andere Sites im Cluster speichern weiterhin lokal.
Beispielwerte für R2:
Enable S3 uploads = true
Enable direct S3 uploads = true
S3 access key ID / secret access key = <dein R2-Token>
S3 region = auto
S3 upload bucket = <Bucket-Name>
S3 endpoint = https://<account-id>.r2.cloudflarestorage.com
S3 CDN URL = https://uploads.yourdomain.com
S3 use ACLs = false (R2 verwendet bucket-basierte Berechtigungen, keine Objekt-ACLs)
S3 backup bucket = <Backup-Bucket-Name>
Backup location = S3
Setze die CORS-Richtlinie deines Buckets direkt im Cloudflare-Dashboard (R2 benötigt die CORS-Rake-Aufgabe von Discourse nicht):
[
{
"AllowedOrigins": ["https://your-site.tld"],
"AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3000
}
]
Teil 2 — Migration bestehender lokaler Uploads
rake uploads:migrate_to_s3 liest die S3-Konfiguration nur aus Umgebungsvariablen — es gibt keinerlei Fallback auf Site-Einstellungen, unabhängig davon, was in der Admin-Oberfläche konfiguriert ist. Dies ist eine echte Lücke in der Aufgabe, kein Konfigurationsfehler. Übergeib die Zugangsdaten inline für einen einmaligen Lauf, anstatt app.yml zu verändern:
./launcher enter app
RAILS_DB=default \
DISCOURSE_S3_REGION=auto \
DISCOURSE_S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com \
DISCOURSE_S3_BUCKET=<bucket name> \
DISCOURSE_S3_ACCESS_KEY_ID=<key> \
DISCOURSE_S3_SECRET_ACCESS_KEY=<secret> \
rake uploads:migrate_to_s3
Diese Umgebungsvariablen existieren nur für diesen Shell-Prozess — nichts bleibt erhalten, wenn du ihn verlässt.
Prüfsummenfehler bei neueren AWS-SDK-Versionen
Wenn du folgenden Fehler erhältst:
Aws::S3::Errors::InvalidRequest: You can only specify one non-default checksum at a time.
liegt dies an einer bekannten Inkompatibilität zwischen neueren aws-sdk-core-Versionen (die standardmäßig eine CRC32-Prüfsumme senden) und R2. Behebe dies, indem du zwei weitere Umgebungsvariablen zum selben Befehl hinzufügst:
export AWS_REQUEST_CHECKSUM_CALCULATION=when_required
export AWS_RESPONSE_CHECKSUM_VALIDATION=when_required
Verbliebene „nicht migrierte“ Einträge nach einem weitgehend erfolgreichen Lauf
Wenn die Aufgabe mit etwas wie 1 of 1291 uploads are not migrated endet, in Panik verfallen — alles andere wurde bereits migriert und die DB-URLs wurden bereits umgeschrieben. Finde den Ausreißer in 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)
In meinem Fall war es ein isolierter Upload-Eintrag (ein Backup-Log-Zip), der die rohe Endpoint-URL von R2 statt des CDN-URL-Formats verwendete, das der Check erwartet — ein Fehlalarm, kein echter Fehler. Korrigiere die URL oder lösche den Eintrag, wenn er keinen sinnvollen Inhalt darstellt.
Teil 3 — Statische Assets (JS/CSS) — der Teil, der tatsächlich clusterweit ist
Hier stößt das Ziel „nur für eine Site“ auf eine harte Wand. Kompilierte Assets werden im gesamten Multisite-Cluster geteilt — es gibt ein kompiliertes JS/CSS-Bundle, nicht eines pro Site. Ob Assets von R2 oder lokal ausgeliefert werden, wird einmalig beim Rails-Start über GlobalSetting.use_s3? entschieden — es gibt keine pro-Site-Überschreibung dafür.
Wenn du Assets auf R2 auslagern möchtest, musst du die Verbindungsdetails (nicht das Upload-Erkennungsflag) in app.yml eintragen:
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: <bucket name>
DISCOURSE_S3_CDN_URL: https://uploads.yourdomain.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
Hinweise:
- Setze
DISCOURSE_CDN_URLnicht. NurDISCOURSE_S3_CDN_URL. Wenn du beide setzt und deine Hauptdomain durch Cloudflare proxied wird, führt dies zu Umleitungsschleifen, wie in der eigenen Warnung des Haupt-S3-Leitfadens beschrieben. - Verwende
bundle exec rake, nichtbundle rake(leichter Tippfehler) — und verwendesudo -E -H -u discourse(das-HsetztHOMEkorrekt für dendiscourse-Benutzer; ohne es fällt Bundler bei jedem Lauf in ein Temp-Verzeichnis zurück). - Die Asset-Auslagerung betrifft beide Sites. Die
<script>/<link>-Tags deiner zweiten Site werden ebenfalls auf die R2-CDN-URL auflösen, da es dasselbe kompilierte Bundle ist. Stelle sicher, dass die CORS-AllowedOriginsdeines Buckets die Domains aller Sites enthalten. - Dies zwingt die tatsächlichen Uploads deiner zweiten Site nicht auf S3 —
enable_s3_uploadsbleibt eine echte pro-Site-Einstellung, unabhängig von der Asset-Bereitstellungs-GlobalSetting. Verifiziere dies mitSiteSetting.Upload.enable_s3_uploadsinrails cfür die DB dieser Site nach dem Neuaufbau.
USE_DB_S3_CONFIG — was es tatsächlich tut (und was nicht)
Du wirst USE_DB_S3_CONFIG=true in einigen Community-Setups (z. B. im Bitnami-Chart) als Methode sehen, damit s3:upload_assets Zugangsdaten aus Site-Einstellungen statt aus Umgebungsvariablen liest. Es funktioniert für die Upload-Aufgabe selbst — aber es schaltet nicht GlobalSetting.use_s3? um, was das Flag ist, das tatsächlich steuert, ob Asset-URLs beim Rendern auf die CDN umgeschrieben werden. Du kannst also erfolgreich Dateien mit USE_DB_S3_CONFIG auf R2 schieben und trotzdem sehen, dass deine Site Assets lokal ausliefert, da der Seiten-Render-Check nie „S3 ist aktiviert“ sieht. Wenn du Assets tatsächlich von R2 ausliefern möchtest, brauchst du die echten DISCOURSE_USE_S3: true + Verbindungs-Umgebungsvariablen in app.yml, nicht nur den DB-Konfigurations-Workaround.
Teil 4 — Was trotzdem nicht auf R2 sein wird, und warum
Selbst wenn der Hook funktioniert, lädt s3:upload_assets nur das hoch, was in Rails.application.assets.load_path ist — dem Sprockets-Manifest von Rails. Drei Kategorien werden außerhalb dieser Pipeline generiert und erscheinen nie in dieser Liste, daher bleiben sie unabhängig von der Konfiguration auf der lokalen Festplatte:
- Theme-CSS — dynamisch pro Theme/Farbschema durch Discourses
Stylesheet::Managerkompiliert, nicht über Sprockets. theme-javascripts— pro Theme kompiliertes JS vonThemeJavascriptCompiler.extra-locale/ Locale-JS-Dateien — generiert durchJsLocaleHelper.
Dies ist kein Konfigurationsproblem — diese waren nie Sprockets-Assets, daher gibt es keine Umgebungsvariable, die sie einbezieht. In der Praxis bedeutet das: Kern-Ember/Vendor-JS-Bundle → erfolgreich auf R2 ausgelagert; Theme-CSS/JS und Locales → bleiben lokal, direkt von der App ausgeliefert. Dies ist ein normaler, funktionierender Zustand, kein kaputter.
Zusammenfassung: Was tatsächlich wohin gehört
| Was | Wo | Geltungsbereich |
|---|---|---|
enable_s3_uploads, s3_upload_bucket, backup_location, s3_backup_bucket |
Admin-Oberfläche, pro Site | Pro Site |
Zugangsdaten + DISCOURSE_S3_REGION/ENDPOINT/BUCKET/CDN_URL + DISCOURSE_USE_S3 |
app.yml, nur wenn du Asset-CDN-Auslagerung möchtest |
Clusterweit (unvermeidlich) |
after_assets_precompile-Hook |
app.yml |
Clusterweit |
AWS_REQUEST_CHECKSUM_CALCULATION / AWS_RESPONSE_CHECKSUM_VALIDATION |
app.yml |
Clusterweit (harmloses SDK-Verhaltensflag) |
| CORS im Bucket | Cloudflare-Dashboard | Muss die Domains aller Sites enthalten, wenn Assets geteilt werden |
Wenn du keine Asset-CDN-Auslagerung benötigst, überspringe Teil 3 komplett — du kannst eine vollständig funktionierende, tatsächlich pro-Site-R2-Einrichtung (nur Uploads + Backups) betreiben, ohne jemals app.yml anzufassen.