Использование Cloudflare R2 только для одного сайта в мульти-сайтовой установке Discourse
Если у вас установлена мульти-сайтовая система Discourse и вы хотите разместить на Cloudflare R2 только один из ваших сайтов (загрузки, резервные копии и, при необходимости, статические ресурсы), стандартное руководство Настройка провайдера объектного хранилища, совместимого с S3, для загрузок, не полностью покрывает особенности мульти-сайтовой конфигурации. В этом посте описано, что происходит на самом деле, какие настройки действуют для каждого сайта отдельно, а какие — для всего кластера, а также несколько подводных камней, с которыми я столкнулся и на решение которых ушло немало времени.
Ключевой момент: GlobalSetting и SiteSetting
Весь этот процесс сводится к одному различию:
- Переменные окружения в
app.yml(DISCOURSE_*) становятсяGlobalSetting— они читаются один раз при запуске контейнера из переменных окружения процесса и разделяются всеми сайтами в кластере.RAILS_DBна них не влияет. - Поля в административном интерфейсе — это обычные
SiteSetting— они хранятся для каждого сайта в его собственной базе данных и действительно относятся только к этому конкретному сайту.
Если для чего-либо существует GlobalSetting, он молча переопределяет и скрывает соответствующее поле SiteSetting в административном интерфейсе. Это означает: все, что вы указываете в app.yml, применяется ко всем сайтам, без исключений, и обходных путей через RAILS_DB не существует.
Часть 1 — Загрузки и резервные копии (действительно на уровне сайта, просто)
Эта часть работает именно так, как вы и надеетесь. enable_s3_uploads, s3_upload_bucket, backup_location, s3_backup_bucket и поля с учетными данными — это все обычные настройки сайта. Настройте их только через Админка → Настройки → поиск “S3”, войдя в тот конкретный сайт, который вы хотите разместить на R2, и не трогайте app.yml. Другие сайты в кластере продолжат хранить данные локально.
Пример значений для R2:
Enable S3 uploads = true
Enable direct S3 uploads = true
S3 access key ID / secret access key = <ваш токен R2>
S3 region = auto
S3 upload bucket = <имя бакета>
S3 endpoint = https://<account-id>.r2.cloudflarestorage.com
S3 CDN URL = https://uploads.yourdomain.com
S3 use ACLs = false (R2 использует разрешения на уровне бакета, а не ACL объектов)
S3 backup bucket = <имя бакета для резервных копий>
Backup location = S3
Настройте политику CORS для вашего бакета непосредственно в панели управления Cloudflare (для R2 не нужна задача rake для CORS в Discourse):
[
{
"AllowedOrigins": ["https://your-site.tld"],
"AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3000
}
]
Часть 2 — Миграция существующих локальных загрузок
rake uploads:migrate_to_s3 только читает конфигурацию S3 из переменных окружения — у него нет никакого резервного механизма для чтения из настроек сайта, независимо от того, что настроено в административном интерфейсе. Это реальный пробел в задаче, а не ошибка конфигурации. Передайте учетные данные напрямую для однократного запуска, вместо того чтобы изменять app.yml:
./launcher enter app
RAILS_DB=default \
DISCOURSE_S3_REGION=auto \
DISCOURSE_S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com \
DISCOURSE_S3_BUCKET=<имя бакета> \
DISCOURSE_S3_ACCESS_KEY_ID=<ключ> \
DISCOURSE_S3_SECRET_ACCESS_KEY=<секрет> \
rake uploads:migrate_to_s3
Эти переменные окружения существуют только для этого процесса оболочки — ничего не сохраняется после выхода.
Ошибка контрольной суммы в новых версиях AWS SDK
Если вы столкнетесь с:
Aws::S3::Errors::InvalidRequest: You can only specify one non-default checksum at a time.
это известная несовместимость между недавними версиями aws-sdk-core (которые по умолчанию отправляют контрольную сумму CRC32) и R2. Исправьте это, добавив две дополнительные переменные окружения к той же команде:
export AWS_REQUEST_CHECKSUM_CALCULATION=when_required
export AWS_RESPONSE_CHECKSUM_VALIDATION=when_required
Оставшиеся записи «не мигрированы» после в целом успешного запуска
Если задача завершается сообщением вроде 1 of 1291 uploads are not migrated, не паникуйте — все остальное уже мигрировано, и URL в БД уже переписаны. Найдите «отставший» файл в 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)
В моем случае это была случайная запись Upload (zip-архив журнала резервного копирования), использующая «сырой» URL-адрес R2 в стиле endpoint, а не формат URL CDN, который ожидает проверка — это ложное срабатывание, а не реальная ошибка. Исправьте URL или удалите запись, если это не содержимое, имеющее значение.
Часть 3 — Статические ресурсы (JS/CSS) — часть, которая действительно действует на весь кластер
Здесь цель «только для одного сайта» упирается в твердую стену. Скомпилированные ресурсы разделяются между всем мульти-сайтовым кластером — существует один скомпилированный бандл JS/CSS, а не один для каждого сайта. То, обслуживаются ли ресурсы с R2 или локально, определяется один раз при запуске Rails через GlobalSetting.use_s3? — переопределения на уровне сайта для этого не существует.
Если вы хотите выгрузить ресурсы на R2, вам нужно указать сведения о подключении (а не флаг включения загрузок) в 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: <имя бакета>
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
Примечания:
- Не устанавливайте
DISCOURSE_CDN_URL. ТолькоDISCOURSE_S3_CDN_URL. Установка обоих, когда ваш основной домен проксируется через Cloudflare, приводит к циклам перенаправления, согласно предупреждению в самом руководстве по S3. - Используйте
bundle exec rake, а неbundle rake(легко ошибиться) — и используйтеsudo -E -H -u discourse(флаг-Hправильно устанавливаетHOMEдля пользователяdiscourse; без него Bundler каждый раз откатывается к временному каталогу). - Выгрузка ресурсов влияет на оба сайта. Теги
<script>/<link>вашего второго сайта также начнут разрешаться в URL CDN R2, так как это тот же скомпилированный бандл. Убедитесь, чтоAllowedOriginsв CORS вашего бакета включает домены всех сайтов. - Это не принудительно переносит фактические загрузки вашего второго сайта на S3 —
enable_s3_uploadsостается настоящей настройкой на уровне сайта, независимой отGlobalSettingдля обслуживания ресурсов. Проверьте это с помощьюSiteSetting.Upload.enable_s3_uploadsвrails cдля БД этого сайта после пересборки.
USE_DB_S3_CONFIG — что оно действительно делает (и чего не делает)
Вы можете увидеть USE_DB_S3_CONFIG=true, упомянутое в некоторых конфигурациях сообщества (например, в чарте Bitnami) как способ заставить s3:upload_assets читать учетные данные из настроек сайта, а не из переменных окружения. Это работает для самого задания загрузки — но оно не переключает GlobalSetting.use_s3?, который является флагом, фактически контролирующим, переписываются ли URL ресурсов в CDN при рендеринге. Таким образом, вы можете успешно выгрузить файлы на R2 с USE_DB_S3_CONFIG и при этом видеть, что ваш сайт обслуживает ресурсы локально, потому что проверка при рендеринге страницы никогда не видит «S3 включен». Если вы хотите, чтобы ресурсы действительно обслуживались с R2, вам нужны настоящие DISCOURSE_USE_S3: true + переменные окружения для подключения в app.yml, а не просто обходной путь с конфигурацией БД.
Часть 4 — Что все равно не будет на R2 и почему
Даже при работающем хуке, s3:upload_assets загружает только то, что находится в Rails.application.assets.load_path — манифест Sprockets Rails. Три категории генерируются вне этого конвейера и никогда не появляются в этом списке, поэтому они остаются на локальном диске, что бы ни было настроено:
- CSS тем — компилируется динамически для каждой темы/схемы цветов менеджером
Stylesheet::ManagerDiscourse, а не через Sprockets. theme-javascripts— JS, скомпилированный для каждой темы изThemeJavascriptCompiler.extra-locale/ файлы локализации JS — генерируютсяJsLocaleHelper.
Это не проблема конфигурации — они изначально не были ресурсами Sprockets, поэтому не существует переменной окружения, которая могла бы их включить. На практике это означает: основной бандл JS Ember/vendor → успешно выгружен на R2; CSS/JS тем и локализации → остаются локально, обслуживаются непосредственно приложением. Это нормальное, рабочее состояние, а не сломанное.
Резюме: что и куда нужно положить
| Что | Где | Область действия |
|---|---|---|
enable_s3_uploads, s3_upload_bucket, backup_location, s3_backup_bucket |
Административный интерфейс, для каждого сайта | На уровне сайта |
Учетные данные + DISCOURSE_S3_REGION/ENDPOINT/BUCKET/CDN_URL + DISCOURSE_USE_S3 |
app.yml, только если вы хотите выгрузку ресурсов в CDN |
На весь кластер (неизбежно) |
Хук after_assets_precompile |
app.yml |
На весь кластер |
AWS_REQUEST_CHECKSUM_CALCULATION / AWS_RESPONSE_CHECKSUM_VALIDATION |
app.yml |
На весь кластер (безвредный флаг поведения SDK) |
| CORS для бакета | Панель управления Cloudflare | Должен включать домены всех сайтов, если ресурсы разделяются |
Если вам не нужна выгрузка ресурсов в CDN, полностью пропустите Часть 3 — вы можете запустить полностью рабочую, действительно настраиваемую на уровне сайта конфигурацию R2 (только загрузки + резервные копии), не трогая app.yml.