Возможно, отвечая на свой же вопрос, вот несколько заметок из опыта использования Garage с вашим руководством:
1. Garage не поддерживает “теги разрешений” для файлов (это реальное ограничение Garage)
Amazon позволяет индивидуально помечать каждый файл как “публичный” или “частный”. Garage это не реализует.
Discourse по умолчанию пытается использовать эту функцию. Хитрость заключается в следующем: когда Discourse говорил “сохраните этот файл, пометьте его как публичный”, Garage принимал запрос и тихо игнорировал тег. Поэтому базовые загрузки выглядели нормально, и проблемы возникли бы гораздо позже, в первый раз, когда кто-то сделал загрузку частной.
Решение: указать Discourse прекратить использование тегов (одна настройка). У Cloudflare R2 есть такое же ограничение, и решение такое же. Garage обрабатывает разрешения по-своему, на уровне бакетов, чего нам достаточно.
2. Discourse настаивает на определенном формате именования бакетов (не является ошибкой Garage)
Discourse не скажет “сервер garage, бакет uploads”. Он настаивает на uploads.garage — имя бакета приклеено спереди, как поддомен. И нет возможности это отключить.
Ничто в нашей сети не знало это имя, поэтому Discourse вообще не мог подключиться — установка прервалась на полпути.
Решение: дал Garage такой стиль именования и зарегистрировал имена. Две строки конфигурации.
Это известная особенность Discourse, а не Garage — именно поэтому хранилище Oracle находится в официальном списке Discourse “не работает”.
3. Изображениям нужен публичный веб-адрес (не имеет отношения к Garage)
Discourse навсегда записывает адрес каждого изображения в пост. Если не указать ему публичный адрес заранее, он сохранил внутренние адреса, доступные только нашему серверу, — поэтому каждое изображение было бы сломано для посетителей навсегда, если бы мы не пересобрали каждый пост.
Решение: установить адрес CDN перед первой загрузкой. Это было бы идентично на Amazon, R2 или любом другом.
Примечание: каждый из этих случаев был тихим. Ничего не сообщало “не поддерживается”. Один принял запрос и проигнорировал его, один выглядел как сетевая ошибка, третий выглядел так, будто все работает нормально.
Так что это просто несколько первоначальных моментов, которые стоит иметь в виду новому пользователю, пробующему это впервые, и/или тому, кто пытается следовать руководству Майкла с использованием Garage.
Вот полные заметки ИИ о нашем опыте теперь, когда форум развернут:
Запуск Discourse на самохостинге Garage (S3) за туннелем Cloudflare — заметки
Garage нет в таблице совместимости “Настройка совместимого с S3 поставщика объектного хранилища”, поэтому вот точка данных. Настройка: Garage v2.3.0, двухконтейнерный Discourse, Debian 13,
один узел, туннель Cloudflare вместо внешнего nginx. Предупреждение заранее: это одно
небольшое развертывание без производственного трафика, поэтому относитесь к этому как к “работает”, а не как к “проверено в бою на масштабе Maker Forums”.
Это работает. Проверено против собственных путей кода Discourse, а не CLI: UploadCreator,
OptimizedImage, ListObjectsV2, HeadObject/GetObject с побайтово точным круговым циклом,
многочастные загрузки, удаление, remove_upload, плюс BackupRestore::Backuper, записывающий в бакет
резервных копий, и BackupStore#files / #download_file, читающий оттуда. Настройка жизненного цикла
работает, поэтому s3_configure_tombstone_policy действительно вступает в силу, а не
тихо игнорируется. PutBucketCors работает, поэтому ручная настройка CORS в порядке.Вы должны настроить адресацию в стиле виртуального хоста, и это сломает db:migrate, если вы
этого не сделаете. Discourse превращает конечную точку http://garage:3900 + бакет “uploads” в хост
uploads.garage:3900, и нигде в s3_helper.rb или
site_settings.yml нет опции стиля пути. Требуются две части: root_domain в [s3_api] в garage.toml,
И разрешаемое имя DNS для каждого бакета (псевдоним сети Docker для каждого бакета, так как в DNS Docker нет подстановочных знаков). Симптом, если вы это упустите, — ошибка Aws::Waiters о
getaddrinfo во время SiteIconManager.ensure_optimized! — выглядит как сетевая неисправность, а не
хранилищная. Каждый новый бакет нуждается в новом имени.Garage не реализует PutObjectAcl / GetObjectAcl, поэтому установите s3_use_acls в false —
то же самое, что и для R2. Здесь две ловушки. Первая: PutObject с --acl public-read тихо
ПРИНИМАЕТСЯ, и заголовок игнорируется, поэтому базовые тесты загрузки проходят, и это ломается только позже при
переходе на безопасную загрузку. Вторая: DISCOURSE_S3_USE_ACLS — это не скрытая глобальная переменная —
помещение её в app.yml ничего не делает. Она стала true при моей первой загрузке, несмотря на то, что была в
окружении. Это должно быть настройкой сайта, и её нужно перепроверять после каждой сборки,
потому что ничто вас не предупреждает.Если вы ставите CDN спереди, направьте его на WEB-конечную точку Garage (:3902), а не на S3 API
(:3900). Анонимные чтения работают только на веб-конечной точке; S3 API правильно возвращает 403
для неаутентифицированных запросов, поэтому CDN, направленный на :3900, будет терпеть неудачу для каждого изображения, пока ваши
учетные данные абсолютно валидны. Вам также нужно выполнитьgarage bucket website --allowдля
бакета uploads и root_domain в [s3_web]. Включите это только для uploads — бакет
резервных копий никогда не должен быть анонимно читаемым.ListObjectVersions возвращает NotImplemented в Garage. Это не имеет значения: grep
s3_helper.rb и file_store/s3_store.rb по list_object_versions / object_versions
возвращает нулевые совпадения. Discourse не использует версионирование объектов S3; истечение срока действия надгробий — это
правила жизненного цикла плюс префикс.Тестирование с aws-cli или mc НЕ доказывает совместимость. Оба по умолчанию используют адресацию в стиле пути
против пользовательской конечной точки; Discourse использует только стиль виртуального хоста. Мой CLI
тест вернул 15/16 и выглядел как зеленый свет, затем реальная установка
сразу же потерпела неудачу из-за разницы в адресации. То же хранилище, те же учетные данные, те же
операции. Если вы оцениваете непроверенный S3-бэкенд, запускайте реальный Discourse — достаточно
одноразового одноконтейнерного экземпляра, и делать это до создания какого-либо контента — вот в чем суть, поскольку переход от S3 к локальному — это дорога в один конец.В частности, для туннеля Cloudflare: раздел external-nginx-for-SSL не применяется
(TLS завершается на краю, нет certbot, нет входящих 80/443), но конфигурация выхода реального IP
по-прежнему необходима и, возможно, даже более важна. Используйте real_ip_header CF-Connecting-IP
вместо X-Forwarded-For — когда туннель является единственным путем входа, это одно
недвусмысленное значение набора краевых узлов. Проверьте это, опубликовав из известного публичного IP и проверив,
что записал nginx; без этого Discourse записывает адрес коннектора Docker для
каждого запроса и ограничивает частоту всего интернета как одного клиента. То, что вы теряете по сравнению
с подходом external-nginx, — это страница обслуживания во время сборок — cloudflared
не может её обслуживать.Небольшое исправление чего-то широко (неправильно) заявленного, в том числе мной: DISCOURSE_S3_CDN_URL
не вписывается необратимо в сохраненные URL. upload.url действительно сохраняет сырой URL S3 с
внутренним хостом, но Discourse подменяет хост CDN во время рендеринга через
Discourse.store.cdn_url / UrlHelper.cook_url. Поздняя настройка исправима —
ценаrake posts:rebake, потому что posts.cooked кэширует HTML от времени приготовления. Все равно
установите это перед первой загрузкой; просто не паникуйте, если вы этого не сделали.Две операционные заметки, не связанные с Garage. ./launcher выводит полную строку docker run,
включая DISCOURSE_S3_SECRET_ACCESS_KEY в открытом виде, поэтому логи инициализации содержат
секреты — стоит ротировать ключи после шумной установки. И одиночный узел Garage
по-прежнему требуетlayout assign+layout apply, прежде чем он начнет что-либо хранить, с
replication_factor = 1, что означает полное отсутствие репликации, поэтому ваша задача резервного копирования — это единственная
долговечность, у вас есть.