Chạy Ollama rootless bằng Podman trên VPS
Hướng dẫn chạy Ollama rootless trên VPS với user riêng, lingering, Quadlet tự khởi động sau reboot, SELinux label và API chỉ mở qua SSH tunnel.
Chạy Ollama trong Podman rootless trên VPS
Để chạy Ollama trong Podman rootless trên server, cần đáp ứng 5 điều mà một hướng dẫn trên desktop có thể bỏ qua. Một user không có đặc quyền riêng sẽ sở hữu container. Phải bật tính năng lingering cho user đó để container tiếp tục chạy sau khi bạn đăng xuất. File Quadlet sẽ giao container cho systemd quản lý để container tự chạy lại sau reboot. Thư mục model phải có SELinux label trên các distribution enforce SELinux. API chỉ lắng nghe trên loopback, và bạn truy cập API qua một SSH (secure shell) tunnel.
Ollama là server dành cho các large language model (LLM). Ollama lưu model weight trên disk, nạp chúng vào memory và xử lý HTTP request trên port 11434. Ollama không có login, API key hoặc user account, nên network là lớp kiểm soát truy cập duy nhất bạn có. Podman chạy container mà không cần daemon và không cần root, vì vậy mọi tiến trình thoát được khỏi container ban đầu chỉ chạy với tư cách một user không có đặc quyền. Nếu muốn xem phần so sánh runtime trước, hãy đọc Podman và Docker khác nhau trên VPS như thế nào. Nếu muốn bỏ qua container hoàn toàn, cài đặt Ollama trực tiếp trên VPS là cách ngắn hơn.
SSD Nodes cung cấp Fedora trong danh sách image, và Fedora mặc định có cả Podman lẫn SELinux (security-enhanced Linux). Mọi command dưới đây đều chạy được trên bất kỳ distribution nào có Podman 5 hoặc mới hơn.
Vì sao phiên bản cho laptop cần thay đổi khi chạy trên server
Fedora Magazine đã đăng một hướng dẫn rõ ràng về stack này vào ngày 5 August 2026: Chạy Ollama cục bộ với Podman trên Fedora Linux, do Yazan Monshed viết. Đây là tài liệu phù hợp để làm quen với các công cụ trong giờ đầu tiên. Tuy nhiên, tài liệu này nhắm đến laptop, và bốn lựa chọn trong đó hoạt động khác trên máy có public IP address.
- Tài liệu khởi động container bằng
podman run -dthông thường. Container được khởi động thủ công sẽ không tự chạy lại sau reboot, vì chưa có cấu hình nào yêu cầu khởi động nó. - Tài liệu dùng tag thay đổi theo thời gian
ollama/ollama. Trên laptop, bạn nhận ra ngay ngày hành vi thay đổi. Trên server, dấu hiệu đầu tiên có thể là một script đột nhiên ngừng hoạt động qua đêm. - Tài liệu publish bằng
-p 11434:11434, khiến dịch vụ bind trên mọi interface. Sau home router, dịch vụ này không thể truy cập từ Internet. Trên VPS, đây là một inference API public không có password. - Tài liệu chạy container bằng user đăng nhập của bạn. Trên server, account sở hữu container không nên sở hữu thêm tài nguyên nào khác, để nếu có break-out thì kẻ tấn công chỉ rơi vào một home directory trống.
Không lựa chọn nào trong số đó là sai đối với máy mà tài liệu hướng đến. Bạn chỉ cần xem xét lại từng lựa chọn khi server có thể truy cập từ mọi nơi và không có ai ngồi trực tiếp trước máy.
Tạo user không có quyền đặc biệt và kiểm tra subuid
Rootless Podman ánh xạ user ID (UID) nội bộ của container vào một dải ID chưa được sử dụng trên host. Dải này được khai báo trong /etc/subuid và /etc/subgid. Nếu không có dải này, container rootless không thể khởi động.
sudo dnf install -y podman # or: sudo apt install -y podman
sudo useradd --create-home --shell /bin/bash --comment "Ollama container owner" ollama
sudo passwd --lock ollama
grep ollama /etc/subuid /etc/subgidgrep phải in ra 2 dòng, mỗi file 1 dòng. Mỗi dòng phải chỉ rõ một dải gồm 65536 ID:
/etc/subuid:ollama:100000:65536
/etc/subgid:ollama:100000:65536Số bắt đầu của bạn sẽ khác, điều đó không sao. Nếu grep không in ra gì, useradd chưa cấp dải ID, và lệnh podman đầu tiên chạy bằng user đó sẽ fail như sau:
Error: cannot find UID/GID for user ollama: no subuid ranges found for user "ollama" in /etc/subuidGán một dải mà chưa user nào khác sử dụng, rồi báo cho Podman biết mapping cũ đã không còn hợp lệ:
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 ollama
sudo -iu ollama podman system migrateKhóa mật khẩu để không ai đăng nhập trực tiếp bằng ollama. Bạn truy cập account này từ admin user bằng sudo -iu ollama.
Bật lingering để service tiếp tục chạy sau khi logout
Instance systemd của user thường khởi động khi đăng nhập và dừng khi logout, đồng thời /run/user/<uid> cũng bị xóa theo. Mọi rootless container thuộc user đó cũng dừng cùng lúc. Lingering giữ cho instance của user tiếp tục chạy mà không cần session được gắn vào.
sudo loginctl enable-linger ollama
loginctl show-user ollama --property=LingerLệnh này phải in ra Linger=yes. Hãy bật lingering trước khi tạo unit, vì thư mục mà unit cần, /run/user/<uid>, chỉ tồn tại sau khi lingering được bật.
Còn một bước nữa thường không được dự tính. sudo -iu ollama cung cấp shell nhưng không cung cấp session bus, nên systemctl --user sẽ fail ngay lập tức:
Failed to connect to bus: $DBUS_SESSION_BUS_ADDRESS and $XDG_RUNTIME_DIR not definedsystemd tìm user bus tại $XDG_RUNTIME_DIR/bus, nhưng sudo -i không thiết lập biến đó. Hãy tự thiết lập biến này trong mọi shell admin dùng để quản lý service này:
sudo -iu ollama
export XDG_RUNTIME_DIR=/run/user/$(id -u)
systemctl --user statusVị trí lưu model blob và dung lượng đĩa cần dự trù
Ollama ghi các weight vào /root/.ollama/models bên trong container. Bind một thư mục trong home của người dùng vào đường dẫn đó để các file được lưu ở vị trí có thể đo dung lượng: /home/ollama/ollama-data/models. Các blob nằm trong models/blobs dưới dạng file được định danh theo nội dung, còn models/manifests chứa index nhỏ để đặt tên cho chúng. Nếu dùng named volume như bài viết trên Fedora Magazine, cùng cây thư mục đó sẽ nằm dưới /home/ollama/.local/share/containers/storage/volumes/<volume>/_data. Dù dùng cách nào, ollama pull và ollama run đều ghi weight vào cùng một cây thư mục; điểm khác nhau giữa hai lệnh chỉ là có mở một chat session sau khi tải xong hay không.
Hãy kiểm tra dung lượng đĩa trước khi pull bất kỳ thứ gì. Kích thước tải xuống được công bố là mức tối thiểu cần có.
The data behind this chart
[
{
"label": "gemma3:4b",
"download_gb": 3.3
},
{
"label": "mistral:7b",
"download_gb": 4.4
},
{
"label": "qwen3:8b",
"download_gb": 5.2
},
{
"label": "gemma3:12b",
"download_gb": 8.1
},
{
"label": "qwen3:14b",
"download_gb": 9.3
},
{
"label": "gemma3:27b",
"download_gb": 17
},
{
"label": "qwen3:30b",
"download_gb": 19
}
]Tất cả 7 dòng đều là số liệu được công bố trên ollama.com/library, không phải dung lượng đo trực tiếp trên đĩa. Tag nhỏ nhất ở đây, gemma3:4b, tải xuống 3.3 GB. Tag lớn nhất, qwen3:30b, tải xuống 19 GB. Bản thân container image còn chiếm thêm dung lượng trong storage riêng của Podman, vì vậy hãy kiểm tra cả hai con số bằng podman system df và df -h /home. Khi được load, một model cũng cần RAM xấp xỉ kích thước file, cộng thêm dung lượng cho context window. Vì vậy, model 19 GB sẽ không chạy trên VPS có 16 GB.
Ghim tag image và dùng đầy đủ tên registry
sudo -iu ollama
export XDG_RUNTIME_DIR=/run/user/$(id -u)
mkdir -p ~/ollama-data ~/.config/containers/systemd
podman pull docker.io/ollama/ollama:0.32.9Dùng tag phiên bản đã phát hành, 0.32.9 tính đến tháng 8 năm 2026, không dùng latest. Tag cố định bảo đảm lần restart lúc 04:00 sử dụng đúng binary bạn đã kiểm thử, nên mọi thay đổi về hành vi đều là thay đổi do bạn thực hiện. Docker Hub cũng phát hành các tag -rc và -rocm cho cùng những phiên bản đó; hãy chọn tag không có hậu tố, trừ khi bạn có GPU AMD.
Ghi cả hostname của registry. Trên Fedora, short name trong systemd unit không có terminal để hiển thị prompt, nên unit sẽ fail với:
Error: short-name "ollama/ollama" did not resolve to an alias and no unqualified-search registries are definedPull thủ công trước là tùy chọn nhưng hữu ích, vì cách này đưa thao tác tải xuống vài gigabyte ra ngoài thời gian chờ start của unit.
Unit Quadlet vẫn hoạt động sau khi reboot
Quadlet là systemd generator của Podman. Bạn viết một file .container, systemd chuyển file đó thành service khi boot, và không cần podman generate systemd nữa. Lưu file này tại /home/ollama/.config/containers/systemd/ollama.container, với chủ sở hữu là user ollama.
[Unit]
Description=Ollama API (rootless)
After=network-online.target
Wants=network-online.target
[Container]
Image=docker.io/ollama/ollama:0.32.9
ContainerName=ollama
PublishPort=127.0.0.1:11434:11434
Volume=/home/ollama/ollama-data:/root/.ollama:Z
Environment=OLLAMA_KEEP_ALIVE=30m
Environment=OLLAMA_MAX_LOADED_MODELS=1
[Service]
Restart=always
TimeoutStartSec=900
[Install]
WantedBy=default.targetTên file quyết định tên service, vì vậy ollama.container trở thành ollama.service.
systemctl --user daemon-reload
systemctl --user start ollama.service
systemctl --user status ollama.servicestatus phải hiển thị active (running). Không chạy systemctl --user enable ollama.service. Unit này không tồn tại dưới dạng file trên disk, nên systemd từ chối:
Failed to enable unit: Unit file /run/user/1001/systemd/generator/ollama.service is transient or generated.Section [Install] đã thực hiện công việc đó. Quadlet tự tạo link start-at-boot trong lúc daemon-reload, vì vậy lệnh đó là bắt buộc. TimeoutStartSec=900 đặt thời gian cho lần start đầu tiên, khi hệ thống vẫn phải pull image; 90 giây mặc định không đủ cho một lượt download 2-gigabyte, nên systemd sẽ kill tiến trình start và đánh dấu là failed. OLLAMA_KEEP_ALIVE=30m giữ model trong memory giữa các request thay vì unload model sau năm phút; các đánh đổi được trình bày trong giữ model Ollama trong memory. Nếu có thuật ngữ systemd nào trong phần này còn mới, cách systemd service và timer hoạt động trên VPS giải thích về chính các unit đó.
Vì sao thư mục model trả về lỗi permission denied dưới SELinux
Trên Fedora, RHEL, Rocky và AlmaLinux, SELinux được bật chế độ enforcing theo mặc định. Tiến trình container chạy trong domain container_t, còn thư mục trong home của người dùng được gắn nhãn user_home_t. Policy không cho phép hai bên truy cập lẫn nhau, nên Ollama không thể tạo cây thư mục model và container thoát. getenforce in ra Enforcing trên các hệ thống này, và denial được ghi lại:
sudo ausearch -m avc -ts recentBạn sẽ thấy một dòng nêu domain và nhãn đích:
avc: denied { write } for pid=1842 comm="ollama" name="models" dev="vda1" ino=131077 scontext=system_u:system_r:container_t:s0:c214,c827 tcontext=unconfined_u:object_r:user_home_t:s0 tclass=dir permlisted=0:Z ở cuối dòng Volume= là cách khắc phục. Lệnh này gắn lại nhãn cho thư mục trên host thành container_file_t và gắn thêm một category MCS (multi-category security) riêng mà chỉ container này có. Chữ thường :z dùng shared label thay thế. Đây là lựa chọn phù hợp khi hai container cùng đọc một thư mục.
Cần đặc biệt lưu ý với :Z vì lệnh này có tính phá hủy và không hiện thông báo. Thao tác gắn nhãn sẽ áp dụng đệ quy. Nếu trỏ lệnh vào /home/ollama, mọi file trong thư mục home đó sẽ bị gắn lại nhãn, làm hỏng quyền truy cập SSH key của người dùng đó. Luôn cung cấp cho :Z một thư mục con riêng và không chứa dữ liệu nào khác. Named volume không cần bước này vì Podman tự gắn nhãn đúng khi tạo volume. Nếu cần xem tổng quan, kiến thức cơ bản về SELinux cho server giải thích context và boolean. Ubuntu và Debian dùng AppArmor thay thế, :Z không làm gì trên các hệ thống đó, và để lệnh này trong unit cũng không gây hại.
Đóng cổng 11434 và truy cập API qua SSH
PublishPort=127.0.0.1:11434:11434 bind phía host vào loopback. Xác nhận lại:
ss -ltnp | grep 11434
curl http://127.0.0.1:11434Output của ss phải hiển thị 127.0.0.1:11434. 0.0.0.0:11434 hoặc *:11434 nghĩa là cổng đang mở ra Internet, và curl phải phản hồi Ollama is running.
Hãy xác định chính xác phía nào đang được bind. Địa chỉ trong PublishPort là địa chỉ của host. Bên trong container, Ollama phải tiếp tục lắng nghe trên mọi interface; đây là giá trị mặc định của image. Đặt Environment=OLLAMA_HOST=127.0.0.1 sẽ bind Ollama vào loopback riêng của container. Khi đó, Podman chuyển tiếp traffic đã publish đến địa chỉ mạng của container, nên mọi request đều bị từ chối, kể cả request từ host.
Cổng 11434 mở ra Internet gây rủi ro theo 2 cách. Ollama không có cơ chế xác thực, nên bất kỳ ai truy cập được cổng này đều có thể liệt kê model của bạn qua /api/tags, chạy inference trên CPU và hạn mức bandwidth của bạn qua /api/generate, tải model mới vào disk và xóa các model hiện có. Thứ hai, HTTP thuần đến một cổng từ xa gửi prompt và completion dưới dạng plain text, nên mọi máy trên đường truyền đều có thể đọc chúng. Cả 2 vấn đề này đều biến mất nếu cổng không bao giờ rời khỏi máy.
Từ workstation, forward cổng qua SSH:
ssh -N -L 11434:127.0.0.1:11434 you@vps.example.comBây giờ http://127.0.0.1:11434 trên laptop của bạn chính là Ollama trên server, nằm bên trong lớp mã hóa của phiên SSH. Nếu laptop đã chạy Ollama, local bind sẽ fail với bind [127.0.0.1]:11434: Address already in use; hãy dùng -L 11435:127.0.0.1:11434 rồi trỏ client đến 11435.
Khi browser client cần truy cập, hãy đặt một reverse proxy có password ở phía trước. Một Caddy site block chỉ gồm 4 dòng, và caddy hash-password sẽ in ra bcrypt hash mà Caddy yêu cầu:
ollama.example.com {
basic_auth {
you $2a$14$replace_with_the_generated_hash
}
reverse_proxy 127.0.0.1:11434
}Caddy tự lấy certificate qua TLS (transport layer security), nên traffic được mã hóa. Trước tiên hãy test client: nhiều tool giao tiếp với Ollama không có trường cho header Authorization, và sẽ fail với basic auth nếu chỉ nhận được 401 Unauthorized. SSH tunnel không gặp vấn đề này, nên đây là khuyến nghị mặc định trong hướng dẫn này.
Tải model và kiểm tra toàn bộ đường đi
podman exec -it ollama ollama pull gemma3:4b
curl -s http://127.0.0.1:11434/api/tags
curl -s http://127.0.0.1:11434/api/generate -d '{"model":"gemma3:4b","prompt":"Reply with the single word: ready","stream":false}'
du -sh ~/ollama-data/models/api/tags trả về JSON liệt kê gemma3:4b. /api/generate trả về một JSON object có field response sau một khoảng dừng để nạp weights từ disk. du phải báo một con số gần với kích thước download được công bố. Sau đó, hãy xác nhận phần mà toàn bộ hướng dẫn này tập trung vào:
sudo reboot
# reconnect, then:
sudo -iu ollama
export XDG_RUNTIME_DIR=/run/user/$(id -u)
systemctl --user is-active ollama.serviceactive nghĩa là model vẫn đang được giữ trong bộ nhớ; section [Install] và daemon-reload đều đã hoạt động đúng. inactive nghĩa là một trong ba phần này còn thiếu.
Các dạng lỗi và chuỗi bạn sẽ thấy
Container biến mất sau khi reboot. Trước tiên hãy kiểm tra loginctl show-user ollama --property=Linger, vì nếu không có Linger=yes thì instance systemd của người dùng sẽ không bao giờ khởi động khi boot. Nếu chế độ lingering đang bật, phần [Install] bị thiếu trong file .container, hoặc bạn đã sửa file nhưng chưa chạy systemctl --user daemon-reload.
Error: statfs /home/ollama/ollama-data: no such file or directory. Source của bind mount phải tồn tại trước khi container khởi động. Podman không tự tạo các thư mục trên host. Chạy mkdir -p ~/ollama-data với user ollama.
Start thất bại sau 90 giây. journalctl --user -u ollama.service hiển thị Start operation timed out. Terminating. vì quá trình pull image vẫn đang chạy. Hãy pull thủ công hoặc giữ TimeoutStartSec=900.
Container khởi động rồi thoát. podman logs ollama và sudo ausearch -m avc -ts recent khi xem cùng nhau sẽ cho biết lỗi có phải do SELinux label hay không. AVC có tên container_t và user_home_t nghĩa là đang thiếu :Z.
Host từ chối request. Chạy curl: (7) Failed to connect to 127.0.0.1 port 11434: Connection refused với service active thường cho thấy OLLAMA_HOST đã được đặt thành địa chỉ loopback bên trong container. Hãy xóa dòng đó.
Quá trình generation rất chậm hoặc container bị kill. Khi không có GPU, inference chạy trên CPU nên model lớn vốn đã chậm. Nếu container chết giữa request và log có signal: killed, đó là kernel out-of-memory killer. Hãy chọn tag nhỏ hơn trong chart ở trên.
Cập nhật image đã pin
Pinning nghĩa là bạn chủ động thực hiện việc cập nhật, thay vì để cập nhật tự động xảy ra. Sửa Image= trong ollama.container, sau đó reload và restart:
systemctl --user daemon-reload
systemctl --user restart ollama.service
podman exec ollama ollama --versionCác model nằm trong bind mount, nên vẫn nguyên vẹn khi image thay đổi. AutoUpdate=registry trong phần [Container] dành cho trường hợp chạy moving tag. Tùy chọn này không có tác dụng hữu ích khi dùng cùng fixed version tag, vì nội dung của tag đó không thay đổi. Hãy sao lưu /home/ollama/ollama-data/models/manifests và file .container, đồng thời bỏ qua các blob. Chúng có kích thước lớn, và ollama pull sẽ fetch lại chúng trên máy mới.
FAQ
Tại sao container Podman rootless của tôi dừng khi tôi đăng xuất?
Instance systemd của người dùng và thư mục /run/user/<uid> của họ sẽ bị dỡ khi phiên cuối cùng của người dùng kết thúc, nên mọi rootless container cũng dừng theo. Chạy sudo loginctl enable-linger ollama và xác nhận loginctl show-user ollama --property=Linger in ra Linger=yes. Bật lingering trước khi tạo Quadlet unit, vì runtime directory mà unit cần chỉ tồn tại sau khi lingering được bật.
Tôi có cần SELinux label trên thư mục model của Ollama không?
Trên Fedora, RHEL, Rocky và AlmaLinux, có, nếu bạn bind mount một thư mục trên host. Container chạy trong domain container_t, còn thư mục trong home folder có label user_home_t, nên thao tác ghi bị từ chối và Ollama thoát. Thêm :Z vào dòng Volume= và cấp cho nó một subdirectory riêng, vì thao tác relabel sẽ áp dụng đệ quy; trỏ :Z vào toàn bộ home directory sẽ làm hỏng quyền truy cập SSH key của người dùng đó. Named volume được Podman gán label đúng và không cần cấu hình thêm.
Model Ollama cần bao nhiêu dung lượng đĩa?
Bắt đầu từ dung lượng download được công bố trên ollama.com/library. Dung lượng này dao động từ 3.3 GB cho gemma3:4b đến 19 GB cho qwen3:30b. Cộng thêm dung lượng Podman image, rồi chừa phần dự phòng, vì model thứ hai không thay thế model đầu tiên trên đĩa. Kiểm tra df -h /home trước khi pull và du -sh ~/ollama-data/models sau đó. Tính RAM theo cách tương tự: khi được load, model cần lượng bộ nhớ gần bằng kích thước file, cộng thêm context window.
Có an toàn khi mở port 11434 trên VPS không?
Không. Ollama không có sẵn bất kỳ cơ chế authentication nào, nên bất kỳ ai truy cập được port này đều có thể liệt kê model, xóa model, pull model mới vào đĩa của bạn, đồng thời chạy inference bằng CPU và băng thông của bạn. HTTP thuần qua Internet cũng gửi mọi prompt và completion dưới dạng plaintext. Bind phía host vào 127.0.0.1 bằng PublishPort=127.0.0.1:11434:11434, xác nhận bằng ss -ltnp | grep 11434, rồi truy cập thông qua SSH tunnel hoặc reverse proxy có yêu cầu password.