MKJ의 의견 있는 Discourse 배포 구성

마이클,

사용해 본 결과, restic이 백업에 "최고"라는 데 동의합니다. 하지만 최근 minio 관련 논란을 고려할 때, minio 대신 garage와 같은 호환 가능한 옵션을 사용해 보셨는지 궁금합니다. 아니면 "어차피 명령어는 모두 같아서 원하는 도구를 사용하면 된다"는 것이 정답일까요? 저는 이제 막 이 도구들을 배우기 시작해서 자세한 차이는 알지 못하고, 현재 권장 사항이 minio 대신 garage를 사용하라는 것 정도만 알고 있습니다.

1개의 좋아요

오, 네, 오늘 바로 지하실에서 시작할 수 있을 거라고 확신해요. 아직 이전만 안 했을 뿐이에요.

1개의 좋아요

또한, 투명 거대 페이지(transparent huge pages)를 비활성화하는 제 지시사항이 조용히 잘못되었음을 밴드 아웃(out of band)으로 지적해 준 @raykholo에게도 감사드립니다.

이 문제를 추적하고 계신 분은 대신 다음을 사용해 주세요:

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를 생성한 경우 삭제할 수 있습니다.

작성 당시 이를 올바르게 검증하지 못했고, 이후에도 제대로 작동하지 않고 있다는 사실을 알아차리지 못했습니다. 좋은 지적 감사합니다!

3개의 좋아요

음. 저는 두 개의 시스템을 가지고 있으며, 방금 그들을 확인해 보았습니다. Ubuntu 22의 경우, 말씀하신 대로 수정이 도움이 되었을 가능성이 있지만, Ubuntu 24의 경우 그 수정이 필요하지 않을 수 있다고 추측해 봅니다. 이 글을 쓰면서, Ubuntu 24에서는 필요하지 않더라도 그 수정이 유효할 수도 있다는 사실을 깨달았습니다. 여러분은 어떤 버전의 OS를 사용 중이신가요?

기본 설정의 문제입니다. 이미 비활성화된 상태라면(무작동, no-op) 비활성화해도 안전하며, 현재 설명된 방법은 널리 지원됩니다.

상단에 설명된 대로 AlmaLinux를 사용 중입니다. Docker가 마침내 v2 네임스페이스를 지원하게 되자, Red Hat 파생 OS로 전환할 첫 기회를 잡았습니다.

1개의 좋아요

아마도 내 질문에 대한 답이 될 수 있도록, 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은 복제가 전혀 없다는 뜻이므로, 백업 작업이 유일한 내구성입니다.

저는 Discourse에서 공개 액세스를 위해 객체 스토어를 사용하지 않습니다. 이는 마이그레이션이 되돌릴 수 없는 과정(one-way door)이기 때문이기도 하지만, 실제로 필요하지 않기 때문이기도 합니다. 제 경우라면, 해당 데이터를 동일한 저장소에서 제공하되, 관리할 VM만 하나 더 추가하면 됩니다. 저는 객체 스토어를 Discourse 외부에서 실행되는 restic 백업 대상으로서만 사용합니다.

썸네일은 포함하되 이미지 없이 생성된 Discourse 백업에 업로드 파일의 호스트 레벨 백업을 결합하고, 호스트에서 restic으로 백업하는 방식은, 제가 아는 한 이러한 제한 사항이 사실상 무의미하게 만듭니다.

1개의 좋아요