Michael,
既然我已经使用过它,我同意 restic 是备份的“最佳”选择,但我好奇的是,鉴于最近 minio 的争议,你是否尝试过使用像 garage 这样的兼容替代品?或者答案是“没关系,它们都使用相同的命令,所以你可以随意选择”?这些工具我目前才刚开始学习,所以我不清楚具体的差异,只知道目前的建议是使用 garage 而不是 minio。
Michael,
既然我已经使用过它,我同意 restic 是备份的“最佳”选择,但我好奇的是,鉴于最近 minio 的争议,你是否尝试过使用像 garage 这样的兼容替代品?或者答案是“没关系,它们都使用相同的命令,所以你可以随意选择”?这些工具我目前才刚开始学习,所以我不清楚具体的差异,只知道目前的建议是使用 garage 而不是 minio。
哦,对,我很确定今天我会从车库开始。只是我还没迁移过去。
还要感谢 @raykholo 私下指出我关于禁用透明大页(transparent huge pages)的说明存在错误。
正在关注此事的朋友,请改用以下方法:
echo 'w /sys/kernel/mm/transparent_hugepage/enabled - - - - never
w /sys/kernel/mm/transparent_hugepage/defrag - - - - never' > /etc/tmpfiles.d/thp.conf
systemd-tmpfiles --create
这些设置无法通过 sysctl 进行配置。如果你之前按照我的旧说明创建了 /etc/sysctl.d/10-huge-pages.conf,可以将其删除。
我在撰写时没有正确验证这一点,此后也一直未能发现它无法正常工作。感谢你的细心发现,谢谢!
嗯。我有两台系统,刚刚看了一下,我推测对于 Ubuntu 22 你确实是对的,你的修正起到了作用;但对于 Ubuntu 24 则不需要。在写这段话时,我意识到即使 Ubuntu 24 不需要,你的修正可能也有效。你使用的是哪个版本的操作系统?
这是默认配置的问题。如果它已经被禁用(无操作),禁用它是安全的,并且现在描述的方法得到了广泛支持。
正如开头所述,我运行的是 AlmaLinux。一旦 Docker 终于 支持 v2 命名空间,我就抓住第一个机会迁移到了 Red Hat 衍生操作系统。
也许是在回答我自己的问题,以下是尝试按照你的操作手册使用 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 规模下经过实战测试”。
它可行。已针对 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 中没有任何路径样式选项。需要两部分:garage.toml 中 [s3_api] 下的 root_domain,以及每个存储桶的可解析 DNS 名称(由于 Docker DNS 不支持通配符,因此每个存储桶需要一个 Docker 网络别名)。如果遗漏它,症状是在 SiteIconManager.ensure_optimized! 期间出现关于 getaddrinfo 的 Aws::Waiters 错误——看起来像网络故障,而非存储故障。每个新存储桶都需要一个新名称。
Garage 未实现 PutObjectAcl / GetObjectAcl,因此将 s3_use_acls 设置为 false——与 R2 相同。这里有两个陷阱。首先,带有 --acl public-read 的 PutObject 被静默接受,标头被忽略,因此基本上传测试通过,仅在安全上传转换时稍后才会出错。其次,DISCOURSE_S3_USE_ACLS 不是一个隐藏的全局变量——将其放在 app.yml 中没有任何作用。尽管它存在于环境变量中,我的首次启动时它仍显示为 true。它必须是一个站点设置,并且在每次重建后需要重新检查,因为没有任何警告提示你。
如果你在前面放置了 CDN,请将其指向 Garage 的 WEB 端点 (:3902),而不是 S3 API (:3900)。匿名读取仅在 Web 端点上有效;S3 API 正确地对未认证请求返回 403,因此指向 :3900 的 CDN 会在每张图片上失败,尽管你的凭证完全有效。你还需要在上传存储桶上运行
garage bucket website --allow,并在 [s3_web] 下设置 root_domain。仅在上传存储桶上启用它——备份存储桶永远不应被匿名读取。ListObjectVersions 在 Garage 上返回 NotImplemented。这无关紧要:在 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),但 real-IP 出口配置仍然至关重要,甚至更为重要。使用 real_ip_header CF-Connecting-IP 而不是 X-Forwarded-For——当隧道是唯一的入口路径时,它是一个单一明确的边缘集值。通过从已知公共 IP 发布并检查 nginx 日志来验证它;如果没有它,Discourse 会为每个请求记录连接器的 Docker 地址,并将整个互联网速率限制为一个客户端。与 external-nginx 方法相比,你失去的是重建期间的维护页面——cloudflared 无法提供它。
对广泛(错误)陈述的内容进行次要更正,包括我自己:DISCOURSE_S3_CDN_URL 并非不可逆地烘焙到存储的 URL 中。upload.url 确实存储带有内部主机的原始 S3 URL,但 Discourse 在渲染时通过 Discourse.store.cdn_url / UrlHelper.cook_url 替换 CDN 主机。后期设置是可恢复的——代价是运行
rake posts:rebake,因为 posts.cooked 缓存了烹饪时的 HTML。仍然应在首次上传之前设置它;但如果你没有设置,也不要惊慌。两个与 Garage 无关的操作注意事项。./launcher 会回显包含明文 DISCOURSE_S3_SECRET_ACCESS_KEY 的完整 docker run 行,因此引导日志包含秘密——值得在嘈杂的安装后轮换密钥。并且,单节点 Garage 在存储任何内容之前仍然需要
layout assign+layout apply,replication_factor = 1 意味着根本没有复制,因此你的备份作业是你唯一的持久性保障。
出于部分原因(迁移是单向的,而且我也不需要),我在 Discourse 中不使用对象存储来提供公共访问。就我而言,这只会从同一存储中提供数据,只是需要另一台虚拟机来管理。我仅在 Discourse 外部将对象存储用作 restic 备份的目标。
Discourse 备份包含缩略图但不包含图片,再加上使用 restic 在主机上对上传目录进行的主机级备份,这意味着据我所知,那些限制是无关紧要的。