Kết nối Cloudflare R2 với Discourse để lưu trữ hình ảnh & video

Khi xây dựng một diễn đàn, một trong những vấn đề sớm muộn chúng ta sẽ gặp phải là dung lượng lưu trữ.

Thành viên đăng ảnh, ảnh đại diện, file đính kèm, video… càng nhiều thì dung lượng trên VPS càng tăng.

Nếu toàn bộ dữ liệu đều lưu trực tiếp trên VPS, dung lượng ổ đĩa sẽ nhanh chóng trở thành vấn đề.

Vì vậy, trong quá trình xây dựng Sorae.vn, mình lựa chọn một giải pháp khác:

Discourse + Cloudflare R2

Cloudflare R2 là dịch vụ Object Storage tương thích với giao thức S3. Discourse có thể kết nối tới R2 thông qua S3 API để lưu trữ các file upload.

Mô hình hoạt động sẽ như sau:


Thành viên
    ↓
Sorae.vn / Discourse
    ↓
Cloudflare R2
    ↓
Hình ảnh / Video / File

VPS lúc này chủ yếu chạy Discourse, còn kho file được đưa sang R2.


1. Chuẩn bị Cloudflare

Trước tiên cần có một tài khoản Cloudflare. và bạn cần thêm Add Domain tên miền của bạn vào Cloudfare nhé.

Nếu tên miền của bạn đã được quản lý bởi Cloudflare thì việc kết nối R2 với tên miền riêng sẽ thuận tiện hơn.

Trong ví dụ này, mình giả sử Sorae đã sử dụng Cloudflare và muốn tạo một kho riêng cho các file:

Bucket: sorae-uploads

Mình khuyên nên đặt bucket riêng cho uploads của Discourse, thay vì trộn tất cả dữ liệu vào một bucket.


2. Tạo R2 Bucket

Đăng nhập Cloudflare Dashboard.

Vào:

Storage & databases → R2 Object Storage

Sau đó chọn:

Create bucket

Đặt tên:

tên miền của bạn-uploads

Bucket chính là nơi Cloudflare R2 lưu các object.

Có thể hiểu đơn giản:

Bucket = ổ cứng trên Cloudflare dành cho Sorae.

Cloudflare hiện cho phép lựa chọn location và storage class khi tạo bucket.


3. Tạo API Token cho Discourse

Discourse cần quyền truy cập R2 để có thể upload file.

Trong ô Tìm kiếm, bạn gõ API để tìm cho nhanh. Chọn API Tokens

Create Account API token

Khi tạo token, cấp quyền:

Object Read & Write

Và nếu chỉ sử dụng cho tên miền của bạn, nên giới hạn token vào bucket:

tên miền của bạn-uploads

Cloudflare khuyến nghị API token có quyền Admin Read & Write và có thể giới hạn quyền vào bucket cụ thể.

Sau khi tạo, Cloudflare cung cấp:

Access Key ID
Secret Access Key

Secret Access Key chỉ hiển thị một lần, vì vậy phải lưu lại ngay.

:warning: Không đăng Access Key hoặc Secret Access Key lên bài viết, GitHub, ảnh chụp màn hình hay gửi cho người khác.


4. Lấy S3 Endpoint của Cloudflare R2

=> Chính là phần S3 API dưới góc phải của ảnh.

R2 cung cấp S3-compatible API tại dạng:

https://<ACCOUNT_ID>.r2.cloudflarestorage.com

Trong đó <ACCOUNT_ID> là Account ID Cloudflare của bạn.

Ví dụ:

https://1234567890abcdef.r2.cloudflarestorage.com

Không thêm tên bucket vào cuối endpoint khi nhập vào Discourse.

Tức là dùng:

https://1234567890abcdef.r2.cloudflarestorage.com

chứ không phải:

https://1234567890abcdef.r2.cloudflarestorage.com/sorae-uploads

Đây là một điểm dễ nhầm khi cấu hình.


5. Tạo Custom Domain cho R2

Đây là bước rất quan trọng.

R2 có thể lưu file nhưng chúng ta vẫn cần một địa chỉ để người dùng truy cập file.

Ví dụ mình muốn hình ảnh của Sorae được phân phối qua:

https://files.sorae.vn

Trong Cloudflare:

R2 → sorae-uploads → Settings → Custom Domains

Chọn:

Add / Connect Custom Domain

Nhập:

files.sorae.vn

Cloudflare sẽ tạo DNS record cần thiết và kết nối domain với bucket.

Sau khi hoàn tất, có thể sử dụng:

https://files.sorae.vn

để phân phối nội dung từ bucket.


6. Vì sao nên dùng Custom Domain?

Cloudflare có cung cấp URL dạng:

