Cách chạy llama.cpp server trên VPS bằng systemd
Build llama-server từ tag cố định, chạy model GGUF qua OpenAI-compatible API, bind localhost và quản lý bằng systemd với giới hạn memory.
Những gì bạn sẽ triển khai
Chạy server llama.cpp trên VPS có nghĩa là dùng một binary duy nhất, llama-server, để load một file model GGUF và xử lý các HTTP request thông 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à công việc vận hành: cố định version, chỉ cho phép port bind trên localhost, viết systemd unit và quyết định cách xử lý 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 2 lựa chọn rõ ràng, trước tiên hãy đọc so sánh ưu và nhược điểm giữa Ollama và llama.cpp, vì phần so sánh đó cố ý không trình bày các bước thực hiện này.
Chọn một tag release và ghi lại
llama.cpp gắn tag release cho gần như mọi lần merge, vì vậy các tag chính là số build. b10488 là bản mới nhất tính đến ngày 18 August 2026. Không có một stable branch được 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 được build sẵn. Với VPS x86 chỉ dùng CPU, đó 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. Các binary này được link với C library của image đã build chúng. Vì vậy, trên một distribution cũ hơn, chúng sẽ fail ngay 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 một VPS nhỏ và loại bỏ toàn bộ nhóm lỗi này. Vì vậy, phần dưới đây sử dụng cách đó.
Xây dựng 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 đúng tag đó và không lấy thêm nội dung nào khác, nên mã nguồn không thể thay đổi ngoài dự kiến trong khi 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, bước configure sẽ fail.
-DBUILD_SHARED_LIBS=OFF tạo ra 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ỉ sao chép executable đến /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 biên dịch các tool khác và test. Trên 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à lựa chọn có chủ đích. Mỗi job compile song song giữ một working set riêng. Vì vậy, -j $(nproc) trên gói cấu hình nhỏ sẽ dẫn đến c++: fatal error: Killed signal terminated program cc1plus, tứ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.
Bạn có thể muốn thay đổi một flag: GGML_NATIVE mặc định bật, nên compiler nhắm đến đú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 sao chép binary sang host khác, hãy thêm -DGGML_NATIVE=OFF, vì binary sử dụng instruction mà CPU kia không có sẽ dừng với Illegal instruction (core dumped) ngay ở lần inference đầu tiên.
Cài đặt binary với 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ữ build number trong filename và trỏ một symlink đến file đó giúp việc upgrade chỉ cần một ln -sfn và một lần restart. Rollback cũng dùng cùng command này với build number cũ.
Tải model GGUF và kiểm tra disk trước
GGUF là format một file duy nhất mà llama.cpp load. Một file chứa weights, tokeniser và metadata, nên không cần cài thêm thành phần nào. 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 model trước khi tải bất kỳ thứ gì.
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ự fetch 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 có 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 service có home directory mà bạn sắp làm cho không thể đọc được. Chạy ls -lh /srv/models sau đó, vì tên file trong cache được suy ra từ tên repository thay vì tên file thuần.
Với service, hãy tải model vào path bạn đã chọn để unit file có một đường dẫn ổ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, được 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 đó nhưng không quantise có kích thước 2.01 GB, nên lựa chọn format làm thay đổi dung lượng hơn 2 lần. Model 20B ở MXFP4 có kích thước 12.11 GB. Dung lượng này không phù hợp với disk của nhiều gói entry-level, và sau đó file vẫn phải được đọc vào memory.
Kiểm tra df -h trước mỗi lần tải xuống. Nếu root filesystem đầy trong lúc đang transfer 12 GB, mọi thành phần khác cần ghi dữ liệu cũng sẽ bị lỗi, bao gồm journal.
Chạy một lần thủ công 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, hỏi server xem 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 HTTP 503 với nội dung sau:
{"error":{"code":503,"message":"Loading model","type":"unavailable_error"}}Khi server sẵn sàng, nội dung trả về là {"status": "ok" }. Sau đó gửi một request thậ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. Trường model có mặt vì các OpenAI client luôn gửi trường 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 cổng này
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 nạp. GET /health là kiểm tra trạng thái sẵn sàng ở 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 throughput do người khác công bố làm cơ sở cho kế hoạch của bạn. Tốc độ inference trên 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 chính máy của bạn và xem 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 thay đổi theo từng giờ.
Giữ trên 127.0.0.1 và đặt proxy phía trước
--host mặc định đã bind vào 127.0.0.1, vì vậy server không thể truy cập từ bên ngoài cho đến khi bạn thay đổi cấu hình này. Hãy giữ nguyên. llama-server không có user model, rate limit hay audit log hữu ích. Cơ chế kiểm soát duy nhất được tích hợp 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ó. Cùng một lỗi khi dùng Ollama cũng có dạng tương tự: khóa bảo vệ model API tự host áp dụng ở đây theo đúng từng dòng.
Thực hiện TLS termination 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 để streaming hoạt động. 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 bao phủ các lần sinh nội dung dài, vì giá trị mặc định 60 giây sẽ biến một 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 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 giữ cho ExecStart đủ ngắn để đọc nhanh.
ProtectSystem=strict làm cho 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 muốn chính service 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 mà nhiều người bỏ qua. Nếu thiếu enable, server sẽ biến mất sau lần reboot tiếp theo. Nếu muốn chạy công việc theo lịch quanh 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 OOM xảy ra
Mức sử dụng memory có 2 phần và chúng hoạt động khác nhau khi bị giới hạn. Theo mặc định, file model được memory-map, nên các page của nó được backed 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 lưu cho mỗi conversation đang hoạt động, là anonymous memory. Nó không thể bị loại bỏ, nên đây là phần khiến process bị kill.
Đó là lý do 2 limit trong unit đảm nhiệm 2 việc khác nhau. MemoryHigh=3G là soft limit: khi vượt quá mức này, kernel tạo reclaim pressure lên cgroup, nên các page của model đã được map sẽ bị evict và được đọc lại từ disk ở token tiếp theo. Service vẫn tiếp tục hoạt động nhưng chậm hơn. MemoryMax=3500M là hard limit: khi vượt quá mức 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, nghĩa là context mà model được train, và với 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 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ẽ bỏ cuộc và systemctl status in ra start request repeated too quickly. Đây là hành vi đúng: một restart loop liên tục đọc lại 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, rồi 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 request đang chạy. Giới hạn memory 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-out 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 hiệu quả 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.
Khi 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 với các flag tự đặ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 cầ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ì build lại. Đâ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 là cùng một bài toán, nhưng chọn 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 build đã 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 thực hiệ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. File GGUF có version và các 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, còn unit file truyền một flag đã bị xóa sẽ fail khi start với thông báo unrecognised argument.
Các trường hợp lỗi và chuỗi bạn sẽ thấy
error while loading shared libraries: libllama.so sau khi bạn copy binary đến một vị trí khác. Bản build mặc định tạo thêm các shared library cùng với 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, dành cho CPU khác với CPU đang chạy binary. Build lại trên máy này, hoặc configure với -DGGML_NATIVE=OFF.
c++: fatal error: Killed signal terminated program cc1plus trong quá trình build. Compiler bị kill vì dùng quá nhiều memory. Giảm -j, hoặc thêm swap cho quá trình 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 ngay 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 ở 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. Đọc một file dung lượng vài gigabyte cần thời gian, còn systemd báo unit active ngay khi process bắt đầu, trước thời điểm model được nạp vào memory khá lâu.
Request bị treo rồi trả về 504 Gateway Time-out. Proxy đã dừng chờ trước khi model hoàn tất. Tăng proxy_read_timeout và tắt proxy_buffering để token được gửi đến client ngay khi chúng được tạo ra.
Unit liên tục flap rồi dừng với start request repeated too quickly. Có process kill unit ở mỗi lần start. 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 build và các flag, đồng thời giữ một model trong một file mà không có tiến trình nào tự ý cập nhật phía sau. 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 model không hoạt động là những việc bạn sẽ phải 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 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 đã thực sự build và test. llama.cpp gắn tag cho gần như mọi merge, với tên là các build number như b10488, đây là bản mới nhất vào ngày 18 August 2026. Không có stable branch riêng, nên “current” thay đổi nhiều lần mỗi ngày. Clone bằng --branch <tag>, cài binary với tên file chứa tag đó, rồi trỏ một symlink vào file này. Nhờ vậy, nâng cấp và rollback đều 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 chính setup của bạn, vì tổng mức sử dụng phụ thuộc vào model, quantisation và context mà bạn cho phép. Chạy systemctl show llama-server -p MemoryCurrent khi có request đang được xử lý và dùng con số bạn thấy.
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à hành vi bình thường sau mỗi lần restart và kéo dài trong thời gian đọc file. Nó chỉ trở thành vấn đề khi client hoặc proxy coi 503 đầu tiên này là lỗi nghiêm trọng. Poll /health cho đến khi nó 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, rate limiting hoặc request log đủ để audit, và check duy nhất được tích hợp sẵn 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 đặt --api-key, để một lỗi trong cấu hình proxy không khiến model bị mở cho tất cả mọi người.