MKJ의 의견 있는 Discourse 배포 구성

아마도 내 질문에 대한 답이 될 수 있도록, Garage를 runbook과 함께 사용하면서 작성한 메모를 공유합니다:


1. Garage는 파일에 "권한 태그"를 지원하지 않습니다 (이것이 Garage의 진정한 한계입니다)

Amazon는 각 파일을 개별적으로 “공개” 또는 "비공개"로 태그할 수 있습니다. Garage는 이를 구현하지 않았습니다.

Discourse는 기본적으로 이를 사용하려 합니다. 기막힌 점은, Discourse가 "이 파일을 저장하고 공개로 표시하세요"라고 했을 때, Garage가 요청을 수락하고 태그를 조용히 무시했다는 것입니다. 그래서 기본 업로드에서는 문제가 없어 보였고, 누군가가 업로드를 비공개로 전환한 첫 번째 순간에야 비로소 문제가 발생했을 것입니다.

해결 방법: Discourse가 태그 사용을 중단하도록 설정합니다 (설정 1개). Cloudflare의 R2에도 동일한 한계가 있으며 동일한 해결 방법이 적용됩니다. Garage는 자체 방식으로, 버킷 수준에서 권한을 처리하며, 이것이 우리가 필요한 전부입니다.


2. Discourse는 버킷을 특정 방식으로 이름 붙이는 것을 고집합니다 (Garage의 결함이 아닙니다)

Discourse는 "서버 garage의 버킷 uploads"라고 말하지 않습니다. uploads.garage라고 고집합니다 — 버킷 이름이 앞부분에 붙어 서브도메인처럼 보이는 형태입니다. 그리고 이를 끄는 옵션이 없습니다.

우리 네트워크의 아무것도 그 이름을 알지 못했기 때문에, Discourse는 아예 연결할 수 없었습니다 — 설치가 중간에 실패했습니다.

해결 방법: Garage에 해당 이름 지정 방식을 부여하고 이름을 등록했습니다. 설정 2줄이었습니다.

이것은 Garage가 아니라 Discourse의 알려진 특이점입니다 — 이것이 Oracle의 스토리지가 Discourse의 공식 “동작하지 않음” 목록에 있는 이유입니다.


3. 이미지는 공개 웹 주소가 필요했습니다 (Garage와는 무관)

Discourse는 각 이미지의 주소를 게시물에 영구적으로 기록합니다. 사전에 공개 주소를 알려주지 않으면, 우리 서버만 접근할 수 있는 내부 주소만 저장됩니다 — 그러면 방문자에게 모든 이미지가 영원히 깨진 상태로 보일 것이며, 모든 게시물을 다시 만들어야 합니다.

해결 방법: 첫 업로드 전에 CDN 주소를 설정했습니다. Amazon, R2, 무엇이든 동일했을 것입니다.


참고: 이 모든 문제는 무음으로 발생했습니다. "지원되지 않음"이라는 메시지는 없었습니다. 하나는 요청을 수락하고 무시했고, 하나는 네트워크 오류처럼 보였고, 하나는 정상적으로 작동하는 것처럼 보였습니다.

따라서, 처음 이 작업을 시도하는新用户나 Michael의 runbook을 Garage로 따르는 사람을 위해, 초기에 염두에 둬야 할 몇 가지 사항입니다.

포럼이 배포된 후 우리가 경험한 전체 AI 메모는 다음과 같습니다:


Cloudflare 터널 뒤에서 자체 호스팅 Garage (S3) 위에서 Discourse 실행 — 메모

