Usando Cloudflare R2 em apenas um site em uma instalação multisite do Discourse
Se você executa uma instalação multisite do Discourse e deseja colocar apenas um dos seus sites no Cloudflare R2 (uploads, backups e, opcionalmente, assets estáticos), o guia padrão Configure an S3 compatible object storage provider for uploads não cobre completamente a peculiaridade do multisite. Este post detalha o que realmente acontece, o que permanece por site versus em todo o cluster, e algumas armadilhas que encontrei ao longo do caminho e que custaram tempo real para serem resolvidas.
O ponto central a entender: GlobalSetting vs SiteSetting
Tudo neste setup se resume a uma distinção:
- Variáveis de ambiente do
app.yml(DISCOURSE_*) tornam-seGlobalSettings — lidas uma vez na inicialização do container, a partir do ambiente do processo, compartilhadas por todos os sites no cluster.RAILS_DBnão tem efeito sobre elas. - Campos da interface de administração (Admin UI) são
SiteSettings comuns — armazenados por site no banco de dados próprio de cada site, genuinamente limitados a aquele único site.
Se uma GlobalSetting existir para algo, ela silenciosamente sobrescreve e oculta o campo correspondente de SiteSetting na interface de administração. Isso significa: o que você colocar no app.yml se aplica a todos os sites, sem exceções, sem contorno via RAILS_DB.
Parte 1 — Uploads e backups (genuinamente por site, fácil)
Esta parte funciona exatamente como você esperaria. enable_s3_uploads, s3_upload_bucket, backup_location, s3_backup_bucket e os campos de credenciais são todos configurações de site comuns. Configure-os apenas através de Admin → Settings → pesquise “S3”, logado no site específico que você deseja no R2, e deixe o app.yml intacto. Outros sites no cluster continuam armazenando localmente.
Valores de exemplo para R2:
Enable S3 uploads = true
Enable direct S3 uploads = true
S3 access key ID / secret access key = <seu token R2>
S3 region = auto
S3 upload bucket = <nome do bucket>
S3 endpoint = https://<account-id>.r2.cloudflarestorage.com
S3 CDN URL = https://uploads.yourdomain.com
S3 use ACLs = false (R2 usa permissões em nível de bucket, não ACLs de objeto)
S3 backup bucket = <nome do bucket de backup>
Backup location = S3
Defina a política CORS do seu bucket diretamente no painel do Cloudflare (R2 não precisa da tarefa rake de CORS do Discourse):
[
{
"AllowedOrigins": ["https://your-site.tld"],
"AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3000
}
]
Parte 2 — Migrando uploads locais existentes
rake uploads:migrate_to_s3 apenas lê a configuração do S3 das variáveis de ambiente — não tem nenhum recurso de fallback para configurações de site, independentemente do que esteja configurado na interface de administração. Esta é uma lacuna real na tarefa, não um erro de configuração. Passe as credenciais inline para uma execução única em vez de tocar no app.yml:
./launcher enter app
RAILS_DB=default \
DISCOURSE_S3_REGION=auto \
DISCOURSE_S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com \
DISCOURSE_S3_BUCKET=<nome do bucket> \
DISCOURSE_S3_ACCESS_KEY_ID=<chave> \
DISCOURSE_S3_SECRET_ACCESS_KEY=<segredo> \
rake uploads:migrate_to_s3
Essas variáveis de ambiente existem apenas para aquele processo de shell — nada persiste após você sair.
Erro de checksum em versões mais novas do AWS SDK
Se você encontrar:
Aws::S3::Errors::InvalidRequest: You can only specify one non-default checksum at a time.
esta é uma incompatibilidade conhecida entre versões recentes do aws-sdk-core (que por padrão enviam um checksum CRC32) e o R2. Corrija adicionando duas variáveis de ambiente adicionais ao mesmo comando:
export AWS_REQUEST_CHECKSUM_CALCULATION=when_required
export AWS_RESPONSE_CHECKSUM_VALIDATION=when_required
Registros “não migrados” restantes após uma execução majoritariamente bem-sucedida
Se a tarefa terminar com algo como 1 of 1291 uploads are not migrated, não entre em pânico — tudo o mais já foi migrado e as URLs do banco de dados já foram reescritas. Encontre o atrasado no 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)
No meu caso, foi um registro Upload avulso (um zip de log de backup) usando a URL de endpoint bruto do R2 em vez do formato de URL do CDN que a verificação espera — um falso positivo, não uma falha real. Corrija a URL ou exclua o registro se não for conteúdo significativo.
Parte 3 — Assets estáticos (JS/CSS) — a parte que é genuinamente em todo o cluster
É aqui que o objetivo de “apenas para um site” encontra um muro sólido. Os assets compilados são compartilhados em todo o cluster multisite — há um único bundle de JS/CSS compilado, não um por site. Se os assets são servidos do R2 ou localmente é decidido uma vez, na inicialização do Rails, via GlobalSetting.use_s3? — não há sobrescrita por site para isso.
Se você deseja descarregar os assets para o R2, você precisa colocar os detalhes de conexão (não a flag de habilitação de upload) no 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: <nome do bucket>
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
Notas:
- Não defina
DISCOURSE_CDN_URL. ApenasDISCOURSE_S3_CDN_URL. Definir ambos, com seu domínio principal proxied através do Cloudflare, causa loops de redirecionamento, conforme o próprio aviso do guia principal de S3. - Use
bundle exec rake, nãobundle rake(erro de digitação fácil) — e usesudo -E -H -u discourse(o-Hdefine oHOMEcorretamente para o usuáriodiscourse; sem isso, o Bundler recorre a um diretório temporário a cada execução). - O descarregamento de assets afeta ambos os sites. As tags
<script>/<link>do seu segundo site também começarão a resolver para a URL do CDN do R2, já que é o mesmo bundle compilado. Certifique-se de queAllowedOriginsdo CORS do seu bucket inclua o domínio de cada site. - Isso não força os uploads reais do seu segundo site para o S3 —
enable_s3_uploadspermanece uma configuração genuinamente por site, independente daGlobalSettingde serviço de assets. Verifique comSiteSetting.Upload.enable_s3_uploadsnorails cpara o banco de dados desse site após a reconstrução.
USE_DB_S3_CONFIG — o que ele realmente faz (e não faz)
Você verá USE_DB_S3_CONFIG=true referenciado em alguns setups da comunidade (ex.: chart da Bitnami) como uma maneira de fazer s3:upload_assets ler credenciais das configurações de site em vez de variáveis de ambiente. Funciona para a tarefa de upload em si — mas não altera GlobalSetting.use_s3?, que é a flag que realmente controla se as URLs de assets são reescritas para o CDN no momento da renderização. Então, você pode empacotar arquivos para o R2 com sucesso usando USE_DB_S3_CONFIG e ainda ver seu site servindo assets localmente, porque a verificação de renderização da página nunca vê “S3 está habilitado”. Se você deseja que os assets sejam realmente servidos do R2, você precisa da real DISCOURSE_USE_S3: true + variáveis de ambiente de conexão no app.yml, não apenas o contorno de configuração do banco de dados.
Parte 4 — O que ainda não estará no R2, e por quê
Mesmo com o hook funcionando, s3:upload_assets apenas envia o que está em Rails.application.assets.load_path — o manifesto Sprockets do Rails. Três categorias são geradas fora desse pipeline e nunca aparecem nesta lista, então elas permanecem no disco local, independentemente do que for feito:
- CSS de temas — compilado dinamicamente por tema/esquema de cores pelo
Stylesheet::Managerdo Discourse, não através do Sprockets. theme-javascripts— JS compilado por tema doThemeJavascriptCompiler.extra-locale/ arquivos JS de localização — gerados peloJsLocaleHelper.
Isso não é um problema de configuração — estes nunca foram assets Sprockets para começar, então não há variável de ambiente que os inclua. Na prática, isso significa: bundle de JS core Ember/vendor → descarregado para o R2 com sucesso; CSS/JS de temas e localizações → permanecem locais, servidos diretamente pelo app. Este é um estado normal e funcional, não um quebrado.
Resumo: o que colocar onde
| O quê | Onde | Escopo |
|---|---|---|
enable_s3_uploads, s3_upload_bucket, backup_location, s3_backup_bucket |
Interface de Administração, por site | Por site |
Credenciais + DISCOURSE_S3_REGION/ENDPOINT/BUCKET/CDN_URL + DISCOURSE_USE_S3 |
app.yml, apenas se você deseja descarregamento de CDN de assets |
Em todo o cluster (inevitável) |
Hook after_assets_precompile |
app.yml |
Em todo o cluster |
AWS_REQUEST_CHECKSUM_CALCULATION / AWS_RESPONSE_CHECKSUM_VALIDATION |
app.yml |
Em todo o cluster (flag de comportamento do SDK inofensiva) |
| CORS no bucket | Painel do Cloudflare | Deve incluir o domínio de cada site se os assets forem compartilhados |
Se você não precisar de descarregamento de CDN de assets, pule a Parte 3 inteiramente — você pode executar um setup R2 totalmente funcional e genuinamente por site (apenas uploads + backups) sem nunca tocar no app.yml.