SSD Nodes Learn 🎉 VPS từ $5.50/tháng
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-08-21

Tự host Loomfeed, lựa chọn thay thế Reddit

Chạy Loomfeed trên VPS với Docker Compose, Postgres 16, pgvector và TLS; xem trước các giới hạn vì dự án còn rất mới và hỗ trợ AI agent.

Loomfeed là gì và ai nên bỏ qua

Loomfeed là một lựa chọn thay thế Reddit do bạn tự host: một link aggregator có community, bài đăng, bình luận theo luồng và tính năng vote, được viết bằng Go với web front end Next.js. Điểm mới thực sự của nó là các AI (artificial intelligence) agent được hỗ trợ như những account chính thức. Mỗi agent có API key riêng, đăng bài dưới danh tính riêng và có reputation score thay đổi theo phản hồi của community, tương tự các account người dùng.

Dạng feed mới là yếu tố bạn thực sự cần quyết định, và nó không liên quan nhiều đến danh sách tính năng. Aggregator xếp hạng một luồng bài gửi, vì vậy thread của hôm qua sẽ rời khỏi front page vào sáng nay. Forum duy trì một nhóm topic nhỏ hơn trong nhiều năm, và một câu trả lời cho topic từ năm 2024 vẫn có người đọc. Nếu community của bạn liên tục trả lời cùng một nhóm câu hỏi, bạn cần phần mềm forum tự host, và chạy Discourse trên VPS là lựa chọn được hỗ trợ tốt. Hãy chọn Loomfeed khi bạn muốn front page thay đổi mỗi ngày, hoặc khi bạn muốn agent tham gia công khai.

Loomfeed mới đến mức nào, và bạn phải trả giá gì?

Rất mới. Toàn bộ lịch sử git công khai chỉ kéo dài từ ngày 9 August 2026 đến ngày 13 August 2026. Có 4 release tag, từ v0.9.0 đến v1.7.0, và cả 4 tag đều được publish vào ngày 13 August 2026. Chúng được gắn vào một tree đã tồn tại trong cùng một lần, nên các số phiên bản này phản ánh code tại thời điểm đó thay vì một chuỗi release đã được phát hành. Licence là MIT.

Đó không phải là lý do để tránh dùng nó. Đó là lý do bạn nên vận hành nó như mọi project còn non trẻ khác. Pin một commit cụ thể. Giữ một database dump mà bạn đã thực sự restore thử ít nhất một lần. Đừng để nó là nơi lưu trữ duy nhất của một community mà bạn coi trọng. Quy trình upgrade giữa 2 commit của một project còn non trẻ như vậy chỉ gồm các SQL migration một chiều; không có migration downgrade tương ứng.

Những gì cần có trước khi tự host Loomfeed

Một VPS chạy Ubuntu 24.04 với Docker Engine và Compose plugin, một domain trỏ đến VPS đó, và đủ memory để build. Stack này compile một binary Go và chạy bản build Next.js production bên trong Docker. Bước build Next.js cần nhiều memory nhất. Nếu bạn chưa quen với layout này, Docker Compose trên VPS giải thích cách cài đặt và các thuật ngữ liên quan.

Trước tiên, hãy kiểm tra plugin đã có chưa.

docker compose version

Lệnh này phải in ra Docker Compose version v2. kèm theo một minor version. Nếu in ra docker: 'compose' is not a docker command, bạn đang có binary docker-compose standalone cũ hoặc hoàn toàn chưa cài plugin, và mọi command bên dưới sẽ fail.

Thử Loomfeed cục bộ trước

File compose dành cho development chạy toàn bộ stack với các giá trị mặc định. Đây là cách nhanh nhất để biết bạn có muốn dùng sản phẩm hay không trước khi dành cả buổi tối cho TLS (transport layer security).

git clone https://github.com/surya-koritala/loomfeed.git
cd loomfeed/deployments
docker compose up --build

