Usar Cloudflare R2 en un solo sitio de una instalación multisitio de Discourse
Si ejecutas una instalación multisitio de Discourse y solo quieres que un sitio utilice Cloudflare R2 (para cargas, copias de seguridad y, opcionalmente, activos estáticos), la guía estándar Configurar un proveedor de almacenamiento de objetos compatible con S3 para cargas no cubre completamente la particularidad multisitio. Esta publicación explica qué sucede realmente, qué se mantiene por sitio y qué es a nivel de clúster, y algunas complicaciones que encontré en el camino y que costaron tiempo resolver.
Lo fundamental a entender: GlobalSetting vs SiteSetting
Todo en esta configuración se reduce a una distinción:
- Variables de entorno en
app.yml(DISCOURSE_*) se convierten enGlobalSetting— se leen una vez al arrancar el contenedor, desde el entorno del proceso, y son compartidas por todos los sitios del clúster.RAILS_DBno tiene efecto sobre ellas. - Los campos de la interfaz de administración son
SiteSettingordinarios — se almacenan por sitio en la base de datos de cada sitio, con un alcance real limitado a ese único sitio.
Si existe un GlobalSetting para algo, sobrescribe silenciosamente y oculta el campo SiteSetting correspondiente en la interfaz de administración. Esto significa: lo que pongas en app.yml se aplica a todos los sitios, sin excepciones y sin un workaround con RAILS_DB.
Parte 1 — Cargas y copias de seguridad (realmente por sitio, fácil)
Esta parte funciona exactamente como esperarías. enable_s3_uploads, s3_upload_bucket, backup_location, s3_backup_bucket y los campos de credenciales son todos ajustes de sitio ordinarios. Configúralos solo a través de Administración → Ajustes → buscar “S3”, iniciando sesión en el sitio específico que quieres en R2, y deja app.yml sin tocar. Otros sitios en el clúster seguirán almacenando localmente.
Valores de ejemplo para R2:
Enable S3 uploads = true
Enable direct S3 uploads = true
S3 access key ID / secret access key = <tu token de R2>
S3 region = auto
S3 upload bucket = <nombre del bucket>
S3 endpoint = https://<account-id>.r2.cloudflarestorage.com
S3 CDN URL = https://uploads.tudominio.com
S3 use ACLs = false (R2 usa permisos a nivel de bucket, no ACL de objetos)
S3 backup bucket = <nombre del bucket de copias de seguridad>
Backup location = S3
Configura la política CORS de tu bucket directamente en el panel de Cloudflare (R2 no necesita la tarea rake de CORS de Discourse):
[
{
"AllowedOrigins": ["https://tu-sitio.tld"],
"AllowedMethods": ["GET", "PUT", ["POST", "DELETE", "HEAD"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3000
}
]
Parte 2 — Migración de cargas locales existentes
rake uploads:migrate_to_s3 solo lee la configuración de S3 de las variables de entorno — no tiene ningún respaldo a los ajustes de sitio, sin importar lo que esté configurado en la interfaz de administración. Esta es una verdadera deficiencia en la tarea, no un error de configuración. Pasa las credenciales en línea para una ejecución única en lugar de tocar app.yml:
./launcher enter app
RAILS_DB=default \
DISCOURSE_S3_REGION=auto \
DISCOURSE_S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com \
DISCOURSE_S3_BUCKET=<nombre del bucket> \
DISCOURSE_S3_ACCESS_KEY_ID=<clave> \
DISCOURSE_S3_SECRET_ACCESS_KEY=<secreto> \
rake uploads:migrate_to_s3
Estas variables de entorno solo existen para ese proceso de shell — nada persiste una vez que sales.
Error de checksum en versiones más nuevas de AWS SDK
Si te encuentras con:
Aws::S3::Errors::InvalidRequest: You can only specify one non-default checksum at a time.
esta es una incompatibilidad conocida entre las versiones recientes de aws-sdk-core (que por defecto envían un checksum CRC32) y R2. Corrígelo añadiendo dos variables de entorno más al mismo comando:
export AWS_REQUEST_CHECKSUM_CALCULATION=when_required
export AWS_RESPONSE_CHECKSUM_VALIDATION=when_required
Registros “no migrados” restantes después de una ejecución mayoritariamente exitosa
Si la tarea termina con algo como 1 of 1291 uploads are not migrated, no entres en pánico — todo lo demás ya se migró y las URLs de la BD ya fueron reescritas. Encuentra el rezagado en 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)
En mi caso, fue un registro Upload suelto (un zip de registro de copia de seguridad) que usaba la URL de estilo endpoint crudo de R2 en lugar del formato de URL de CDN que la comprobación espera — un falso positivo, no un fallo real. Corrige la URL o elimina el registro si no es contenido significativo.
Parte 3 — Activos estáticos (JS/CSS) — la parte que es realmente a nivel de clúster
Aquí es donde el objetivo de “solo para un sitio” choca con un muro. Los activos compilados se comparten en todo el clúster multisitio — hay un solo paquete JS/CSS compilado, no uno por sitio. Si los activos se sirven desde R2 o localmente se decide una vez, al arrancar Rails, mediante GlobalSetting.use_s3? — no hay una superposición por sitio para esto.
Si quieres descargar los activos a R2, debes poner los detalles de conexión (no la bandera de habilitar cargas) en 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: <nombre del bucket>
DISCOURSE_S3_CDN_URL: https://uploads.tudominio.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
Notas:
- No configures
DISCOURSE_CDN_URL. SoloDISCOURSE_S3_CDN_URL. Configurar ambos, con tu dominio principal proxy a través de Cloudflare, causa bucles de redirección según la propia advertencia de la guía principal de S3. - Usa
bundle exec rake, nobundle rake(error tipográfico fácil) — y usasudo -E -H -u discourse(la-HconfiguraHOMEcorrectamente para el usuariodiscourse; sin ella, Bundler retrocede a un directorio temporal en cada ejecución). - La descarga de activos afecta a ambos sitios. Las etiquetas
<script>/<link>de tu segundo sitio también comenzarán a resolverse a la URL de CDN de R2, ya que es el mismo paquete compilado. Asegúrate de queAllowedOriginsde CORS de tu bucket incluya el dominio de cada sitio. - Esto no fuerza las cargas reales de tu segundo sitio a S3 —
enable_s3_uploadssigue siendo un ajuste genuinamente por sitio, independiente de laGlobalSettingde servicio de activos. Verifica conSiteSetting.Upload.enable_s3_uploadsenrails cpara la BD de ese sitio después de recompilar.
USE_DB_S3_CONFIG — qué hace realmente (y qué no)
Verás USE_DB_S3_CONFIG=true referenciado en algunas configuraciones de la comunidad (p. ej. el chart de Bitnami) como una forma de hacer que s3:upload_assets lea las credenciales de los ajustes de sitio en lugar de las variables de entorno. Funciona para la tarea de carga en sí — pero no invierte GlobalSetting.use_s3?, que es la bandera que realmente controla si las URLs de activos se reescriben a la CDN en el momento de la renderización. Así que puedes empujar archivos a R2 con éxito usando USE_DB_S3_CONFIG y aun así ver que tu sitio sirve activos localmente, porque la comprobación de renderización de la página nunca ve “S3 está habilitado”. Si quieres que los activos se sirvan realmente desde R2, necesitas el verdadero DISCOURSE_USE_S3: true + variables de entorno de conexión en app.yml, no solo el workaround de configuración de BD.
Parte 4 — Qué no estará en R2, y por qué
Incluso con el hook funcionando, s3:upload_assets solo sube lo que está en Rails.application.assets.load_path — el manifiesto de Sprockets de Rails. Tres categorías se generan fuera de esa tubería y nunca aparecen en esta lista, por lo que permanecen en el disco local sin importar qué:
- CSS de temas — compilado dinámicamente por tema/esquema de color por
Stylesheet::Managerde Discourse, no a través de Sprockets. theme-javascripts— JS compilado por tema desdeThemeJavascriptCompiler.extra-locale/ archivos JS de localización — generados porJsLocaleHelper.
Esto no es un problema de configuración — estos nunca fueron activos de Sprockets en primer lugar, por lo que no hay ninguna variable de entorno que los incluya. En la práctica, esto significa: paquete JS core Ember/vendor → descargado a R2 con éxito; CSS/JS de temas y localizaciones → permanecen locales, servidos directamente por la aplicación. Este es un estado normal y funcional, no uno roto.
Resumen: qué poner realmente dónde
| Qué | Dónde | Alcance |
|---|---|---|
enable_s3_uploads, s3_upload_bucket, backup_location, s3_backup_bucket |
Interfaz de administración, por sitio | Por sitio |
Credenciales + DISCOURSE_S3_REGION/ENDPOINT/BUCKET/CDN_URL + DISCOURSE_USE_S3 |
app.yml, solo si quieres descarga de CDN de activos |
A nivel de clúster (inevitable) |
Hook after_assets_precompile |
app.yml |
A nivel de clúster |
AWS_REQUEST_CHECKSUM_CALCULATION / AWS_RESPONSE_CHECKSUM_VALIDATION |
app.yml |
A nivel de clúster (bandera de comportamiento de SDK inofensiva) |
| CORS en el bucket | Panel de Cloudflare | Debe incluir el dominio de cada sitio si los activos se comparten |
Si no necesitas la descarga de CDN de activos, omite la Parte 3 por completo — puedes ejecutar una configuración de R2 completamente funcional y genuinamente por sitio (solo cargas + copias de seguridad) sin tocar app.yml nunca.