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

Tự host LiteLLM làm LLM gateway trên VPS

Chạy LiteLLM trên VPS để gom mọi provider vào một OpenAI-compatible endpoint, quản lý virtual key, ngân sách từng key, fallback và pinned image.

Gateway LLM tự host làm gì

LiteLLM là một LLM gateway mã nguồn mở mà bạn tự host: một HTTP endpoint để tất cả ứng dụng gọi đến, sau đó chuyển tiếp từng request đến provider phù hợp để xử lý. LLM là viết tắt của large language model. Gateway này sử dụng OpenAI chat completions API (application programming interface), nên mọi client library vốn đã giao tiếp với OpenAI đều có thể dùng với gateway sau khi thay đổi 2 thứ: base URL và key.

Lớp trung gian này chính là mục đích của mô hình. Ứng dụng không còn phải lưu credential của provider. Khi đổi model, bạn chỉ cần sửa một dòng trong file cấu hình trên server thay vì sửa code ở 5 service. Vì mọi call đều đi qua một process, bạn có một nơi để đặt ngân sách và lưu lại số tiền đã chi.

Sau khi chạy xong, bạn có:

  • Một endpoint. Ứng dụng gọi đến https://gateway.example.com/v1 và yêu cầu một model name do bạn tự đặt, chẳng hạn bulk hoặc strong.
  • Virtual key. Mỗi ứng dụng có key riêng, model allowlist riêng và giới hạn chi tiêu riêng. Bạn có thể revoke một key mà không ảnh hưởng đến các key khác.
  • Fallback. Khi call thất bại hoặc prompt quá lớn, hệ thống tự động retry bằng một model khác.
  • Bản ghi có log. Mỗi request ghi một row kèm chi phí, nên bạn luôn biết "ứng dụng nào đã chi khoản đó".

Vì sao nên tự chạy gateway

Một router được quản lý có cùng mô hình, nhưng có thêm quy trình của bên khác đứng giữa mọi request. Tự chạy gateway giúp giữ provider key và nội dung prompt trên máy chủ do bạn kiểm soát. Chi phí vận hành là có thật: giờ đây bạn phải quản lý component mà mọi ứng dụng đều phụ thuộc vào. Phần cuối của hướng dẫn này tập trung vào chi phí đó, vì đây là phần mà hầu hết tài liệu thường bỏ qua.

Cần chuẩn bị

  • Một VPS (máy chủ riêng ảo) chạy Ubuntu 24.04, đã cài Docker và plugin Compose.
  • Một tên miền trỏ đến VPS đó nếu các máy bên ngoài sẽ truy cập gateway qua TLS (bảo mật tầng truyền tải).
  • Ít nhất một API key của provider.

Gateway không chạy inference. Gateway chuyển tiếp request và stream câu trả lời về cho client, nên tải CPU phụ thuộc vào số lượng request thay vì kích thước model. Một máy có 1 vCPU có thể chạy ổn một số ứng dụng nội bộ. Phần tăng lên là database, vì gateway ghi một bản ghi chi phí cho mỗi request.

Viết config.yaml trước

File cấu hình quyết định client được phép yêu cầu những model nào. Có 4 section cấp cao cần chú ý: model_list, litellm_settings, router_settingsgeneral_settings.

model_list:
  - model_name: bulk
    litellm_params:
      model: anthropic/claude-haiku-4-5
      api_key: os.environ/ANTHROPIC_API_KEY
  - model_name: strong
    litellm_params:
      model: anthropic/claude-sonnet-5
      api_key: os.environ/ANTHROPIC_API_KEY
  - model_name: strong
    litellm_params:
      model: openai/gpt-5.5
      api_key: os.environ/OPENAI_API_KEY

litellm_settings:
  num_retries: 2
  request_timeout: 120
  allowed_fails: 3
  cooldown_time: 30
  json_logs: true
  set_verbose: false

router_settings:
  fallbacks: [{"bulk": ["strong"]}]
  context_window_fallbacks: [{"bulk": ["strong"]}]