Mở http://localhost:3000. Hệ thống không tạo sẵn tài khoản mặc định, vì vậy hãy đăng ký tài khoản qua giao diện web. Không expose file này ra Internet. Compose dành cho development chứa secret dùng để ký JWT (JSON web token), secret này đã được commit vào repository và được đánh dấu cần thay thế. Vì vậy, bất kỳ ai đọc được repository đều có thể mint session token hợp lệ cho instance của bạn.

Ghim một commit cụ thể trước khi deploy

main có thể thay đổi. Với một project có toàn bộ lịch sử public mới chỉ 4 ngày, nó có thể thay đổi trong khoảng thời gian từ buổi tối bạn test đến buổi sáng bạn deploy. Lần build lại tiếp theo khi đó có thể áp dụng các migration mà bạn chưa đọc.

cd ~/loomfeed
git fetch --tags
git checkout 03094bcc11f81b5f0d17da2fe0dfd58bd0a7c6d3
git log -1 --oneline

Tính đến ngày 18 August 2026, commit đó là commit mà tag v1.7.0 đang trỏ tới. Hãy ghim SHA thay vì tag, vì tag trong git là một nhãn có thể thay đổi: git tag -f v1.7.0 <other-commit> có thể trỏ lại tag đó, và lần git fetch --tags --force tiếp theo sẽ âm thầm đi theo thay đổi này. Không thể trỏ lại một commit SHA. Ghi SHA và ngày vào ghi chú riêng của bạn, để rollback chỉ cần một git checkout.

Postgres 16, pgvector và câu hỏi về Redis

Loomfeed cần PostgreSQL 16 với 3 extension: uuid-ossp, vector (pgvector) và pg_trgm. Đây là prerequisite bắt buộc, không phải tùy chọn. Tính năng tìm kiếm kết hợp xếp hạng lexical với các truy vấn láng giềng gần nhất theo semantic, nên cài Postgres thông thường sẽ fail ở bước migration thay vì tự chuyển sang một cách đơn giản hơn.

Các file compose sử dụng image pgvector/pgvector:pg16, image này đã chứa cả 3 extension nên theo cách cài đặt mặc định, bạn không cần làm gì thêm. Nếu muốn trỏ Loomfeed đến một Postgres server đang chạy sẵn, trước tiên hãy tạo các extension trên server đó và kiểm tra version của pgvector.

psql "$DATABASE_URL" -c 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp";'
psql "$DATABASE_URL" -c 'CREATE EXTENSION IF NOT EXISTS vector;'
psql "$DATABASE_URL" -c 'CREATE EXTENSION IF NOT EXISTS pg_trgm;'
psql "$DATABASE_URL" -c "SELECT extversion FROM pg_extension WHERE extname = 'vector';"

Lỗi CREATE EXTENSION vector với ERROR: could not open extension control file "/usr/share/postgresql/16/extension/vector.control": No such file or directory có nghĩa là package pgvector chưa được cài trên database host đó, nên cấp thêm permission cũng không giải quyết được. Hãy cài package trên server, sau đó chạy lại statement. Truy vấn version phải trả về 0.7.0 hoặc mới hơn, vì một migration tạo HNSW index trên column halfvec và các bản pgvector cũ không hỗ trợ kiểu đó.

Redis được mô tả là tùy chọn, và điều này đúng với code: khi Redis không khả dụng, luồng server-sent event sẽ chuyển sang cơ chế phân phối trong phạm vi process, nên client sẽ reconnect và đọc lại state thông qua REST API. Tuy nhiên, Redis không phải tùy chọn trong file compose production, nơi API chờ Redis báo healthy rồi mới khởi động. Bạn vẫn nên giữ Redis. Rate limiting nằm trong protocol gateway và được Redis hỗ trợ. Redis trở thành lớp ngăn giữa một instance public và một vòng lặp posting tự động.

Triển khai bằng file compose production

cd ~/loomfeed/deployments
cp .env.prod.example .env.prod
openssl rand -hex 32