Garage는 “S3 호환 오브젝트 스토리지 제공자 구성” 호환성 테이블에 없으므로, 여기 데이터 포인트를 하나 제공합니다. 설정: Garage v2.3.0, 컨테이너 2개 Discourse, Debian 13,
단일 노드, 외부 nginx 대신 Cloudflare 터널. 사전 주의: 이것은 아직 프로덕션 트래픽이 없는 작은 배포이므로, "동작함"으로 취급하되 "Maker Forums 규모에서 검증됨"으로 보지 마세요.

  1. 작동합니다. CLI가 아니라 Discourse의 자체 코드 경로(UploadCreator, OptimizedImage, ListObjectsV2, 바이트 단위 정확도가 검증된 HeadObject/GetObject, 멀티파트, 삭제, remove_upload, backups 버킷에 기록하는 BackupRestore::Backuper 및 이를 다시 읽는 BackupStore#files / #download_file)에 대해 검증했습니다. 라이프사이클 구성이 작동하므로 s3_configure_tombstone_policy가 조용히 무시되지 않고 실제로 효과를 냅니다. PutBucketCors가 작동하므로 수동 CORS도 문제없습니다.

  2. 가상 호스트 스타일 주소를 구성해야 하며, 그렇지 않으면 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 오류로, 스토리지 오류가 아니라 네트워크 고장처럼 보입니다. 모든 새 버킷에는 새 이름이 필요합니다.

  3. Garage는 PutObjectAcl / GetObjectAcl을 구현하지 않으므로 s3_use_acls를 false로 설정하세요 — R2와 동일합니다. 여기서 함정이 두 개 있습니다. 첫째, --acl public-read로 PutObject를 하면 조용히 수락되고 헤더가 무시되므로, 기본 업로드 테스트는 통과하지만 나중에 보안 업로드 전환 시에만 깨집니다. 둘째, DISCOURSE_S3_USE_ACLS는 섀도우 글로벌 변수가 아닙니다 — app.yml에 넣으면 아무 효과가 없습니다. 환경 변수에 있었음에도 첫 부팅 시 true로 나타났습니다. 사이트 설정이어야 하며, 아무도 경고하지 않으므로 모든 재빌드 후 재확인해야 합니다.

  4. CDN을 앞에 두면, S3 API(:3900)가 아니라 Garage의 WEB 엔드포인트(:3902)를 가리키세요. 익명 읽기는 웹 엔드포인트에서만 작동하며, S3 API는 인증되지 않은 요청을 올바르게 403으로 처리하므로, :3900을 가리키는 CDN은 자격 증명이 완벽히 유효함에도 모든 이미지에서 실패합니다. 또한 uploads 버킷에 garage bucket website --allow와 [s3_web] 아래 root_domain이 필요합니다. uploads에만 활성화하세요 — backups 버킷은 절대 익명으로 읽을 수 없어야 합니다.

  5. Garage에서 ListObjectVersions는 NotImplemented을 반환합니다. 중요하지 않습니다: s3_helper.rb와 file_store/s3_store.rb에서 list_object_versions / object_versions를 검색해도 0건입니다. Discourse는 S3 오브젝트 버전 관리를 사용하지 않으며, 톰스톤 만료는 라이프사이클 규칙과 접두사(prefix)입니다.

  6. aws-cli 또는 mc로 테스트하는 것은 호환성을 증명하지 않습니다. 둘 다 커스텀 엔드포인트에 대해 기본적으로 패스 스타일 주소를 사용합니다. Discourse는 가상 호스트 스타일만 사용합니다. 제 CLI 테스트는 15/16으로 녹색 신호처럼 보였지만, 실제 설치에서는 주소 지정 차이로 즉시 실패했습니다. 동일한 스토어, 동일한 자격 증명, 동일한 작업. 검증되지 않은 S3 백엔드를 평가한다면 실제 Discourse를 구동하세요 — 일회용 단일 컨테이너 인스턴스로 충분하며, 콘텐츠가 존재하기 전에 수행하는 것이 핵심입니다. S3 → 로컬은 일방통행이므로.

  7. Cloudflare 터널의 경우: SSL용 외부-nginx 섹션은 적용되지 않습니다 (TLS가 엣지에서 종료되고, certbot도, 수신 80/443도 없음), 하지만 real-IP 아웃렛 구성은 여전히 필수이며, 오히려 더 중요합니다. X Forwarded-For 대신 real_ip_header CF-Connecting-IP를 사용하세요 — 터널이 유일한 입구 경로인 경우, 이는 모호하지 않은 단일 엣트 설정 값입니다. 알려진 공개 IP에서 게시물을 작성하고 nginx가 기록한 것을 확인하여 검증하세요. 이것이 없으면 Discourse는 모든 요청에 대해 커넥터의 Docker 주소를 기록하고, 인터넷 전체를 하나의 클라이언트로 간주하여 속도 제한을 걸 것입니다. 외부-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은 복제가 전혀 없다는 뜻이므로, 백업 작업이 유일한 내구성입니다.