Chạy llama.cpp server trên VPS bằng systemd
Build llama-server từ tag cố định, phục vụ model GGUF qua OpenAI-compatible API, chỉ bind localhost và chạy bằng systemd với giới hạn memory.
Bạn sẽ xây dựng gì
Chạy llama.cpp server trên một VPS nghĩa là chỉ cần một binary, llama-server, để load một file model GGUF duy nhất và trả lời các HTTP request qua API tương thích với OpenAI. Trỏ bất kỳ OpenAI client nào đến http://127.0.0.1:8080/v1 là có thể sử dụng. Phần cài đặt là phần dễ.
Phần còn lại là vận hành: cố định version, chỉ bind port trên localhost, viết systemd unit và quyết định phải xử lý thế nào khi máy hết memory. Đây là nội dung của guide này. Nếu bạn chưa quyết định giữa hai lựa chọn phổ biến, trước hết hãy đọc so sánh ưu và nhược điểm giữa Ollama và llama.cpp, vì guide này cố ý tập trung vào phần hướng dẫn triển khai mà bài so sánh đó không đề cập.
Chọn một release tag và ghi lại
llama.cpp gắn tag cho gần như mọi lần merge, nên các tag chính là số bản build. b10488 là bản mới nhất tính đến ngày 18 August 2026. Không có nhánh stable duy trì lâu dài. Vì vậy, "latest" luôn thay đổi và version bạn đã kiểm thử là version duy nhất bạn có thể hỗ trợ. Hãy chọn một tag, ghi lại, rồi dùng đúng chuỗi đó trong lệnh clone, tên binary và ghi chú của bạn.
Mỗi tag cũng có các archive dựng sẵn. Với VPS x86 chỉ dùng CPU, archive đó là llama-b10488-bin-ubuntu-x64.tar.gz. Nếu dùng VPS ARM thay vì x86, archive arm64 nằm ngay bên cạnh.
curl -LO https://github.com/ggml-org/llama.cpp/releases/download/b10488/llama-b10488-bin-ubuntu-x64.tar.gz
tar tf llama-b10488-bin-ubuntu-x64.tar.gz | headHãy liệt kê archive trước khi giải nén để biết các file sẽ được đặt ở đâu. Những binary đó được link với C library của image dùng để build chúng. Vì vậy, trên bản distribution cũ hơn, chúng sẽ fail khi khởi động với lỗi nêu tên version GLIBC_ chưa được cài đặt. Build từ source chỉ mất vài phút trên VPS nhỏ và loại bỏ hoàn toàn nhóm lỗi này, nên phần dưới đây dùng cách đó.
Build llama-server từ một tag cố định
sudo apt update
sudo apt install -y build-essential cmake git libssl-dev
git clone --depth 1 --branch b10488 https://github.com/ggml-org/llama.cpp
cd llama.cpp
cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DLLAMA_BUILD_TESTS=OFF -DLLAMA_BUILD_EXAMPLES=OFF
cmake --build build --config Release -t llama-server -j 2--branch b10488 trên một bản clone --depth 1 sẽ checkout tag đó và không lấy thêm nội dung nào khác. Vì vậy, mã nguồn không thể thay đổi ngoài ý muốn trong lúc bạn build.
libssl-dev quan trọng vì tùy chọn LLAMA_OPENSSL được bật mặc định. Tùy chọn này cho phép binary tải model qua HTTPS về sau. Nếu thiếu các header này, bước configure sẽ fail.
-DBUILD_SHARED_LIBS=OFF tạo một binary tự chứa đầy đủ. Bản build mặc định đặt các shared library cạnh executable. Vì vậy, chỉ copy executable sang /usr/local/bin sẽ fail với error while loading shared libraries: libllama.so.
-t llama-server chỉ build target server. Bản build mặc định cũng compile các tool khác và test. Trên một VPS 2 core, việc này tốn thêm vài phút cho những file bạn sẽ không bao giờ chạy.
-j 2 là có chủ đích. Mỗi job compile song song giữ một working set riêng. Vì vậy, -j $(nproc) trên một plan nhỏ sẽ dẫn đến c++: fatal error: Killed signal terminated program cc1plus. Đây là lúc kernel out-of-memory killer dừng compiler. Hãy giảm số job hoặc thêm swap cho quá trình build.
Có một flag bạn có thể muốn thay đổi: GGML_NATIVE mặc định là bật, nên compiler target đúng CPU đang thực hiện build. Đây là lựa chọn phù hợp khi bạn build trên chính máy sẽ chạy binary. Nếu build một lần rồi copy binary sang host khác, hãy thêm -DGGML_NATIVE=OFF. Nếu không, binary dùng các instruction mà CPU kia không hỗ trợ sẽ dừng với Illegal instruction (core dumped) ngay ở lần inference đầu tiên.
Cài đặt binary bằng tên có chứa tag.
./build/bin/llama-server --version
sudo install -m 755 build/bin/llama-server /usr/local/bin/llama-server-b10488
sudo ln -sfn /usr/local/bin/llama-server-b10488 /usr/local/bin/llama-server--version in ra build number và commit. Các giá trị này phải khớp với tag bạn đã checkout. Nếu không khớp, bạn đã build một phiên bản khác. Giữ number trong filename và trỏ một symlink đến file đó giúp việc upgrade chỉ cần một ln -sfn rồi restart một lần. Rollback cũng dùng cùng lệnh đó nhưng với number cũ.
Tải model GGUF và kiểm tra disk trước
GGUF là format file duy nhất mà llama.cpp có thể load. Một file chứa weights, tokeniser và metadata, nên không cần cài thêm gì. Hậu tố trong tên file cho biết quantisation, tức precision dùng để lưu weights: Q4_K_M là bản kết hợp 4-bit, Q8_0 là 8-bit, còn f16 là file half-precision chưa quantise.
Tạo service account và thư mục chứa model trước khi tải.
sudo useradd --system --home /srv/llama --create-home --shell /usr/sbin/nologin llama
sudo install -d -o llama -g llama /srv/models
df -h /srvServer có thể tự tải model bằng -hf. Đây là cách nhanh nhất để xác nhận build hoạt động.
sudo -u llama env LLAMA_CACHE=/srv/models /usr/local/bin/llama-server \
-hf ggml-org/gemma-3-1b-it-GGUF:Q4_K_M --host 127.0.0.1 --port 8080LLAMA_CACHE đặt thư mục tải xuống. Nếu không dùng tùy chọn này, file sẽ được lưu vào ~/.cache/llama.cpp dưới account đã chạy command, đây là vị trí không phù hợp cho một service có home directory sắp bị đặt thành không thể đọc. Sau đó chạy ls -lh /srv/models, vì tên file trong cache được tạo từ tên repository thay vì tên file thuần.
Đối với service, hãy tải model vào path do bạn chọn để unit file có một vị trí ổn định để trỏ tới.
sudo -u llama curl -L --output-dir /srv/models -O \
https://huggingface.co/ggml-org/gemma-3-1b-it-GGUF/resolve/main/gemma-3-1b-it-Q4_K_M.ggufDisk là giới hạn mà người dùng thường gặp đầu tiên. Đây là kích thước file được công bố của 2 model, kiểm tra vào ngày 18 August 2026.
The data behind this chart
[
{
"label": "gemma-3-1b-it Q4_K_M",
"size_gb": 0.81
},
{
"label": "gemma-3-1b-it Q8_0",
"size_gb": 1.07
},
{
"label": "gemma-3-1b-it f16",
"size_gb": 2.01
},
{
"label": "gpt-oss-20b MXFP4",
"size_gb": 12.11
}
]File 4-bit của model 1B có kích thước 0.81 GB. Cùng model đó ở dạng không quantise có kích thước 2.01 GB, nên lựa chọn format làm kích thước thay đổi hơn 2 lần. Model 20B ở MXFP4 có kích thước 12.11 GB. Kích thước này không vừa với disk trên nhiều gói entry-level, và sau đó file vẫn phải được đọc vào memory. Nếu bạn đang cân nhắc một family cụ thể, bài kiểm tra kích thước tương tự cho GLM cho thấy giá của model lớn nhanh chóng vượt quá khả năng của VPS, trong khi một model nhỏ hơn vẫn vừa.
Kiểm tra df -h trước mỗi lần tải. Nếu root filesystem đầy trong lúc truyền 12 GB, mọi thứ khác cần ghi dữ liệu, bao gồm journal, cũng sẽ lỗi.
Chạy thủ công một lần rồi kiểm tra
sudo -u llama /usr/local/bin/llama-server \
--model /srv/models/gemma-3-1b-it-Q4_K_M.gguf \
--host 127.0.0.1 --port 8080 \
--ctx-size 4096 --parallel 1 --threads 2 --no-webuiTrong một session khác, yêu cầu server cho biết nó đã sẵn sàng chưa.
curl -s http://127.0.0.1:8080/healthTrong khi file đang được nạp, bạn nhận được HTTP 503 với body sau:
{"error":{"code":503,"message":"Loading model","type":"unavailable_error"}}Khi đã sẵn sàng, body là {"status": "ok" }. Sau đó gửi một request thực tế.
curl -s http://127.0.0.1:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"local","messages":[{"role":"user","content":"Say hello in five words."}]}'Một JSON object có array choices cho biết server đang hoạt động. Field model có mặt vì các client OpenAI luôn gửi field này. Server này chỉ nạp một model, nên giá trị đó không được dùng để chọn model.
API tương thích với OpenAI và các endpoint khác trên port
POST /v1/chat/completions, POST /v1/completions và POST /v1/embeddings là các route tương thích với OpenAI, còn GET /v1/models cho biết model đã được load. GET /health là readiness check ở trên, GET /props trả về các thiết lập hiện tại của server, còn GET /metrics cung cấp các counter của Prometheus khi bạn khởi động với --metrics.
Mọi OpenAI SDK đều hoạt động sau khi bạn đặt base URL thành http://127.0.0.1:8080/v1 và truyền vào một chuỗi API key không rỗng. Không có thành phần nào kiểm tra key đó cho đến khi bạn tự đặt --api-key.
Đừng dùng thông lượng do người khác công bố làm số liệu cho kế hoạch của bạn. Tốc độ inference bằng CPU phụ thuộc vào số core, băng thông memory và các tenant dùng chung host với bạn. Vì vậy, hãy đo số token mỗi giây trên máy của bạn và coi kết quả đó là số liệu thực tế. Steal time từ tenant gây nhiễu xuất hiện ở đây dưới dạng tốc độ sinh output thay đổi theo từng giờ.
Giữ dịch vụ trên 127.0.0.1 và đặt proxy phía trước
--host mặc định đã bind vào 127.0.0.1, nên server không thể truy cập từ bên ngoài cho đến khi bạn thay đổi giá trị này. Hãy giữ nguyên. llama-server không có mô hình người dùng, rate limit hay audit log hữu ích. Control tích hợp duy nhất là --api-key, chỉ so sánh một chuỗi. Một inference port mở sẽ cung cấp tài nguyên tính toán miễn phí cho bất kỳ ai tìm thấy nó. Lỗi tương tự khi cấu hình Ollama cũng có cùng dạng: khóa bảo vệ model API tự host áp dụng nguyên tắc tương tự cho từng dòng cấu hình.
Thực hiện TLS termination (transport layer security) trong nginx và proxy đến loopback port.
server {
listen 443 ssl;
server_name llm.example.com;
location /v1/ {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
proxy_read_timeout 600s;
}
}proxy_buffering off là bắt buộc cho streaming. Khi bật buffering, nginx giữ các server-sent events (SSE) cho đến 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. proxy_read_timeout 600s xử lý các lần generate kéo dài, vì giá trị mặc định 60 giây sẽ biến câu trả lời chậm thành 504 Gateway Time-out. Lấy certificate bằng Certbot và Let's Encrypt trên nginx.
Unit systemd
Viết /etc/systemd/system/llama-server.service.
[Unit]
Description=llama.cpp server
After=network-online.target
Wants=network-online.target
[Service]
User=llama
Group=llama
Environment=LLAMA_ARG_MODEL=/srv/models/gemma-3-1b-it-Q4_K_M.gguf
Environment=LLAMA_ARG_HOST=127.0.0.1
Environment=LLAMA_ARG_PORT=8080
Environment=LLAMA_ARG_CTX_SIZE=4096
Environment=LLAMA_ARG_N_PARALLEL=1
Environment=LLAMA_ARG_THREADS=2
ExecStart=/usr/local/bin/llama-server --no-webui
Restart=on-failure
RestartSec=5
TimeoutStopSec=30
MemoryHigh=3G
MemoryMax=3500M
OOMPolicy=stop
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
[Install]
WantedBy=multi-user.targetCác thiết lập nằm trong các dòng Environment= vì llama-server đọc các biến LLAMA_ARG_* cho hầu hết các flag, còn đối số dòng lệnh sẽ ghi đè biến tương ứng. Nhờ đó, bạn chỉ cần thay đổi kích thước context ở một chỗ, đồng thời ExecStart vẫn đủ ngắn để xem nhanh.
ProtectSystem=strict khiến toàn bộ filesystem ở chế độ chỉ đọc đối với unit này. Điều đó phù hợp vì server chỉ đọc model. Thêm ReadWritePaths=/srv/models nếu bạn muốn service tự tải model bằng -hf. ProtectHome=yes ẩn /home và /root. Đây là lý do thứ hai để lưu model trong /srv: khi bật ProtectHome, đường dẫn ~/.cache/llama.cpp mặc định hoàn toàn không hiển thị với process.
sudo systemctl daemon-reload
sudo systemctl enable --now llama-server
systemctl status llama-server
curl -s http://127.0.0.1:8080/health
journalctl -u llama-server -n 50 --no-pagerenable --now là phần nhiều người bỏ qua. Nếu không có enable, server sẽ biến mất sau lần reboot tiếp theo. Nếu muốn lập lịch công việc liên quan đến service, chẳng hạn kiểm tra bản release mới mỗi đêm, systemd service kèm timer là cơ chế phù hợp.
Xác định cách xử lý OOM trước khi xảy ra
Mức sử dụng bộ nhớ có 2 phần và chúng hoạt động khác nhau khi chịu giới hạn. Theo mặc định, file model được memory-map nên các page của nó được hỗ trợ bởi file: kernel có thể loại bỏ chúng rồi đọc lại từ disk. KV cache, tức state theo từng token mà server giữ cho mỗi conversation đang hoạt động, là anonymous memory. Kernel không thể loại bỏ phần này, nên đây là phần khiến process bị kill.
Đó là lý do 2 giới hạn trong unit đảm nhiệm 2 vai trò khác nhau. MemoryHigh=3G là soft limit: khi vượt quá giới hạn này, kernel tạo áp lực reclaim lên cgroup, vì vậy các page của model đã được map sẽ bị loại bỏ và được đọc lại từ disk ở token tiếp theo. Service vẫn tiếp tục chạy nhưng chậm hơn. MemoryMax=3500M là hard limit: khi vượt quá giới hạn này, process bị kill và journal ghi rõ điều đó.
llama-server.service: A process of this unit has been killed by the OOM killer.Tự đặt --ctx-size. Giá trị mặc định là 0, tức context mà model được train với, và trên model có context dài hiện đại, giá trị này sẽ cấp phát một KV cache rất lớn ngay khi startup. Khi đó service sẽ chết trước khi xử lý được một request nào. --parallel nhân cùng mức chi phí này, vì mỗi slot giữ state conversation riêng, nên hãy để ở mức 1 cho đến khi bạn biết mình cần chạy đồng thời.
Với Restart=on-failure, service bị kill sẽ tự khởi động lại. Nếu service bị kill ở mọi lần start, systemd sẽ dừng thử sau số lần giới hạn và systemctl status in ra start request repeated too quickly. Đây là hành vi đúng: restart loop liên tục đọc lại một file 12 GB mỗi 5 giây còn tệ hơn việc service ngừng hoạt động. Hãy sửa limit hoặc context size, sau đó xóa state bằng sudo systemctl reset-failed llama-server.
Theo dõi giá trị thực tế bằng systemctl show llama-server -p MemoryCurrent trong khi một request đang chạy. Giới hạn bộ nhớ và CPU của process bằng systemd trình bày chi tiết hơn về các directive này.
Tránh dùng swap cho workload này. Model bị swap ra sẽ biến mỗi token thành các lần đọc disk tại offset ngẫu nhiên. Memory-map file model tạo hiệu ứng tương tự nhưng ít gây hại hơn, vì kernel đọc trực tiếp các page cần thiết từ file.
Ollama là lựa chọn phù hợp hơn
Đây là điểm cần chọn hướng. Chọn llama-server khi bạn muốn một process duy nhất với các flag do bạn đặt, một bản build đã pin và một file do bạn chọn. Không có gì thay đổi bên dưới vì không có process nào khác đang chạy.
Chọn Ollama khi bạn muốn quản lý model: pull model theo tên, lưu nhiều model trên disk, unload model đang idle và upgrade bằng một command duy nhất thay vì rebuild. Đây là những việc thực tế mà nếu không dùng Ollama, bạn phải tự viết script. Chạy Ollama trên VPS thực hiện cùng công việc này nhưng đánh đổi theo hướng ngược lại. Cả hai đều cung cấp API tương thích với OpenAI, nên code phía client vẫn hoạt động khi chuyển đổi theo bất kỳ hướng nào.
Nâng cấp bản build được pin
Thay bNNNNN bằng tag mà bạn muốn chuyển sang.
cd llama.cpp
git fetch --tags
git checkout bNNNNN
cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DLLAMA_BUILD_TESTS=OFF -DLLAMA_BUILD_EXAMPLES=OFF
cmake --build build --config Release -t llama-server -j 2
sudo install -m 755 build/bin/llama-server /usr/local/bin/llama-server-bNNNNN
sudo ln -sfn /usr/local/bin/llama-server-bNNNNN /usr/local/bin/llama-server
sudo systemctl restart llama-serverBinary cũ vẫn còn trên disk, nên rollback chỉ cần một ln -sfn để quay lại llama-server-b10488 rồi restart. Đọc release notes trước khi chuyển phiên bản. Các file GGUF được version hóa và file cũ vẫn tiếp tục được load, nhưng các flag có thể được đổi tên: --mlock và --no-mmap đã deprecated để thay bằng --load-mode, và unit file truyền một flag đã bị xóa sẽ fail khi start với thông báo unrecognised argument.
Các lỗi thường gặp và các chuỗi bạn sẽ thấy
error while loading shared libraries: libllama.so sau khi bạn copy binary sang một vị trí khác. Bản build mặc định tạo các shared library bên cạnh binary. Build lại với -DBUILD_SHARED_LIBS=OFF, hoặc copy toàn bộ thư mục build/bin.
Illegal instruction (core dumped) khi khởi động hoặc ở request đầu tiên. Binary được compile với GGML_NATIVE, cho một CPU khác với CPU đang chạy binary đó. Build lại trên máy này, hoặc cấu hình bằng -DGGML_NATIVE=OFF.
c++: fatal error: Killed signal terminated program cc1plus trong lúc build. Compiler bị kill vì sử dụng quá nhiều memory. Giảm -j, hoặc thêm swap trong lúc build rồi xóa swap sau đó.
curl: (7) Failed to connect ... Connection refused từ laptop của bạn. Đây là hành vi đúng: server listen trên địa chỉ loopback của VPS. Test trực tiếp trên VPS, hoặc mở tunnel bằng ssh -L 8080:127.0.0.1:8080 user@your-vps rồi dùng http://127.0.0.1:8080 trên máy local.
HTTP 503 với "message":"Loading model" trong vài giây hoặc vài phút đầu sau khi restart. Việc đọc một file có dung lượng vài GB cần thời gian, còn systemd báo unit là active ngay khi process bắt đầu, trước khi model được nạp vào memory.
Request bị treo rồi trả về 504 Gateway Time-out. Proxy đã timeout trước khi model xử lý xong. Tăng proxy_read_timeout và tắt proxy_buffering để token được gửi đến client ngay khi được tạo.
Unit liên tục flap rồi dừng với start request repeated too quickly. Có tiến trình nào đó kill unit trong mỗi lần khởi động. Kiểm tra journalctl -u llama-server để tìm dòng OOM killer, sau đó giảm --ctx-size, giảm --parallel hoặc tăng MemoryMax.
FAQ
Tôi nên chạy server của llama.cpp hay Ollama trên VPS?
Chạy llama-server khi bạn muốn cố định chính xác một build, truyền các flag cụ thể và giữ một model trong một file mà không có thành phần nào tự ý cập nhật. Chạy Ollama khi bạn muốn quản lý model và nâng cấp bằng một lệnh, vì việc pull model theo tên, giữ nhiều model trên disk và unload các model không hoạt động là những việc bạn sẽ phải tự viết script nếu không dùng Ollama. Cả hai đều cung cấp API tương thích với OpenAI, nên code của client không cần thay đổi nếu sau này bạn chuyển đổi.
Tôi nên cố định phiên bản llama.cpp nào?
Dùng bất kỳ tag nào mà bạn đã build và test thực tế. llama.cpp tạo tag cho gần như mọi merge, với tên là các build number như b10488; đây là tag mới nhất vào ngày 18 August 2026. Không có stable branch riêng, nên "current" thay đổi vài lần mỗi ngày. Clone bằng --branch <tag>, cài binary với filename chứa tag đó và trỏ một symlink vào file này, để việc upgrade và rollback mỗi việc chỉ cần một lệnh.
llama-server cần bao nhiêu RAM?
Bắt đầu từ kích thước của file GGUF, sau đó cộng thêm KV cache. KV cache tăng theo --ctx-size và số lượng slot --parallel. Các số liệu được công bố không thay thế cho việc đo trên setup của bạn, vì tổng mức sử dụng phụ thuộc vào model, quantisation và context bạn cho phép. Chạy systemctl show llama-server -p MemoryCurrent khi đang có request và dùng con số hiển thị.
Vì sao /health trả về 503 với "Loading model"?
Process đã khởi động nhưng file model chưa được nạp vào memory, nên server trả về {"error":{"code":503,"message":"Loading model","type":"unavailable_error"}}. Đây là trạng thái bình thường sau mỗi lần restart và kéo dài trong thời gian cần để đọc file. Nó chỉ trở thành vấn đề khi client hoặc proxy coi 503 đầu tiên đó là lỗi nghiêm trọng. Poll /health cho đến khi lệnh trả về {"status": "ok" }.
Tôi có thể expose llama-server trực tiếp ra Internet không?
Không bind nó vào 0.0.0.0 rồi mở port. Nó không có account, không có rate limiting và không có request log đủ để audit. Kiểm tra tích hợp duy nhất là --api-key, chỉ so sánh một string. Giữ bind mặc định 127.0.0.1, đặt nginx phía trước với TLS và cũng thiết lập --api-key, để một lỗi trong cấu hình proxy không khiến model bị mở cho mọi người.