# MKJ 的 Discourse 部署配置

**URL:** <https://meta.discourse.org/t/mkjs-opinionated-discourse-deployment-configuration/193355>\
**Category:** Sysadmins\
**Tags:** explanation, install\
**Created:** [2021年六月9日 23:34 UTC](https://meta.discourse.org/t/mkjs-opinionated-discourse-deployment-configuration/193355 "2021-06-09T23:34:00Z")\
**Posts on this page:** 1\
**Showing post:** 46

<div class="post-metadata">

**Author:** ![raykholo](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/raykholo/32/129848_2.png) [@raykholo](https://meta.discourse.org/u/raykholo)\
**Post date:** [2026年八月7日 15:35 UTC](https://meta.discourse.org/t/mkjs-opinionated-discourse-deployment-configuration/193355/46 "2026-08-07T15:35:33Z")

</div>

也许是在回答我自己的问题，以下是尝试按照你的操作手册使用 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. 图片需要一个公开的 Web 地址** （_与 Garage 无关_）

Discourse 会将每张图片的地址永久写入帖子中。如果不提前告知它公开地址，它只会保存只有我们服务器才能访问的内部地址——因此，除非我们重建所有帖子，否则访客看到的每张图片都会永远损坏。

**解决方法：** 在首次上传之前设置 CDN 地址。在 Amazon、R2 或任何其他服务上都会是一样的。

* * *

注意：上述每一个问题都是静默发生的。没有任何提示说“不支持”。一个接受了请求并忽略了它，一个看起来像网络错误，另一个看起来运行良好。

因此，这只是给初次尝试的新用户，和/或试图跟随 Michael 的操作手册使用 Garage 的人的一些初步注意事项。

以下是论坛部署完成后，我们经历的完整 AI 笔记：

* * *

> 在 Cloudflare 隧道后的自托管 Garage (S3) 上运行 Discourse — 笔记
> 
> Garage 不在“配置 S3 兼容对象存储提供商”的兼容性表中，所以这里提供一个数据点。设置：Garage v2.3.0，双容器 Discourse，Debian 13，  
> 单节点，使用 Cloudflare 隧道代替外部 nginx。预先说明：这是一个小型部署，尚未有生产流量，因此请将其视为“可行”，而非“在 Maker Forums 规模下经过实战测试”。
> 
> 1. 它可行。已针对 Discourse 自身的代码路径（而非 CLI）进行验证：UploadCreator、OptimizedImage、ListObjectsV2、HeadObject/GetObject（字节精确往返）、分块上传、删除、remove\_upload，以及 BackupRestore::Backuper 写入备份存储桶和 BackupStore#files / #download\_file 从中读取。生命周期配置有效，因此 s3\_configure\_tombstone\_policy 实际生效，而非被静默忽略。PutBucketCors 有效，因此手动配置 CORS 没问题。
> 
> 2. 你必须配置虚拟主机样式寻址，否则 db:migrate 会失败。Discourse 将端点 [http://garage:3900](http://garage:3900) + 存储桶“uploads”转换为主机名 uploads.garage:3900，而在 s3\_helper.rb 或 site\_settings.yml 中没有任何路径样式选项。需要两部分：garage.toml 中 [s3\_api] 下的 root\_domain，以及每个存储桶的可解析 DNS 名称（由于 Docker DNS 不支持通配符，因此每个存储桶需要一个 Docker 网络别名）。如果遗漏它，症状是在 SiteIconManager.ensure\_optimized! 期间出现关于 getaddrinfo 的 Aws::Waiters 错误——看起来像网络故障，而非存储故障。每个新存储桶都需要一个新名称。
> 
> 3. Garage 未实现 PutObjectAcl / GetObjectAcl，因此将 s3\_use\_acls 设置为 false——与 R2 相同。这里有两个陷阱。首先，带有 --acl public-read 的 PutObject 被静默接受，标头被忽略，因此基本上传测试通过，仅在安全上传转换时稍后才会出错。其次，DISCOURSE\_S3\_USE\_ACLS 不是一个隐藏的全局变量——将其放在 app.yml 中没有任何作用。尽管它存在于环境变量中，我的首次启动时它仍显示为 true。它必须是一个站点设置，并且在每次重建后需要重新检查，因为没有任何警告提示你。
> 
> 4. 如果你在前面放置了 CDN，请将其指向 Garage 的 WEB 端点 (:3902)，而不是 S3 API (:3900)。匿名读取仅在 Web 端点上有效；S3 API 正确地对未认证请求返回 403，因此指向 :3900 的 CDN 会在每张图片上失败，尽管你的凭证完全有效。你还需要在上传存储桶上运行 `garage bucket website --allow`，并在 [s3\_web] 下设置 root\_domain。仅在上传存储桶上启用它——备份存储桶永远不应被匿名读取。
> 
> 5. ListObjectVersions 在 Garage 上返回 NotImplemented。这无关紧要：在 s3\_helper.rb 和 file\_store/s3\_store.rb 中搜索 list\_object\_versions / object\_versions 返回零个匹配项。Discourse 不使用 S3 对象版本控制；墓碑过期是通过生命周期规则加前缀实现的。
> 
> 6. 使用 aws-cli 或 mc 测试并不能证明兼容性。两者都默认对自定义端点使用路径样式寻址；Discourse 仅使用虚拟主机样式。我的 CLI 测试结果为 15/16，看起来像是绿灯，但实际安装因寻址差异立即失败。相同的存储，相同的凭证，相同的操作。如果你正在评估一个未经证实的 S3 后端，请运行真实的 Discourse——一个一次性单容器实例就足够了，而在任何内容存在之前这样做正是其目的所在，因为 S3 → 本地是单向的。
> 
> 7. 具体针对 Cloudflare 隧道：external-nginx-for-SSL 部分不适用（TLS 在边缘终止，无 certbot，无入站 80/443），但 real-IP 出口配置仍然至关重要，甚至更为重要。使用 real\_ip\_header CF-Connecting-IP 而不是 X-Forwarded-For——当隧道是唯一的入口路径时，它是一个单一明确的边缘集值。通过从已知公共 IP 发布并检查 nginx 日志来验证它；如果没有它，Discourse 会为每个请求记录连接器的 Docker 地址，并将整个互联网速率限制为一个客户端。与 external-nginx 方法相比，你失去的是重建期间的维护页面——cloudflared 无法提供它。
> 
> 8. 对广泛（错误）陈述的内容进行次要更正，包括我自己：DISCOURSE\_S3\_CDN\_URL 并非不可逆地烘焙到存储的 URL 中。upload.url 确实存储带有内部主机的原始 S3 URL，但 Discourse 在渲染时通过 Discourse.store.cdn\_url / UrlHelper.cook\_url 替换 CDN 主机。后期设置是可恢复的——代价是运行 `rake posts:rebake`，因为 posts.cooked 缓存了烹饪时的 HTML。仍然应在首次上传之前设置它；但如果你没有设置，也不要惊慌。
> 
> 9. 两个与 Garage 无关的操作注意事项。./launcher 会回显包含明文 DISCOURSE\_S3\_SECRET\_ACCESS\_KEY 的完整 docker run 行，因此引导日志包含秘密——值得在嘈杂的安装后轮换密钥。并且，单节点 Garage 在存储任何内容之前仍然需要 `layout assign` + `layout apply`，replication\_factor = 1 意味着根本没有复制，因此你的备份作业是你唯一的持久性保障。

---

_[View the full topic](https://meta.discourse.org/t/mkjs-opinionated-discourse-deployment-configuration/193355)._