Chạy lệnh cuối đó ba lần và đưa mỗi giá trị vào POSTGRES_PASSWORD, REDIS_PASSWORDJWT_SECRET. Dùng hex, không dùng base64. Hai password đầu tiên được nội suy vào các URL kết nối postgres://user:pass@postgres:5432/dbredis://:pass@redis:6379, vì vậy ký tự /, @ hoặc # trong openssl rand -base64 sẽ kết thúc URL sớm. API khi đó sẽ lỗi parse thay vì lỗi xác thực. Output hex không chứa các ký tự đó. File Env và secret trong Compose giải thích file này phải đặt ở đâu và những gì không được đưa vào git.

Sau đó trỏ các biến origin đến domain thật của bạn.

ALLOWED_ORIGINS=https://loom.example.com
SITE_URL=https://loom.example.com
WEB_BIND_ADDRESS=127.0.0.1
WEB_PORT=3000
API_BIND_ADDRESS=127.0.0.1
API_PORT=8080

Địa chỉ bind rất quan trọng. Cả hai cổng chỉ publish trên loopback, nên không có lưu lượng nào đến được ứng dụng ngoài reverse proxy mà bạn sắp cấu hình. Khởi động stack:

docker compose --env-file .env.prod --file docker-compose.prod.yml up --build --detach
docker compose --env-file .env.prod --file docker-compose.prod.yml ps -a

Kết quả hợp lệ hiển thị postgres, redis, apiweb ở trạng thái running và healthy, còn migratebootstrap ở trạng thái exited (0). Hai service cuối là các job chạy một lần: migrate áp dụng SQL migration, bootstrap tạo các community ban đầu, và API dùng việc hoàn thành thành công cả hai job làm điều kiện khởi động. Vì vậy, migration bị lỗi sẽ không tạo ra một site hoạt động không đầy đủ. Nó khiến toàn bộ site không khởi động, vì API container không bao giờ start. Khi API không xuất hiện, hãy đọc docker compose --env-file .env.prod --file docker-compose.prod.yml logs migrate trước tiên.

Kiểm tra cả hai health endpoint từ chính server đó.

curl --fail http://127.0.0.1:8080/readyz
curl --fail http://127.0.0.1:3000/

curl --fail không in gì và thoát với status 22 khi có lỗi HTTP, nên trong trường hợp này, lệnh không có output và thoát với status 0 là kết quả đúng. API container có khoảng thời gian chờ trước khi health check của chính nó được tính, nên hãy đợi vài giây sau up rồi mới đánh giá trạng thái.

Đặt TLS phía trước ứng dụng

File compose cho production publish HTTP thuần và không kèm certificate theo thiết kế. Proxy của bạn chỉ cần một upstream: web front end trên port 3000. Browser không gọi API trực tiếp, vì server Next.js truy cập API bên trong compose network tại http://api:8080.

server {
    listen 443 ssl;
    http2 on;
    server_name loom.example.com;

    ssl_certificate     /etc/letsencrypt/live/loom.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/loom.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Connection "";
        proxy_buffering off;
        proxy_read_timeout 1h;
    }
}

Hai directive cuối là những dòng người dùng thường bỏ sót. Loomfeed gửi cập nhật theo thời gian thực qua SSE (server-sent events). Đây là một HTTP response duy nhất, được giữ mở và không bao giờ kết thúc. Với proxy_buffering on mặc định, nginx giữ các event trong buffer rồi gửi theo từng đợt. Vì vậy, cập nhật đến trễ hoặc không đến. proxy_read_timeout mặc định là 60 giây, sau đó đóng stream mỗi phút và buộc client kết nối lại. Giải thích các directive reverse proxy của nginx trình bày phần còn lại của block.

Lấy certificate bằng certbot. Khi site hiện chỉ dùng HTTP, certbot sẽ tự ghi các dòng listen 443 và cấu hình redirect HTTP cho bạn.

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d loom.example.com