*.r2.dev

nhưng Cloudflare xác định đây là URL dành cho mục đích phát triển và bị giới hạn.

Đối với website thật, Cloudflare khuyến nghị sử dụng Custom Domain.

Custom Domain còn cho phép sử dụng các tính năng của Cloudflare như cache, WAF và các tính năng bảo mật khác.

Với Sorae, mình sẽ dùng:

files.sorae.vn

thay vì một địa chỉ r2.dev.


7. Cấu hình CORS cho R2

Nếu sử dụng Direct S3 Uploads, trình duyệt của thành viên có thể upload trực tiếp lên R2.

Khi đó cần cấu hình CORS.

Trong bucket:

R2 → sorae-uploads → Settings → CORS Policy

Có thể sử dụng cấu hình tương tự:

[
  {
    "AllowedOrigins": [
      "https://sorae.vn"
    ],
    "AllowedMethods": [
      "GET",
      "PUT",
      "POST",
      "DELETE",
      "HEAD"
    ],
    "AllowedHeaders": [
      "*"
    ],
    "ExposeHeaders": [
      "ETag"
    ],
    "MaxAgeSeconds": 3000
  }
]

Nếu Discourse của bạn chạy ở:

https://forum.sorae.vn

thì thay:

https://sorae.vn

bằng:

https://forum.sorae.vn

Discourse hiện hướng dẫn cấu hình CORS theo cách này cho trường hợp R2 + Direct S3 Uploads.


8. Cấu hình Discourse

Bây giờ đến phần kết nối Discourse với R2.

Trong Discourse:

Admin → Settings

Tìm:

S3

Các thiết lập quan trọng gồm:

Enable S3 uploads
Enable direct S3 uploads
S3 access key ID
S3 secret access key
S3 region
S3 upload bucket
S3 endpoint
S3 CDN URL
S3 use ACLs

Cấu hình ví dụ:

Discourse Giá trị
Enable S3 uploads true
Enable direct S3 uploads true
S3 Access Key ID Access Key của R2
S3 Secret Access Key Secret Key của R2
S3 Region auto
S3 Upload Bucket sorae-uploads
S3 Endpoint https://<ACCOUNT_ID>.r2.cloudflarestorage.com
S3 CDN URL https://files.sorae.vn
S3 Use ACLs false

Cloudflare R2 sử dụng region auto cho S3 API.

Đặc biệt:

S3 Use ACLs phải tắt

Đặt:

S3 Use ACLs = false

R2 sử dụng cơ chế quyền ở cấp bucket thay vì ACL kiểu S3 truyền thống. Discourse cũng lưu ý phải tắt setting này khi dùng R2.


9. Có cần sửa app.yml không?

Không nhất thiết.

Discourse cho phép cấu hình các thiết lập S3 trực tiếp trong:

Admin → All site settings

Bạn cần vào Admin - Tập Tin → rồi điền thông tin như ảnh mình hướng dẫn ở trên.

Nếu muốn quản lý cấu hình bằng app.yml, có thể sử dụng các biến:

DISCOURSE_ENABLE_S3_UPLOADS: true
DISCOURSE_S3_REGION: auto
DISCOURSE_S3_ENDPOINT: https://<ACCOUNT_ID>.r2.cloudflarestorage.com
DISCOURSE_S3_ACCESS_KEY_ID: "YOUR_ACCESS_KEY"
DISCOURSE_S3_SECRET_ACCESS_KEY: "YOUR_SECRET_KEY"
DISCOURSE_S3_UPLOAD_BUCKET: sorae-uploads
DISCOURSE_S3_CDN_URL: https://files.sorae.vn
DISCOURSE_ENABLE_DIRECT_S3_UPLOADS: true
DISCOURSE_S3_USE_ACLS: false

Đây là các biến mà hướng dẫn Discourse hiện tại sử dụng cho Cloudflare R2.


10. Một lưu ý quan trọng về CDN

Nếu Sorae đã chạy qua Cloudflare, không cần cấu hình thêm DISCOURSE_CDN_URL chỉ để phục vụ website chính.

Với R2, chúng ta chỉ cần:

DISCOURSE_S3_CDN_URL

trỏ tới:

https://files.sorae.vn

Discourse hiện cũng lưu ý không nên cấu hình DISCOURSE_CDN_URL riêng cho R2 trong trường hợp này.


11. Kiểm tra upload

Sau khi cấu hình xong, thử tạo một Topic mới trên Sorae.

Upload một hình ảnh.

Ví dụ:

Ảnh từ máy tính
        ↓
Discourse
        ↓
Cloudflare R2
        ↓
sorae-uploads
        ↓
files.sorae.vn