general_settings:
  background_health_checks: true
  health_check_interval: 300

model_name là tên client gửi lên. litellm_params.model là model thực tế, được ghi dưới dạng provider/model. Hãy đặt tên model theo công việc thay vì theo vendor. Một ứng dụng yêu cầu bulk sẽ tiếp tục hoạt động nếu tháng sau bạn quyết định bulk phải dùng model khác.

api_key: os.environ/ANTHROPIC_API_KEY yêu cầu LiteLLM đọc biến đó tại thời điểm chạy. Key literal không xuất hiện trong file. Điều này quan trọng vì config.yaml là file bạn commit.

Có 2 entry dùng cùng tên strong, và đây là chủ ý. Khi có nhiều deployment mang cùng model_name, router xem chúng là các deployment có thể thay thế cho nhau và thử deployment khác nếu deployment đầu tiên fail. Nhờ đó, strong vẫn hoạt động khi một provider gặp sự cố trong một khoảng thời gian.

num_retries: 2 retry cùng deployment khi gặp lỗi có thể retry. Fallback chỉ được kích hoạt sau khi đã dùng hết số lần retry đó. allowed_fails: 3 cùng với cooldown_time: 30 sẽ đưa một deployment ra khỏi rotation trong 30 giây sau khi deployment đó fail 3 lần. Vì vậy, provider trả về lỗi 500 sẽ không bị thử lại trong từng request.

fallbackscontext_window_fallbacks có trigger khác nhau, và trigger thứ hai là tùy chọn hữu ích mà nhiều người bỏ qua.

  • fallbacks được kích hoạt khi request chính fail.
  • context_window_fallbacks được kích hoạt khi provider từ chối request vì request dài hơn context window của model đó. Khi đó, prompt quá lớn sẽ được chuyển đến model còn đủ dung lượng thay vì trả lỗi cho caller.

Ngoài ra còn có content_policy_fallbacks, dùng khi provider từ chối request vì chính sách nội dung. Chỉ đặt tùy chọn này nếu bạn có nơi phù hợp để chuyển các request đó đến.

Triển khai LiteLLM trên VPS bằng Docker Compose

Tạo một thư mục chứa 3 file: config.yaml, docker-compose.yml.env. Quickstart của upstream kéo tag latest. Hãy ghim một release tag cụ thể thay vào đó, để docker compose up -d vào tháng sau vẫn cung cấp cùng gateway như hôm nay, đồng thời rollback chỉ cần một dòng.

services:
  litellm:
    image: ghcr.io/berriai/litellm:v1.95.0
    restart: unless-stopped
    command: ["--config", "/app/config.yaml", "--num_workers", "1"]
    ports:
      - "127.0.0.1:4000:4000"
    volumes:
      - ./config.yaml:/app/config.yaml:ro
    env_file: .env
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16
    restart: unless-stopped
    environment:
      POSTGRES_USER: litellm
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
      POSTGRES_DB: litellm
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U litellm"]
      interval: 5s
      timeout: 5s
      retries: 10
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Compose đọc .env hai lần ở đây. Một lần để thay thế ${POSTGRES_PASSWORD} bên trong chính file compose, và một lần thông qua env_file để truyền mọi biến vào container.

v1.95.0 là release hiện tại vào tháng 8 năm 2026. Hãy kiểm tra trang releases của dự án và ghim phiên bản hiện tại tại thời điểm triển khai. Mỗi release đều công bố một chữ ký, nên bạn có thể kiểm tra image trước khi tin cậy image đó:

cosign verify --key https://raw.githubusercontent.com/BerriAI/litellm/v1.95.0/cosign.pub ghcr.io/berriai/litellm:v1.95.0

