Tự host RAG trên VPS với vector DB và LLM

SSH vào VPS, gõ free -h thấy 4GB RAM, và bạn đang cần một hệ thống RAG để truy vấn trên bộ tài liệu nội bộ. Giữa việc gửi toàn bộ dữ liệu ra ngoài cho API thương mại và việc tự host toàn bộ ngay trên máy chủ của mình, khoảng cách về công sức không lớn như bạn nghĩ. Bài này mình sẽ hướng dẫn dựng một pipeline RAG hoàn chỉnh, chạy trên VPS 4GB RAM, dùng Qdrant làm vector database, Ollama chạy embedding và LLM local, tất cả gói trong Docker Compose. Toàn bộ dữ liệu không rời khỏi máy chủ của bạn.
Tóm tắt nhanh
- Kiến trúc RAG gồm 3 thành phần: embedding model, vector database và LLM sinh câu trả lời.
- Qdrant là vector database viết bằng Rust, nhẹ, chạy tốt trên VPS 2-4GB RAM.
- Ollama quản lý model dễ dàng, model
nomic-embed-text(137M tham số) đủ tốt cho embedding tiếng Việt ở mức cơ bản. - Pipeline gồm 2 giai đoạn: indexing (chia chunk, nhúng, ghi vào Qdrant) và query (nhúng câu hỏi, tìm kiếm, đưa vào prompt cho LLM).
Yêu cầu trước khi bắt đầu
- Một VPS chạy Ubuntu 24.04 LTS, tối thiểu 4GB RAM (2GB là chạy được nhưng sẽ phải giảm model nhỏ hơn). Bài này dùng VPS Linux có full root để cài Docker.
- Docker Engine và Docker Compose v2 đã cài sẵn. Nếu chưa có, xem bài cách cài Docker trên Ubuntu chi tiết từ A đến Z.
- Hiểu cơ bản về REST API và JSON. Python là điểm cộng vì Qdrant có client chính thức, nhưng không bắt buộc nếu bạn dùng HTTP API trực tiếp.
Vì sao nên tự host RAG thay vì dùng API thương mại?
RAG (Retrieval-Augmented Generation) là kỹ thuật kết hợp giữa truy xuất thông tin và sinh văn bản: trước khi hỏi LLM, hệ thống tìm kiếm các đoạn văn bản liên quan trong kho dữ liệu riêng của bạn, rồi nhúng chúng vào prompt để model có ngữ cảnh trả lời. Cách này giải quyết hai vấn đề lớn của LLM: không biết dữ liệu riêng của bạn và dễ "ảo giác" khi không có nguồn tham chiếu.
Khi dùng các dịch vụ như ChatGPT hay API của OpenAI, dữ liệu của bạn buộc phải gửi sang máy chủ nước ngoài. Với tài liệu nội bộ, hợp đồng pháp lý hoặc dữ liệu khách hàng, rủi ro này thường không chấp nhận được. Tự host trên VPS giữ toàn bộ dữ liệu trong phạm vi kiểm soát của bạn. Thêm nữa, chi phí vận hành một hệ thống RAG cho nội bộ vài trăm tài liệu thấp hơn nhiều so với việc trả phí API theo token mỗi tháng.
Tất nhiên có đánh đổi: bạn tự lo hạ tầng, tự cập nhật model, tự xử lý khi vector database đầy. Nhưng với một VPS Linux 4GB RAM, toàn bộ hệ thống này chạy tốt cho nhóm nhỏ hoặc dùng cá nhân.
Kiến trúc hệ thống RAG tự host
Hệ thống gồm ba container chính, mỗi container một vai trò riêng, kết nối qua mạng Docker nội bộ:
| Thành phần | Container | Vai trò |
|---|---|---|
| Vector database | Qdrant | Lưu vector embedding và metadata, phục vụ tìm kiếm theo độ tương đồng |
| Embedding + LLM | Ollama | Sinh vector cho đoạn văn bản và sinh câu trả lời |
| Ứng dụng | FastAPI app | Nhận tài liệu đầu vào, chia chunk, gọi Ollama nhúng, ghi vào Qdrant; nhận câu hỏi và trả lời |
Ứng dụng API là nơi bạn viết logic RAG. Mình dùng FastAPI vì nhẹ và dễ viết, nhưng bạn hoàn toàn có thể thay bằng script Python đơn giản chạy nền, hoặc tích hợp vào VPS n8n làm workflow. Điểm quan trọng là tách riêng ba thành phần để khi cần scale, bạn chỉ việc nhân bản container tương ứng.
Bước 1 - Cài đặt Qdrant và Ollama bằng Docker Compose
Tạo thư mục làm việc và file docker-compose.yml. Mình dùng Qdrant phiên bản mới nhất và Ollama với model sẽ tải sau. Cấu hình này khai báo hai service, nhưng chưa có app vì cần viết code trước.
mkdir -p ~/rag-system && cd ~/rag-system
nano docker-compose.yml
Dán nội dung sau:
services:
qdrant:
image: qdrant/qdrant:latest
container_name: qdrant
ports:
- "6333:6333"
volumes:
- ./qdrant_storage:/qdrant/storage
restart: unless-stopped
ollama:
image: ollama/ollama:latest
container_name: ollama
ports:
- "11434:11434"
volumes:
- ./ollama_models:/root/.ollama
restart: unless-stopped
Volume là điểm quan trọng nhất ở đây. qdrant_storage và ollama_models là nơi dữ liệu thật được lưu. Nếu không mount, khi container bị xóa, toàn bộ vector và model cũng mất theo. Mình từng dính trường hợp chạy docker compose down để bảo trì rồi mất cả kho dữ liệu vì quên khai báo volume.
Chạy hệ thống và kiểm tra:
docker compose up -d
docker ps
Kết quả mong đợi: hai container qdrant và ollama ở trạng thái Up. Kiểm tra API của Qdrant:
curl http://localhost:6333/collections
Output sẽ là {"result":{"collections":[]}}. Qdrant chưa có collection nào, đúng như mong đợi.
Bước 2 - Tải embedding model và LLM vào Ollama
Ollama mới cài xong chưa có model nào. Bạn cần tải hai model: một cho embedding và một cho sinh văn bản. Với VPS 4GB RAM, mình khuyên dùng nomic-embed-text cho embedding và qwen2.5:3b hoặc llama3.2:3b cho LLM. Model 7B chạy được nhưng sẽ rất chậm trên CPU, mỗi câu trả lời có thể mất 3-5 phút.
docker exec ollama ollama pull nomic-embed-text
docker exec ollama ollama pull qwen2.5:3b
Model embedding có dung lượng khoảng 274MB, model LLM 3B khoảng 1.9GB. Tổng cộng dưới 2.5GB, cộng với RAM chạy container là vừa đủ trong 4GB. Lưu ý lệnh docker exec ollama ollama pull chạy bên trong container, không phải trực tiếp trên host. Nhiều bạn mới hay gõ nhầm ollama pull trên host và báo lỗi command not found.
Verify bằng lệnh:
docker exec ollama ollama list
Bạn sẽ thấy hai model trong danh sách. Model embedding không cần GPU, chạy nhanh trên CPU. Model LLM 3B trên CPU cũng chấp nhận được, khoảng 10-20 token/giây tùy CPU của VPS.
Bước 3 - Viết ứng dụng API xử lý indexing và query
Đây là phần lõi của hệ thống RAG. Bạn cần một script Python làm ba việc: chia nhỏ tài liệu thành chunk, gọi Ollama để nhúng các chunk đó thành vector rồi ghi vào Qdrant, và khi có câu hỏi thì nhúng câu hỏi, tìm vector gần nhất và gửi cho LLM. Mình viết bằng FastAPI để có endpoint HTTP, dễ dàng tích hợp với hệ thống khác.
Tạo file app.py:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import httpx, uuid
app = FastAPI()
OLLAMA_URL = "http://ollama:11434"
QDRANT_URL = "http://qdrant:6333"
COLLECTION = "documents"
EMBED_MODEL = "nomic-embed-text"
LLM_MODEL = "qwen2.5:3b"
class ChunkRequest(BaseModel):
text: str
metadata: dict = {}
class QueryRequest(BaseModel):
question: str
top_k: int = 5
def get_embedding(text: str) -> list:
resp = httpx.post(f"{OLLAMA_URL}/api/embed", json={"model": EMBED_MODEL, "input": text})
resp.raise_for_status()
return resp.json()["embeddings"][0]
@app.on_event("startup")
def init_collection():
httpx.put(f"{QDRANT_URL}/collections/{COLLECTION}", json={
"vectors": {"size": 768, "distance": "Cosine"}
})
@app.post("/upsert")
def upsert(req: ChunkRequest):
vec = get_embedding(req.text)
point_id = str(uuid.uuid4())
httpx.put(f"{QDRANT_URL}/collections/{COLLECTION}/points", json={
"points": [{"id": point_id, "vector": vec, "payload": {"text": req.text, **req.metadata}}]
})
return {"id": point_id}
@app.post("/query")
def query(req: QueryRequest):
vec = get_embedding(req.question)
resp = httpx.post(f"{QDRANT_URL}/collections/{COLLECTION}/points/search", json={
"vector": vec, "limit": req.top_k, "with_payload": True
})
results = resp.json()["result"]
ctx = "\n\n".join([r["payload"]["text"] for r in results])
prompt = f"Trả lời dựa trên ngữ cảnh sau:\n{ctx}\n\nCâu hỏi: {req.question}"
llm_resp = httpx.post(f"{OLLAMA_URL}/api/generate", json={"model": LLM_MODEL, "prompt": prompt, "stream": False})
return {"answer": llm_resp.json()["response"], "sources": [r["payload"]["text"][:200] for r in results]}
Lưu ý kích thước vector 768 trong init_collection. Model nomic-embed-text sinh vector 768 chiều, nếu bạn đổi model embedding khác thì phải đổi con số này cho khớp, nếu không Qdrant sẽ báo lỗi dimension mismatch. Đây là lỗi mình gặp khá nhiều khi thử nghiệm các model khác nhau.
Bước 4 - Chạy toàn bộ hệ thống bằng Docker Compose
Giờ bạn cần thêm service app vào docker-compose.yml. Sửa lại file để có ba service:
services:
qdrant:
image: qdrant/qdrant:latest
container_name: qdrant
ports:
- "6333:6333"
volumes:
- ./qdrant_storage:/qdrant/storage
restart: unless-stopped
ollama:
image: ollama/ollama:latest
container_name: ollama
ports:
- "11434:11434"
volumes:
- ./ollama_models:/root/.ollama
restart: unless-stopped
app:
build: .
container_name: rag_app
ports:
- "8000:8000"
depends_on:
- qdrant
- ollama
restart: unless-stopped
Bạn cần thêm file Dockerfile để đóng gói ứng dụng Python:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
File requirements.txt khai báo các thư viện cần thiết:
fastapi
uvicorn
httpx
pydantic
Khi đã có đủ file, chạy lại hệ thống:
docker compose up -d --build
Container app sẽ build image mới rồi khởi động. Kiểm tra bằng lệnh:
docker compose ps
Cả ba container phải ở trạng thái Up. Nếu container app liên tục restart, xem log để biết lỗi:
docker compose logs app
Bước 5 - Thêm tài liệu và truy vấn thử nghiệm
Hệ thống đã chạy, giờ là lúc kiểm tra thực tế. Đầu tiên, thêm một chunk tài liệu vào hệ thống. Giả sử bạn có một đoạn giới thiệu về dịch vụ:
curl -X POST http://localhost:8000/upsert \
-H "Content-Type: application/json" \
-d '{"text": "thueVPS cung cấp VPS Linux và Windows trên hạ tầng NVMe, IPv4 riêng đặt tại Việt Nam, full root, thanh toán theo tháng.", "metadata": {"source": "intro.docx"}}'
Output sẽ trả về một ID ngẫu nhiên, ví dụ {"id":"a3f2c1..."}. Thêm vài chunk nữa với nội dung khác nhau để có đủ dữ liệu tìm kiếm. Sau đó đặt câu hỏi:
curl -X POST http://localhost:8000/query \
-H "Content-Type: application/json" \
-d '{"question": "thueVPS có hỗ trợ thanh toán theo tháng không?"}'
Hệ thống sẽ nhúng câu hỏi, tìm các vector gần nhất trong Qdrant, nhúng kết quả vào prompt và gửi cho qwen2.5:3b. Output có dạng:
{"answer": "Dựa trên thông tin được cung cấp, thueVPS cung cấp dịch vụ VPS với phương thức thanh toán theo tháng.", "sources": ["thueVPS cung cấp VPS Linux..."]}
Phần sources là điểm mạnh của RAG: bạn luôn biết model lấy thông tin từ đâu, dễ kiểm chứng độ chính xác. Nếu model trả lời sai, bạn xem lại chunk nguồn để biết dữ liệu có đúng không.
Tối ưu cho VPS RAM thấp và xử lý lỗi thường gặp
Hệ thống này chạy được trên 4GB RAM, nhưng muốn ổn định lâu dài bạn cần vài điều chỉnh. Trước hết, tắt swap nếu không cần thiết và giới hạn RAM cho từng container trong Docker Compose bằng khai báo mem_limit. Ollama là container ngốn nhiều nhất, mình đặt giới hạn 2GB, Qdrant 512MB, app 512MB. Lý do là khi Ollama tải model vào RAM, nếu không giới hạn nó có thể chiếm toàn bộ bộ nhớ và khiến VPS bị treo.
Lỗi phổ biến nhất mình gặp là HttpError: connection refused khi app gọi sang Ollama hoặc Qdrant. Nguyên nhân thường là container app khởi động trước khi hai service kia sẵn sàng. Docker Compose depends_on chỉ đợi container chạy, không đợi service bên trong sẵn sàng. Giải pháp đơn giản là thêm lệnh chờ trong code hoặc khai báo restart: on-failure để Docker thử lại.
Lỗi thứ hai là dimension mismatch khi upsert vào Qdrant. Kiểm tra lại size trong lúc tạo collection phải khớp với chiều vector của model embedding. Lỗi thứ ba là model LLM quá lớn so với RAM. Nếu VPS chỉ có 2GB RAM, bạn nên dùng model qwen2.5:1.5b hoặc llama3.2:1b thay vì 3B để tránh OOM kill.
Câu hỏi thường gặp
RAG khác gì với fine-tuning model?
Fine-tuning thay đổi trọng số của model, cần GPU và dữ liệu huấn luyện lớn. RAG không sửa model mà chỉ bổ sung ngữ cảnh vào prompt trước khi sinh câu trả lời. RAG rẻ hơn, cập nhật dữ liệu tức thời và kiểm chứng được nguồn thông tin. Fine-tuning hợp khi bạn cần model học một phong cách hoặc lĩnh vực chuyên sâu, còn lại RAG đáp ứng tốt cho bài toán tài liệu nội bộ.
Có cần GPU để chạy RAG tự host trên VPS?
Không bắt buộc. Với model embedding nhỏ như nomic-embed-text, CPU xử lý rất nhanh. Model LLM 1-3B tham số chạy trên CPU có tốc độ chấp nhận được, khoảng 10-20 token/giây. Nếu cần tốc độ cao hơn hoặc dùng model 7B trở lên, bạn cần VPS có GPU, chi phí sẽ cao hơn đáng kể.
Nên chọn Qdrant hay Chroma hay PostgreSQL có pgvector?
Qdrant mạnh về hiệu năng tìm kiếm vector và có API HTTP dễ dùng, hợp làm service độc lập. Chroma nhẹ, chạy trong Python, hợp khi bạn muốn nhúng thư viện trực tiếp vào ứng dụng. pgvector cho phép dùng chung database SQL và vector, giảm số lượng service nhưng hiệu năng tìm kiếm ở quy mô lớn không bằng Qdrant. Với hệ thống nhỏ, Qdrant là lựa chọn cân bằng nhất.
Chunk kích thước bao nhiêu là hợp lý?
Không có con số chuẩn rất cao. Với model embedding nhúng toàn bộ câu, chunk 300-500 token cho kết quả tốt ở phần lớn trường hợp. Chunk quá nhỏ làm mất ngữ cảnh, chunk quá lớn làm giảm độ chính xác khi tìm kiếm vì vector pha trộn nhiều chủ đề. Bạn nên thử với khoảng 300 token, xem kết quả rồi điều chỉnh theo đặc thù tài liệu.
Làm sao để thêm model tốt hơn khi VPS có cấu hình mạnh hơn?
Chỉ cần sửa biến LLM_MODEL trong app.py và chạy docker exec ollama ollama pull tên-model là xong. Với VPS 16GB RAM, bạn có thể chạy qwen2.5:7b hoặc llama3.1:8b mượt mà. Nhớ cập nhật lại image Ollama để có phiên bản hỗ trợ model mới nhất.
Bài viết liên quan
- Self-host API LLM với Ollama trên VPS
- Tự dựng AI chatbot riêng với Ollama và Open WebUI trên VPS
- Xây chatbot RAG bằng n8n và vector store trên VPS riêng
- Tự host object storage tương thích S3 với MinIO trên VPS