Sau khi upload, mở ảnh và kiểm tra URL.

Nếu cấu hình đúng, URL hình ảnh sẽ sử dụng domain R2 của chúng ta, ví dụ:

https://files.sorae.vn/...

thay vì phải lấy file trực tiếp từ VPS.


12. Video cũng có thể lưu trên R2

R2 không chỉ dùng cho hình ảnh.

Có thể lưu:

  • JPG
  • PNG
  • WebP
  • GIF
  • MP4
  • PDF
  • ZIP
  • Và nhiều loại file khác.

R2 là object storage nên về bản chất nó lưu các object/file chứ không giới hạn riêng cho hình ảnh.

Tuy nhiên, với một diễn đàn cộng đồng, mình vẫn khuyên không nên biến R2 thành nơi lưu trữ video dung lượng cực lớn ngay từ đầu.

Video có thể nhanh chóng làm dung lượng tăng rất mạnh.


13. Một điều cần hiểu rõ: R2 không thay thế VPS

Sau khi cấu hình xong, Sorae vẫn cần VPS để chạy:

Discourse
PostgreSQL
Redis
Web application

R2 chỉ đảm nhiệm:

Hình ảnh
Video
File upload

Mô hình lúc này sẽ là:

                  SORAE.VN
                     │
                     ▼
                  Discourse
                     │
          ┌──────────┴──────────┐
          ▼                     ▼
        VPS                  Cloudflare R2
          │                     │
   Database / App          Images / Files
                                │
                                ▼
                         files.sorae.vn

Đây là mô hình mình thấy rất phù hợp với một cộng đồng muốn phát triển lâu dài.


14. Một lưu ý về file riêng tư

Có một điểm cần đặc biệt chú ý:

Không nên mặc định coi R2 public bucket là nơi lưu file riêng tư.

R2 mặc định là private. Khi kết nối Custom Domain và bật public access, nội dung trong bucket có thể được truy cập công khai qua domain đó.

Với bucket dành cho ảnh upload công khai của một diễn đàn như Sorae, điều này thường phù hợp.

Nhưng nếu có:

  • tài liệu nội bộ;
  • file cá nhân;
  • dữ liệu nhạy cảm;
  • file chỉ dành cho một nhóm thành viên;

thì cần thiết kế cơ chế bảo vệ khác.

Discourse cũng lưu ý rằng secure uploads hiện chỉ được hỗ trợ đầy đủ với AWS S3, vì vậy không nên coi R2 là giải pháp tương đương cho mọi trường hợp file riêng tư.


15. Kết quả sau khi hoàn thành

Sau khi kết nối thành công, Sorae sẽ có một kiến trúc khá đẹp:

🌐 Sorae.vn
       │
       ▼
   Discourse
       │
       ├── 📝 Nội dung
       ├── 👤 Thành viên
       ├── 💬 Thảo luận
       │
       └── 📷 Upload
                │
                ▼
        ☁️ Cloudflare R2
                │
                ▼
        files.sorae.vn

Thay vì để tất cả hình ảnh và file nằm trong ổ cứng VPS, chúng được đưa sang Object Storage.

Điều này giúp việc mở rộng hệ thống về sau dễ dàng hơn.


Một số lỗi người mới dễ gặp

:cross_mark: Sai endpoint

Không nhập:

https://<ACCOUNT_ID>.r2.cloudflarestorage.com/sorae-uploads

Mà nhập:

https://<ACCOUNT_ID>.r2.cloudflarestorage.com

:cross_mark: Bật S3 Use ACLs

Với R2:

S3 Use ACLs = false

:cross_mark: Chưa cấu hình Custom Domain

Nếu muốn ảnh được phân phối qua:

files.sorae.vn

phải kết nối Custom Domain với bucket.

:cross_mark: API Token chỉ có quyền Read

Discourse cần upload nên token phải có:

Object Read & Write.

:cross_mark: CORS sai domain

Nếu Discourse chạy ở:

https://sorae.vn

thì CORS phải cho phép origin đó.


Kết luận

Khi bắt đầu một diễn đàn, có thể bạn chưa cần Cloudflare R2 ngay.

Nhưng khi lượng nội dung tăng lên, đặc biệt là hình ảnh và video, việc tách application serverobject storage sẽ trở nên rất hữu ích.

Với Sorae, mình lựa chọn:

Vultr → chạy Discourse

Cloudflare R2 → lưu trữ file

Cloudflare Custom Domain/CDN → phân phối file

Đây cũng là một trong những bước mình thấy thú vị khi xây Sorae: từ một diễn đàn chạy trên một VPS nhỏ, từng bước tách các thành phần ra để hệ thống có thể mở rộng khi cộng đồng phát triển.