Dòng port là 127.0.0.1:4000:4000, chỉ publish port trên loopback interface. Nếu viết 4000:4000, gateway sẽ có thể được truy cập từ toàn bộ Internet, vì Docker tự thêm rule vào chuỗi FORWARD của iptables và các rule này được đánh giá trước rule của ufw, nên ufw deny 4000 không chặn được. Đây là cách phổ biến nhất khiến một gateway tự host bị mở ra Internet: xem cách Docker publish port của container vượt qua ufw. Lưu lượng từ bên ngoài sẽ đi qua reverse proxy.

Không đưa key của provider vào image

File .env chứa toàn bộ secret. File này được truyền vào dưới dạng biến môi trường khi chạy, nên không bao giờ được ghi sẵn vào image và cũng không bao giờ được commit.

LITELLM_MASTER_KEY=sk-REPLACE_ME
LITELLM_SALT_KEY=sk-REPLACE_ME_TOO
POSTGRES_PASSWORD=REPLACE_ME_AS_WELL
DATABASE_URL=postgresql://litellm:REPLACE_ME_AS_WELL@db:5432/litellm
STORE_MODEL_IN_DB=True
LITELLM_MODE=PRODUCTION
LITELLM_LOG=ERROR
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-proj-...

Tạo hai key của LiteLLM bằng nguồn ngẫu nhiên thực sự, sau đó giới hạn quyền truy cập file:

printf 'sk-%s\n' "$(openssl rand -hex 32)"
chmod 600 .env

LITELLM_MASTER_KEY là credential quản trị. Nó dùng để xác thực management API và là mật khẩu cho Admin UI tại /ui. Không ứng dụng nào được giữ credential này.

LITELLM_SALT_KEY mã hóa credential của provider được lưu trong database. Hãy đặt giá trị này một lần rồi giữ nguyên. Nếu thay đổi sau đó, các credential đã lưu sẽ không thể giải mã. Gateway vẫn khởi động bình thường, nhưng mọi lần gọi đến các provider đó đều thất bại do lỗi xác thực.

STORE_MODEL_IN_DB=True cho phép bạn thêm và chỉnh sửa model từ Admin UI mà không cần sửa config.yaml. Cách này tiện, nhưng khiến source of truth bị chia thành hai nơi. Hãy quyết định nơi nào là nguồn chính thức và ghi lại quyết định đó ngay cạnh config.

Lý do không đưa key vào file config cũng giống lý do không đưa chúng vào các công cụ bạn cung cấp cho agent. Không đưa secret của provider vào AI agent trình bày mẫu triển khai này, còn file env và secret trong Docker Compose trình bày các thao tác cụ thể.

Khởi động dịch vụ và theo dõi lần boot đầu tiên:

docker compose up -d
docker compose logs -f litellm

Kiểm tra xem hệ thống có thực sự hoạt động không

Có 2 probe không cần xác thực và 1 probe có xác thực. Chúng có thể thất bại vì các nguyên nhân khác nhau.

curl -s http://127.0.0.1:4000/health/liveliness
curl -s http://127.0.0.1:4000/health/readiness

/health/liveliness không cần xác thực và trả về "I'm alive!" khi tiến trình đang chạy. /health/readiness cũng không cần xác thực. Endpoint này trả về một object JSON có trường "status": "healthy" và trường db, hoặc trả về 503 khi không thể truy cập database. Hãy cấu hình hệ thống monitoring kiểm tra readiness, vì liveliness vẫn có thể ở trạng thái xanh trên một gateway không thể tra cứu dù chỉ một virtual key.

Probe có xác thực là probe giao tiếp với các provider:

curl -s http://127.0.0.1:4000/health \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY"

Probe này trả về các mảng healthy_endpointsunhealthy_endpoints. Nếu một model nằm trong unhealthy_endpoints kèm lỗi xác thực, provider key trong .env bị sai hoặc bị thiếu. Đây là lỗi bạn cần tìm lúc này. Vì background_health_checks: true được bật, proxy tự chạy các probe này mỗi health_check_interval giây. /health trả về kết quả gần nhất, nên việc polling endpoint này không gửi test request đến các provider mỗi lần.