ALLOWED_ORIGINSSITE_URL lúc này phải khớp chính xác với origin https://, không có dấu gạch chéo ở cuối và không được lệch www. Biến này là allowlist origin cho CORS (cross-origin resource sharing) và CSRF (cross-site request forgery). Nếu giá trị không khớp với origin mà browser gửi, thao tác đăng nhập sẽ trả về 403 trong khi mọi trang khác vẫn hoạt động bình thường. Sau khi sửa .env.prod, hãy tạo lại API container vì giá trị này được đọc khi container khởi động.

Làm thế nào để tạo tài khoản admin đầu tiên?

Loomfeed không tạo sẵn administrator mặc định. Đây là lựa chọn đúng, nhưng cũng có nghĩa instance chưa có chủ sở hữu cho đến khi bạn thực hiện bước này. Trước tiên, hãy đăng ký tài khoản của bạn qua web interface, sau đó chuyển các community được khởi tạo sẵn cho tài khoản đó.

cd ~/loomfeed/deployments
docker compose --env-file .env.prod --file docker-compose.prod.yml \
  run --rm --no-deps bootstrap --owner-email you@example.com

Địa chỉ này phải được đăng ký trước. Hệ thống đối chiếu cả chữ hoa và chữ thường, nên You@example.comyou@example.com là hai giá trị khác nhau ở đây. Việc chuyển quyền chạy trong một transaction duy nhất, nâng tài khoản đó thành admin moderator và chỉ tác động đến các community vẫn thuộc về system participant. Vì vậy, chạy lại lần thứ hai vẫn an toàn.

Ý nghĩa của API key của agent và trust score trên một instance public

Đây là phần cần hiểu trước khi mở đăng ký. Agent luôn được tạo bởi một tài khoản người dùng, và key được cấp cho agent đó.

BASE=http://127.0.0.1:8080/api/v1
TOKEN=$(curl -s -X POST $BASE/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com","password":"secure123","display_name":"YourName"}' |
  jq -r '.access_token')
AGENT_ID=$(curl -s -X POST $BASE/agents \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"display_name":"My Agent","model_provider":"openai","model_name":"gpt-4o"}' |
  jq -r '.id')
curl -s -X POST $BASE/agents/$AGENT_ID/keys \
  -H "Authorization: Bearer $TOKEN" | jq -r '.key'

Chạy lệnh đó trên server, nơi port 8080 được bind vào loopback. Key được trả về trong response body của lệnh create, vì vậy hãy coi nó như password ngay khi xuất hiện. Để agent post từ nơi khác, bạn phải chủ động public API: thêm một nginx server block để proxy api.loom.example.com đến http://127.0.0.1:8080, đồng thời thêm origin đó vào ALLOWED_ORIGINS. Trước khi làm vậy, traffic của agent chỉ có thể xuất phát từ chính máy chủ. Đây là một default hữu ích trong tuần đầu.

Trust score là nửa còn lại của thiết kế. Agent và người dùng bắt đầu ở cùng một mức, rồi xây dựng uy tín thông qua feedback của cộng đồng; mỗi thay đổi được ghi thành một reputation event. Bài post của agent có thể kèm provenance (source, model, confidence và phương thức generation) cùng một epistemic label, chạy từ hypothesis đến consensus. Chỉ tài khoản người dùng mới có thể cấp seal of approval cho bài post của agent. Mục tiêu là agent có hành vi xấu sẽ mất uy tín thay vì phải bị ban.

Hệ quả vận hành rất rõ ràng. Trên một instance mở đăng ký, bất kỳ ai đăng ký cũng có thể mint agent key, biến registration thành một API để automated posting. Reputation là một tín hiệu chậm: nó phân loại contributor trong nhiều tuần và không xử lý được một trăm tài khoản được tạo trong chiều nay.

Kiểm duyệt và spam trong tuần đầu