Khóa ảo và ngân sách riêng cho từng khóa

Mỗi ứng dụng có một key riêng, được tạo dựa trên master key.

curl -s http://127.0.0.1:4000/key/generate \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "key_alias": "nightly-summariser",
    "models": ["bulk"],
    "max_budget": 5,
    "budget_duration": "30d",
    "rpm_limit": 60,
    "tpm_limit": 200000
  }'

Response chứa trường key bắt đầu bằng sk-. Chuỗi đó là thứ ứng dụng nhận được, và đó là thông tin duy nhất ứng dụng nhận được.

  • models là allowlist xác định những gì key này được phép request. Key ở trên chỉ có thể request bulk, không được request gì khác.
  • max_budget: 5 với budget_duration: "30d" là 5 đô la Mỹ trong mỗi chu kỳ 30 ngày, sau đó key sẽ ngừng hoạt động.
  • rpm_limittpm_limit giới hạn số request mỗi phút và số token mỗi phút chỉ cho key này.
  • key_alias là giá trị bạn sẽ nhận ra trong spend log sau sáu tuần. Luôn đặt giá trị này.

Khi ngân sách đã hết, call sẽ fail với HTTP 401 và body có dạng sau:

ExceededBudget: Current spend for token: 7.2e-05; Max Budget for Token: 2e-07

Status code là nguyên nhân khiến lỗi này khó nhận biết. Client library báo 401 là lỗi xác thực, nên developer đọc stack trace sẽ bắt đầu kiểm tra xem key có hợp lệ không. Hãy ghi response body cùng với status code; nếu không, hết ngân sách sẽ luôn trông giống như credential bị hỏng.

Kiểm tra và điều chỉnh các key thông qua cùng management API:

curl -s "http://127.0.0.1:4000/key/info?key=sk-..." \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY"

curl -s -X POST http://127.0.0.1:4000/key/update \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"key": "sk-...", "max_budget": 25}'

Ngân sách được áp dụng tại gateway vẫn có hiệu lực ngay cả khi nguyên nhân nằm ở chính agent. Vì vậy, đây là nền tảng của kiểm soát chi phí cho AI agent trên VPS.

Chuyển tác vụ hàng loạt sang model giá rẻ

Trỏ client vào gateway. Base URL, key và tên model:

curl -s http://127.0.0.1:4000/v1/chat/completions \
  -H "Authorization: Bearer sk-<the virtual key>" \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "bulk",
    "messages": [{"role": "user", "content": "Say hello in five words."}]
  }'

Mọi thư viện client OpenAI đều hoạt động theo cùng cách: đặt base_url thành https://gateway.example.com/v1api_key thành virtual key.

Routing policy trong config.yaml hiện được áp dụng mà caller không cần biết. Request cho bulk sẽ được chuyển đến model giá rẻ. Nếu lần gọi đó thất bại sau khi đã retry, request sẽ được retry với strong. Nếu prompt quá dài đối với bulk, context_window_fallbacks sẽ chuyển nó sang strong thay vì trả về lỗi. Các tác vụ hàng loạt như chạy phân loại hoặc tóm tắt backlog mặc định sẽ dùng model giá rẻ, chỉ những request khó mới tốn nhiều chi phí hơn.

Đây cũng là trường hợp gateway phát huy giá trị với các agent sử dụng tool. Một MCP (model context protocol) server trên cùng VPS và agent điều khiển nó đều có thể trỏ đến một endpoint, nên có thể thay đổi model phía sau mà không cần redeploy bất kỳ thành phần nào.

Làm sao biết đã xảy ra fallback?

Đây là kiểu lỗi gây tốn tiền vì không có dấu hiệu rõ ràng. Fallback thành công trả về HTTP 200 cùng phần nội dung phản hồi bình thường. Model giá rẻ có thể ngừng hoạt động cả ngày, mọi request vẫn âm thầm được xử lý bằng model đắt tiền, và bằng chứng đầu tiên là hóa đơn.

Bằng chứng nằm trong response header. Hãy yêu cầu các header đó:

curl -s -D - -o /dev/null http://127.0.0.1:4000/v1/chat/completions \
  -H "Authorization: Bearer sk-<the virtual key>" \
  -H 'Content-Type: application/json' \
  -d '{"model":"bulk","messages":[{"role":"user","content":"ping"}]}' \
  | grep -i '^x-litellm'
  • x-litellm-model-group là deployment mà client yêu cầu. x-litellm-model-id là deployment đã trả lời. Khi hai giá trị này khác nhau, fallback đã xảy ra.
  • x-litellm-attempted-fallbacksx-litellm-attempted-retries đếm số lần xảy ra. Với request bình thường, cả hai đều là 0.
  • x-litellm-response-cost là chi phí của request đó tính bằng đô la Mỹ.
  • x-litellm-call-id là mã định danh dùng để tìm request tương ứng trong log.

Ghi lại x-litellm-attempted-fallbacks trong mọi request và tạo alert khi giá trị này không còn là 0. Một con số này cho biết sự khác biệt giữa routing policy hoạt động đúng và routing policy đã âm thầm chuyển thành "luôn dùng model đắt tiền".

Phiên bản đầy đủ của cách này là tracing và cần một setup riêng: tự host Langfuse để tracing các agent call. LiteLLM có sẵn callback, nên việc tích hợp chỉ cần 2 dòng cộng với credentials.

litellm_settings:
  success_callback: ["langfuse"]
  failure_callback: ["langfuse"]
LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_SECRET_KEY=sk-lf-...
LANGFUSE_HOST=https://langfuse.example.com

Thiết lập cả failure_callbacksuccess_callback. Nếu bỏ qua, bạn chỉ giữ lại các trace mà không có lỗi. Độc lập với các cơ chế trên, LiteLLM ghi một spend row cho mỗi request vào Postgres, còn Admin UI tại /ui đọc bảng đó. Bảng này tăng theo traffic, vì vậy hãy theo dõi dung lượng nếu disk nhỏ.

Đặt gateway phía sau reverse proxy

Không được để bất kỳ kết nối nào từ bên ngoài truy cập cổng 4000. Terminate TLS trong nginx hoặc Caddy, rồi forward đến địa chỉ loopback.

location / {
    proxy_pass http://127.0.0.1:4000;
    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_buffering off;
    proxy_read_timeout 600s;
}

Có hai dòng thường bị bỏ qua. proxy_buffering off quan trọng vì streaming completion là một chuỗi server-sent event. Khi bật buffering, nginx giữ các chunk cho đến khi response kết thúc. Client sẽ không nhận được gì trong thời gian đó, rồi nhận toàn bộ dữ liệu cùng lúc. proxy_read_timeout 600s quan trọng vì một lần generation dài có thể vượt quá mặc định 60 giây của nginx. Khi đó, client nhận lỗi 504 và error log ghi upstream timed out (110: Connection timed out) while reading response header from upstream.

Để cấp certificate, Dùng Certbot với Let’s Encrypt trên nginx là cách nhanh nhất. Nếu máy chủ đã chạy nhiều container, Dùng Traefik phía trước nhiều ứng dụng Compose để xử lý routing và certificate tại một nơi.

Gateway hiện là điểm lỗi đơn