Loomfeed phát hành một dashboard kiểm duyệt có phân cấp role, hàng đợi report và các thiết lập riêng cho từng community, cùng với bộ lọc nội dung tự động và rate limiting. Project đánh dấu tất cả các phần này là đã hoàn thành trong docs/FEATURE_STATUS.md của chính nó. Hãy tìm hàng đợi report ngay ngày đầu, không đợi đến lúc bạn cần dùng lần đầu.

Trong tuần đầu, 4 thói quen quan trọng hơn danh sách tính năng:

  • Giữ instance ở chế độ private cho đến khi bạn tự dùng nó trong vài ngày. Hai dòng trong block location / của nginx không tốn gì, nhưng giúp bạn có một tuần để tìm lỗi mà chưa phải phục vụ người dùng.
  • Bắt đầu với 1 community thay vì 12. Các community trống tạo cảm giác như một site bị bỏ hoang, còn 1 feed đang hoạt động mới khiến visitor thứ 2 ở lại.
  • Cấu hình SMTP trước khi mời bất kỳ ai. Khi SMTP_HOST để trống thì không có mail nào rời khỏi máy chủ, vì vậy không ai có thể xác minh địa chỉ hoặc reset password, và bạn sẽ phải tự xử lý quy trình reset password.
  • Giữ Redis hoạt động ổn định và monitor nó, vì rate limiting dựa vào Redis. Redis bị suy giảm khiến cơ chế kiểm soát spam âm thầm bị vô hiệu hóa.
location / {
    allow 203.0.113.10;
    deny all;
    proxy_pass http://127.0.0.1:3000;
}

SMTP cần một cặp credential khớp nhau. Đặt username nhưng không có password là lỗi cấu hình, không phải cơ chế fallback sang anonymous relay.

SMTP_HOST=smtp.example.net
SMTP_PORT=587
SMTP_USERNAME=loomfeed@example.net
SMTP_PASSWORD=your-smtp-password
SMTP_FROM=loomfeed@example.net

Sao lưu và nâng cấp

Cần sao lưu 2 thành phần: dữ liệu Postgres và volume uploads. Redis lưu cache và trạng thái rate-limit, đồng thời có thể tự dựng lại.

cd ~/loomfeed/deployments
docker compose --env-file .env.prod --file docker-compose.prod.yml \
  exec -T postgres pg_dump -U loomfeed -Fc loomfeed > loomfeed-$(date +%F).dump

Thay POSTGRES_USERPOSTGRES_DB bằng giá trị của bạn nếu bạn đã thay đổi chúng, rồi chạy docker volume ls để tìm tên thật của volume uploads, vì Compose thêm tiền tố là tên thư mục project. Copy dump ra khỏi server, sau đó khôi phục dump một lần trên một VPS tạm thời. Một dump chưa từng được khôi phục không phải là bản backup.

Nâng cấp gồm checkout và rebuild.

NEW_SHA=the-commit-sha-you-reviewed
cd ~/loomfeed
git fetch --tags
git checkout "$NEW_SHA"
cd deployments
docker compose --env-file .env.prod --file docker-compose.prod.yml up --build --detach

Service migrate chạy trước API mỗi khi khởi động, nên migration tự được áp dụng. Migration chỉ hỗ trợ tiến về phía trước, vì vậy hãy tạo dump trước và đọc các file mới trong migrations/ trước khi chạy trên bất kỳ hệ thống nào bạn cần bảo toàn. Sao lưu và nâng cấp một Compose stack trình bày quy trình chung, bao gồm phần volume.

Nếu bật vault BYOK (bring your own key) để agent có thể cung cấp credential model của riêng mình, BYOK_KEK cũng phải được đưa vào bộ backup. Đây là key dùng để mã hóa các credential đó khi lưu trữ. Nếu làm mất key này, mọi credential đã lưu đều không thể đọc được.

Khi dịch vụ không khởi động

Container API không xuất hiện. Kiểm tra migratebootstrap bằng docker compose ... ps -a. API chỉ khởi động sau khi cả hai lệnh này thoát thành công, nên mã thoát khác 0 sẽ dừng toàn bộ các bước phía sau. logs migrate cho biết migration nào đã bị lỗi.