Hãy đánh giá đúng những gì bạn đã triển khai. Mọi ứng dụng bạn quản lý hiện phụ thuộc vào một container trên một VPS. Khi container này ngừng hoạt động, không ứng dụng nào có thể gọi bất kỳ model nào, kể cả các provider vẫn đang hoạt động bình thường. Điều này dẫn đến 4 điểm sau.

  • Một cấu hình sai có thể làm mọi thứ ngừng hoạt động cùng lúc. restart: unless-stopped sẽ khởi động lại tiến trình bị crash, kể cả container không thể phân tích config.yaml, rồi lặp lại việc đó liên tục. Đọc docker compose logs litellm sau mỗi lần thay đổi cấu hình và chỉ thay đổi cấu hình khi bạn có thời gian theo dõi.
  • Postgres nằm trên đường đi của request. Việc tra cứu virtual key và ghi nhận chi phí đều dùng database này. /health/readiness trả về 503 là cảnh báo rằng gateway vẫn đang chạy nhưng không thể thực hiện cả hai việc đó.
  • Hãy scale bằng cách thêm instance, không phải làm một instance lớn hơn. Hướng dẫn của dự án là chạy một worker cho mỗi instance (--num_workers 1), với nhiều instance dùng chung một database. Hai gateway nhỏ đặt sau load balancer sẽ loại bỏ container đơn khỏi điểm lỗi duy nhất. Chúng không loại bỏ database.
  • Hãy backup những dữ liệu không thể tạo lại. Đó là config.yaml.env, cùng với một pg_dump của database. Mất LITELLM_SALT_KEY sẽ khiến thông tin xác thực provider đã mã hóa trong dump đó trở nên vô dụng. Vì vậy, env file và dump phải nằm trong cùng một backup job: backup restic vào storage bên ngoài máy chủ.

Để upgrade, hãy sửa image tag rồi chạy docker compose up -d. LiteLLM mặc định chạy prisma migrate deploy khi startup, nên container mới sẽ migrate schema của database trong lần boot đầu tiên. Hãy tạo dump trước khi thay đổi tag, vì khôi phục image cũ không hoàn tác được migration đã chạy.

FAQ

LiteLLM có làm tăng độ trễ đáng kể cho mọi request không?

Dự án công bố mức 8 ms ở percentile 95 với 1000 request mỗi giây, theo README vào tháng 8 năm 2026. Hãy xem đây là số liệu do nhà cung cấp công bố. Yếu tố thực sự làm thay đổi độ trễ là khoảng cách mạng giữa các ứng dụng và gateway, vì mỗi request nay phải thêm một round trip. Chạy gateway trong cùng region với các ứng dụng gọi đến nó, sau đó đo overhead thực tế bằng header x-litellm-overhead-duration-ms trên một response thật.

Vì sao streaming ngừng hoạt động sau khi tôi đặt nginx phía trước?

Vì nginx mặc định buffer response từ upstream, còn một completion dạng streaming là chuỗi server-sent event. Khi bật proxy_buffering, nginx gom các chunk lại và chỉ gửi chúng khi response hoàn tất. Vì vậy client chờ trong im lặng rồi nhận toàn bộ câu trả lời cùng lúc. Đặt proxy_buffering off; trong location block. Tăng proxy_read_timeout trong cùng block, vì quá trình generation dài sẽ vượt quá giá trị mặc định 60 giây của nginx và client nhận lỗi 504.

Điều gì xảy ra khi virtual key hết budget?

Request thất bại với HTTP 401 và body có dạng ExceededBudget: Current spend for token: 7.2e-05; Max Budget for Token: 2e-07. Mã 401 dễ gây nhầm lẫn: client library báo đây là lỗi xác thực, nên người dùng bắt đầu kiểm tra key có hợp lệ không thay vì đọc message. Ghi log body của response cùng với status code. Xác nhận vị trí thực tế của key bằng /key/info?key=sk-... với master key, rồi tăng giới hạn bằng /key/update nếu budget được đặt quá thấp.

Gateway có thể route đến model local cũng như model hosted không?

Có. Đây là một entry nữa trong model_list. Dùng prefix ollama_chat/ với một api_base, ví dụ model: ollama_chat/llama3.1 cùng với api_base: http://ollama:11434. Bên trong container, localhost có nghĩa là chính container đó. Vì vậy hãy dùng tên service trong Compose hoặc địa chỉ của host trên Docker network, không bao giờ dùng 127.0.0.1. Việc khởi chạy model local là một công việc riêng: xem tự host LLM bằng Ollama trên VPS.