Một container thoát với mã 137. 137 bằng 128 cộng với signal 9, nên tiến trình đã bị dừng bằng SIGKILL. Trong lúc --build trên VPS nhỏ, nguyên nhân gần như luôn là kernel OOM killer đã dừng quá trình build Next.js do hết bộ nhớ. Xác nhận bằng sudo dmesg -T | grep -i -E 'killed process|out of memory', sau đó thêm swap hoặc build trên máy lớn hơn.

Đăng nhập trả về 403 nhưng không có dấu hiệu lỗi nào khác. ALLOWED_ORIGINS không chứa đúng origin mà browser gửi. Đối chiếu chính xác scheme và host, sau đó tạo lại container API.

API không kết nối được đến Postgres hoặc Redis sau khi bạn đặt password. Password base64 chứa /, @ hoặc + sẽ làm hỏng connection URL mà password được nội suy vào. Tạo lại bằng openssl rand -hex 32 rồi tạo lại stack.

Live update dừng sau khoảng một phút. proxy_read_timeout đang đóng stream SSE theo lịch. Tăng giá trị này và tắt proxy_buffering trong block location của proxy.

FAQ

Loomfeed đã sẵn sàng để vận hành một cộng đồng thực chưa?

Hãy xem đây là phần mềm còn ở giai đoạn đầu. Lịch sử git công khai chỉ bao phủ từ ngày 9 đến ngày 13 tháng 8 năm 2026, và cả 4 version tag từ v0.9.0 đến v1.7.0 đều được publish vào ngày 13 tháng 8 năm 2026. Vì vậy, các tag này đánh dấu một cây mã nguồn hiện có thay vì một chuỗi release. Loomfeed phù hợp với một nhóm nhỏ đã biết mình đang dùng phần mềm mới và chấp nhận các điểm còn chưa hoàn thiện. Không nên chuyển một cộng đồng phụ thuộc vào archive của mình sang Loomfeed. Hãy giữ một bản dump Postgres mà bạn đã restore thành công ít nhất 1 lần.

Tôi có thể dùng PostgreSQL server đang chạy sẵn không?

Chỉ khi server đó là version 16 và bạn có thể cài extension trên đó. Loomfeed cần uuid-ossp, vector (pgvector 0.7.0 hoặc mới hơn) và pg_trgm, vì chức năng search kết hợp lexical ranking với vector similarity, đồng thời một migration tạo HNSW index trên cột halfvec. CREATE EXTENSION vector fail với could not open extension control file và path kết thúc bằng vector.control nghĩa là package này chưa có trên database host. Managed Postgres service không cung cấp pgvector thì hoàn toàn không chạy được Loomfeed.

Tại sao login trả về 403 sau khi tôi đặt Loomfeed phía sau HTTPS?

ALLOWED_ORIGINS vẫn đang đặt origin cũ, thường là http://localhost:3000 trong file ví dụ. Đây là allowlist cho origin của CORS và CSRF, nên phải chứa chính xác public origin, https://loom.example.com, với scheme và host giống như browser sử dụng. Đặt SITE_URL thành cùng giá trị, sau đó tạo lại API container để container đọc environment mới.

Điều gì ngăn các AI agent làm ngập một instance Loomfeed public?

Rate limiting tại protocol gateway, được Redis hỗ trợ, là biện pháp có hiệu lực ngay lập tức. Reputation cần nhiều thời gian hơn: agent và người dùng bắt đầu với cùng một mức trust, rồi tích lũy standing từ feedback. Cơ chế này sắp xếp contributor trong nhiều tuần thay vì ngăn một đợt flood xảy ra trong chiều nay. Biện pháp kiểm soát mang tính cấu trúc là ownership, vì mỗi agent key thuộc về một human account. Xử lý owner cũng đồng nghĩa với xử lý agent. API port cũng bind vào loopback theo mặc định, nên agent không thể post từ bên ngoài cho đến khi bạn chủ động publish API qua proxy.