SSD Nodes Learn 🎉 VPS từ $5.50/tháng
Hướng dẫn Matt ConnorBởi Matt Connor

Podman vs Docker trên VPS: khác nhau thực sự ở đâu

Podman không có daemon và mặc định chạy rootless. Tìm hiểu điều đó ảnh hưởng thế nào đến compose, quadlet, port dưới 1024 và quyền sở hữu volume trên VPS.

Điểm thực sự khác nhau giữa Podman và Docker

Podman và Docker đều chạy các image OCI (open container initiative) trên VPS, nên lựa chọn không phụ thuộc vào việc phần mềm nào có thể chạy. Khác biệt nằm ở mô hình tiến trình. Docker chạy một daemon với quyền root để quản lý mọi container, còn lệnh docker là một client nhỏ gửi yêu cầu cho daemon đó thực hiện công việc. Podman không có daemon: podman run khởi động container dưới dạng tiến trình con của tiến trình đã gọi lệnh, bằng user không có đặc quyền của bạn.

Mọi khác biệt khác đều bắt nguồn từ điểm này. systemd sẽ chịu trách nhiệm tự động khởi động thay cho daemon. Quyền sở hữu volume đi qua user namespace, nên owner bạn thấy bằng ls -l trên host không phải là owner mà container thấy. Các cổng dưới 1024 sẽ không bind được cho đến khi bạn thay đổi thiết lập kernel. CLI docker (command line interface) vẫn hoạt động thông qua một wrapper, cho đến khi có thành phần cần Docker socket.

Không có daemon: thực sự chạy gì khi bạn khởi động một container

Trên Docker host, pstree -a hiển thị dockerd với quyền root, containerd bên cạnh nó và một containerd-shim-runc-v2 cho mỗi container đang chạy. Ứng dụng của bạn là tiến trình con của shim đó, còn shim là tiến trình con của PID 1. Không có gì kết nối container với shell đã khởi động nó. Khi dừng daemon, bạn mất control plane của mọi container trên máy. Nếu tắt thiết lập mặc định live-restore, systemctl restart docker cũng khởi động lại các container của bạn.

Podman không có tiến trình tương đương. Khi khởi động một container, bạn nhận được một tiến trình conmon (container monitor) giữ tiến trình chính của container. Tiến trình này thuộc về user đã chạy lệnh.

podman run -d --name web -p 8080:80 docker.io/library/caddy:2
ps -o user,pid,ppid,args -C conmon
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080

ps phải liệt kê conmon đang chạy dưới user đăng nhập của bạn, không phải root, và curl phải in ra 200. Vì không có service trung tâm nào sở hữu container, sudo apt upgrade podman không dừng bất kỳ container nào đang chạy. Nếu monitor của một container bị crash, các container khác vẫn tiếp tục chạy.

Việc không có daemon cũng có cái giá của nó. Không có gì tự khởi động các container sau khi reboot. --restart=always của Docker là cam kết do daemon thực hiện khi boot. Podman thay thế cơ chế này bằng systemd. Đó là mục đích của phần quadlet bên dưới.

Socket là nửa còn lại của vấn đề. /var/run/docker.sock là endpoint API (application programming interface) thuộc root. Bất kỳ process nào có quyền ghi vào đó đều có thể khởi động một container có quyền cao và mount filesystem của host. Thêm một user vào group docker sẽ cấp cho user đó quyền root theo một con đường vòng chậm hơn. Bạn nên đọc phần này cùng với chỉ cấp cho mỗi service account đúng quyền mà nó cần. Podman không expose socket trừ khi bạn yêu cầu. Socket được tạo ra sẽ thuộc về một user duy nhất tại /run/user/<uid>/podman/podman.sock.

Cài Podman trên Ubuntu 24.04 và xác nhận rootless hoạt động đúng

sudo apt update
sudo apt install -y podman uidmap
podman --version
podman info | grep -i rootless

Gói uidmap cung cấp newuidmapnewgidmap. Đây là các setuid helper cho phép user thông thường sử dụng một dải subordinate ID. Nếu thiếu chúng, rootless container sẽ không khởi động. podman info phải in ra rootless: true.

Ubuntu 24.04 phát hành Podman 4.9, còn Debian 13 phát hành Podman 5.x, theo kiểm tra vào tháng 8 năm 2026. Khác biệt này quan trọng vì file quadlet cần phiên bản 4.4 trở lên, còn file quadlet .pod cần phiên bản 5.0. Chạy podman --version trước khi sao chép ví dụ từ tài liệu upstream.

Mỗi rootless user cần một dải subordinate ID:

grep "$USER" /etc/subuid /etc/subgid

User được tạo bằng adduser trên Ubuntu sẽ tự động có dải này. User được tạo bằng useradd -M hoặc bằng công cụ cấu hình thường không có, và lỗi sẽ cho biết điều đó:

Error: cannot find UID/GID for user deploy: no subuid ranges found for user "deploy" in /etc/subuid - check rootless mode in man pages.

Gán một dải, sau đó reset storage của user đó để sử dụng mapping mới:

sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 deploy
podman system migrate

Một bất ngờ khác trong lần chạy đầu tiên: Podman không mặc định sử dụng Docker Hub. Short image name được phân giải dựa trên unqualified-search-registries trong /etc/containers/registries.conf. Khi chạy trong script không gắn terminal, thao tác pull sẽ fail với short-name resolution enforced but cannot prompt without a TTY. Luôn ghi đầy đủ image name. Dùng docker.io/library/nginx:1.27 thay cho nginx.

Rootless container thực sự mang lại gì trên một server thuê

Rootless container chạy trong user namespace, một tính năng của kernel cho phép mỗi process có một bảng ánh xạ user ID riêng. Bên trong namespace, superuser của container là UID (user ID) 0. Bên ngoài namespace, trên VPS của bạn, process đó chỉ là user đăng nhập thông thường. Root trong container không phải là root trên host.

Đó là phạm vi lợi ích thực tế. Một image yêu cầu chạy bằng root, một web application có lỗ hổng remote code execution hoặc một cơ chế escape cần UID 0 ở bên ngoài namespace: tất cả đều chỉ có quyền của user không đặc quyền của bạn, thay vì quyền trên toàn bộ máy. Rootless không bảo vệ bạn khỏi các lỗi trong kernel. Nó cũng không bảo vệ các file của chính bạn, vì process sau khi escape vẫn chạy dưới user của bạn và có thể đọc mọi thứ mà bạn đọc được.

Docker cũng có thể chạy rootless. dockerd-rootless-setuptool.sh install thiết lập một daemon riêng cho từng user và hoạt động tốt. Điểm khác biệt nằm ở mặc định. Với Podman, rootless được bật mà không cần yêu cầu thêm. Vì vậy, lỗi đầu tiên bạn gặp sẽ là container không thể bind vào port 80, thay vì một service âm thầm chạy bằng root trong hai năm.

Vì sao các file trong volume của tôi có owner là UID 100999?

Nguyên nhân là user namespace đó. UID 0 trong container được ánh xạ thành UID của bạn trên host. UID 1 trong container được ánh xạ thành ID đầu tiên trong dải subuid của bạn, rồi tăng dần từ đó. Với dải bắt đầu từ 100000, UID 1000 trong container sẽ thành UID 100999 trên host.

mkdir -p "$PWD/data"
podman run --rm --user 1000 -v "$PWD/data:/data" docker.io/library/alpine:3 sh -c 'id -u; touch /data/f'
ls -ln "$PWD/data"

Container in ra 1000. Danh sách trên host hiển thị owner là 100999, vì 100000 cộng 1000 trừ 1 bằng 100999. Không có gì bị hỏng, và lệnh chown đơn giản cũng không sửa được việc này, vì user không có đặc quyền không thể thay đổi ownership của file bên ngoài namespace.

Có 4 cách xử lý:

  • podman unshare chown 1000:1000 "$PWD/data" chạy chown bên trong cùng user namespace, nơi các số có đúng ý nghĩa đối với container.
  • -v "$PWD/data:/data:U" yêu cầu Podman tự sửa ownership của thư mục nguồn. Hãy dùng trên thư mục mới, không dùng trên dữ liệu bạn cần giữ.
  • --userns=keep-id ánh xạ UID trên host của bạn thành cùng UID bên trong container, để các file mới tạo ra có owner là bạn.
  • Named volume như -v appdata:/data bỏ qua vấn đề này, vì Podman tạo volume bên trong storage của bạn với ownership đã đúng.

Nếu bạn từng gặp vấn đề này trong Docker, thì đây là cùng một vấn đề nhưng xảy ra thêm một lớp ánh xạ. Các biến PUID và PGID mà nhiều image cung cấp đặt UID mà process bên trong container sử dụng; với rootless Podman, UID đó lại được ánh xạ thêm một lần. PUID=1000 bên trong rootless container vẫn ghi các file trên host với owner là 100999. Hãy chọn các số có tính đến lần ánh xạ thứ hai này, hoặc chuyển dữ liệu vào named volume và không cần quan tâm đến vấn đề đó nữa.

Có thêm 2 lưu ý về mount. Các flag :z:Z thường xuất hiện trong ví dụ dành cho Fedora và RHEL là tùy chọn relabel của SELinux; Ubuntu dùng AppArmor nên các flag này không có tác dụng ở đó. Rootless Podman cũng không thể mount thư mục trên host mà user của bạn không có quyền đọc. Đây là cơ chế bảo vệ, không phải lỗi.

Vì sao Podman không chạy root lại từ chối publish cổng 80?

Vì bind một cổng nhỏ hơn 1024 cần privilege mà user của bạn không có. Thông báo lỗi cho biết cách khắc phục:

Error: rootlessport cannot expose privileged port 80, you can add 'net.ipv4.ip_unprivileged_port_start=80' to /etc/sysctl.conf (currently 1024), or choose a larger port number (>= 1024): listen tcp 0.0.0.0:80: bind: permission denied

Có 2 cách xử lý. Hạ ngưỡng cho toàn bộ host:

echo 'net.ipv4.ip_unprivileged_port_start=80' | sudo tee /etc/sysctl.d/99-podman.conf
sudo sysctl --system
sysctl net.ipv4.ip_unprivileged_port_start

Lệnh cuối cùng phải trả về 80. Cần hiểu rõ setting này làm gì: mọi user trên máy giờ có thể bind cổng 80 và 443, không chỉ user đang chạy container. Trên một VPS chỉ có một admin, đây là đánh đổi có thể chấp nhận. Trên máy có account của người khác, cách này không phù hợp. Cách còn lại là publish trên cổng 8080 rồi đặt reverse proxy phía trước. Đây cũng là nơi bạn muốn certbot cấp và gia hạn chứng chỉ trên nginx.

Việc publish ở chế độ rootless cũng thay đổi thông tin mà application nhìn thấy. Podman 4.x mặc định dùng slirp4netns với port handler rootlesskit. Các kết nối được forward sẽ có source address đã bị viết lại, nên access log ghi mọi visitor là 10.0.2.100. Podman 5.0 đổi mặc định thành pasta để giữ địa chỉ client thật. Trên 4.x, --network slirp4netns:port_handler=slirp4netns sẽ khôi phục source address thật, nhưng throughput có thể giảm.

Có một điểm đáng mừng: port được publish ở chế độ rootless là một listening socket thông thường do process không phải root sở hữu, nên các input rule của firewall vẫn áp dụng cho nó. Docker publish port bằng cách ghi các rule NAT (network address translation) cùng các rule accept cho forwarding của riêng nó. Đây chính là lý do port Docker đã publish vẫn bỏ qua rule ufw mà bạn tưởng đang chặn nó. Podman chạy root dùng cơ chế tương tự và cũng gặp bẫy này. Rootless thì không.

Các file Docker Compose của tôi còn hoạt động với Podman không?

Phần lớn là có, theo 2 cách khác nhau. Cách đầu tiên là podman-compose, một triển khai riêng đọc cùng file đó và điều khiển Podman CLI:

sudo apt install -y podman-compose
podman-compose up -d
podman ps

Cách thứ hai là dùng Docker Compose thực sự để kết nối đến API tương thích với Docker của Podman qua socket theo từng user:

systemctl --user enable --now podman.socket
export DOCKER_HOST="unix:///run/user/$(id -u)/podman/podman.sock"
docker compose up -d
podman ps

docker compose pspodman ps phải liệt kê cùng một danh sách container vì chỉ có một bộ container duy nhất. Phân giải tên cũng hoạt động: network backend mặc định của Podman là netavark chạy aardvark-dns, nên các container trong network do user định nghĩa có thể tìm thấy nhau bằng tên.

Các giới hạn vẫn tồn tại. Bất kỳ thành phần nào mount /var/run/docker.sock đều phải trỏ đến socket của Podman hoặc bị loại bỏ. network_mode: host hoạt động khác trong user namespace. depends_on cùng với condition: service_healthy được hỗ trợ không đồng đều giữa các phiên bản podman-compose. restart: always không tự hoạt động sau khi reboot; phần tiếp theo sẽ khắc phục điều đó. Compose vẫn là cách tốt để mô tả một stack nhiều container trong một file duy nhất, còn với Podman, nó đóng vai trò lớp chuyển đổi. Với stack dự định duy trì trong nhiều năm, hãy chuyển sang quadlet và chỉ duy trì một lớp trừu tượng thay vì 2 lớp.

Pod: ý tưởng mà Docker không có câu trả lời

Pod là một nhóm container dùng chung một network namespace. Podman khởi động một container infra nhỏ để giữ namespace đó luôn tồn tại, sau đó các thành viên kết nối với nhau qua 127.0.0.1 mà không cần user-defined network hay service discovery.

podman pod create --name app -p 8080:80
podman run -d --pod app --name app-cache docker.io/library/redis:7
podman run -d --pod app --name app-web docker.io/library/nginx:1.27
podman pod ps
podman ps --pod

podman pod ps sẽ hiển thị pod Running cùng 3 container, trong đó có cả infra container. Giờ đây, web container kết nối đến Redis tại 127.0.0.1:6379 thay vì tại app-cache:6379. Có 2 quy tắc xuất phát từ namespace dùng chung: publish port trên pod, không publish trên member; và không có 2 member nào được listen trên cùng một port.

Đây là mô hình của Kubernetes, và Podman hỗ trợ rất tốt mô hình này. podman kube generate app > app.yaml tạo Kubernetes manifest từ trạng thái đang chạy (các package cũ ghi là podman generate kube), còn podman kube play app.yaml tái tạo manifest đó trên host khác. Quadlet có loại unit .kube để chạy file như vậy dưới dạng systemd service. Đây là cách nhóm service thực sự khác biệt, đồng thời là lý do thuyết phục nhất để chọn Podman nếu Kubernetes có thể xuất hiện trong kế hoạch của bạn.

Tự động khởi động không cần daemon: quadlet unit

Quadlet là một systemd generator. Nó chuyển một file ngắn mô tả container thành một systemd service thực khi boot. File dành cho user rootless đặt trong ~/.config/containers/systemd/, còn file dành cho root đặt trong /etc/containers/systemd/.

~/.config/containers/systemd/caddy.container:

[Unit]
Description=Caddy web server
After=network-online.target

[Container]
Image=docker.io/library/caddy:2
ContainerName=caddy
PublishPort=8080:80
Volume=caddy-data.volume:/data
Environment=TZ=UTC
AutoUpdate=registry

[Service]
Restart=always
MemoryMax=512M

[Install]
WantedBy=default.target

~/.config/containers/systemd/caddy-data.volume có thể gần như để trống, vì section header là phần tạo volume:

[Volume]
systemctl --user daemon-reload
systemctl --user start caddy
systemctl --user status caddy
journalctl --user -u caddy -n 50

Tên service được lấy từ tên file: caddy.container trở thành caddy.service. Không chạy systemctl --user enable caddy. Generated unit không thể được enable và systemd trả về Failed to enable unit: Unit /run/user/1000/systemd/generator/caddy.service is transient or generated.. Section [Install] là phần khởi động container khi boot, còn daemon-reload sẽ tạo lại unit sau khi bạn sửa file.

Bây giờ là setting khiến gần như mọi người mắc lỗi:

sudo loginctl enable-linger deploy
loginctl show-user deploy --property=Linger

Kết quả mong đợi là Linger=yes. Nếu không bật linger, systemd sẽ dừng toàn bộ user session khi kết nối SSH cuối cùng của bạn đóng. Vì vậy mọi rootless container cũng dừng theo và không container nào tự khởi động lại khi boot. Container biến mất khi bạn logout luôn có nguyên nhân này.

Vì container là process chính của một service unit thông thường, các cơ chế điều khiển của systemd được áp dụng trực tiếp. MemoryMax=CPUQuota= trong section [Service] hoạt động chính xác như với bất kỳ service nào khác mà bạn giới hạn bằng systemd. Cơ chế này cần cgroup v2 (control group version 2). Ubuntu đã dùng cgroup v2 mặc định từ 22.04. Xác nhận bằng podman info | grep -i cgroup.

Update có cơ chế tương ứng. AutoUpdate=registry cùng với systemctl --user enable --now podman-auto-update.timer sẽ kiểm tra registry để tìm image mới hơn trên cùng tag, restart unit và rollback về image trước đó nếu container mới không khởi động được. Chạy podman auto-update --dry-run trước để xem lệnh sẽ thay đổi gì. Lệnh podman generate systemd cũ vẫn tồn tại nhưng đã deprecated, vì vậy hãy dùng quadlet cho mọi cấu hình mới.

Nơi alias docker hoạt động và nơi không hoạt động

sudo apt install -y podman-docker
sudo touch /etc/containers/nodocker
docker ps

podman-docker cài một wrapper /usr/bin/docker gọi Podman. Nếu không có file nodocker, mỗi lần gọi trước tiên sẽ in Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg. Wrapper này bao phủ các lệnh bạn dùng hằng ngày: run, ps, logs, exec, build, pull, push, inspect, cp, volume, network.

Những gì không tương thích là một danh sách ngắn hơn nhưng quan trọng hơn. Swarm mode không có phiên bản tương đương, nên không có nơi để chạy một Swarm stack. Các công cụ giao tiếp với Docker socket cần Podman socket được export, và một số công cụ vẫn nhận ra sự khác biệt này; Docker provider của Traefik hoạt động khi trỏ đến /run/user/<uid>/podman/podman.sock, còn Watchtower hoàn toàn không có vai trò tương đương, vì podman auto-update đảm nhiệm công việc đó. Storage được tách riêng, nên Podman không thể thấy các image bạn đã pull bằng Docker, và podman images trên một Docker host đang hoạt động sẽ bắt đầu ở trạng thái trống.

Di chuyển một stack đang chạy, từng bước

  1. Tạo hoặc chọn user không có quyền đặc biệt để sở hữu các container, rồi xác nhận user đó có một range trong /etc/subuid.
  2. Pull lại mọi image lấy từ registry bằng tên đầy đủ. Podman có image store riêng và sẽ không đọc image store của Docker.
  3. Chuyển các image được build cục bộ bằng docker save app:1.4 | podman load.
  4. Dừng Docker container, copy nội dung của từng volume ra khỏi /var/lib/docker/volumes/<name>/_data, rồi sửa ownership bằng podman unshare chown -R 1000:1000 <path>.
  5. Quyết định cách xử lý port: publish port lớn hơn 1024 phía sau reverse proxy, hoặc đặt net.ipv4.ip_unprivileged_port_start.
  6. Viết một file quadlet cho mỗi container, chạy systemctl --user daemon-reload, rồi start từng service.
  7. Chạy sudo loginctl enable-linger <user>, reboot VPS, đăng nhập lại và kiểm tra podman ps liệt kê lại mọi service.

Hai engine không dùng chung dữ liệu nào: image storage và network tách biệt. Vì vậy, bạn có thể chạy cả hai trong lúc migrate; thứ duy nhất chúng có thể tranh chấp là số port trên host. Di chuyển từng service, theo dõi nó trong một ngày, rồi chuyển service tiếp theo.

Podman và Docker: lựa chọn nào phù hợp với VPS của bạn?

Hãy dùng Docker nếu stack của bạn nằm trong các file compose mà những người khác cũng duy trì, hoặc nếu bạn phụ thuộc vào các công cụ giao tiếp với Docker socket. Khả năng tương thích với những gì mọi người khác viết là một tính năng thực tế, và Docker có lợi thế hơn ở điểm này. Một team có laptop đều chạy Docker cũng hưởng lợi rõ ràng khi chạy cùng engine trong môi trường production.

Hãy chuyển sang Podman nếu VPS chỉ chạy một số ít service do bạn kiểm soát từ đầu đến cuối, hoặc nếu bạn muốn mỗi ứng dụng chạy dưới một user không có đặc quyền riêng mà trên máy hoàn toàn không có group docker. Khả năng đồng bộ với bản phân phối cũng quan trọng: RHEL và các bản rebuild của RHEL phát hành Podman làm engine được hỗ trợ, nên trên các hệ thống đó Podman là lựa chọn ít phát sinh vấn đề hơn. Nếu bạn đã quản lý mọi thứ khác bằng systemd unit, quadlet sẽ giống như phần còn thiếu được bổ sung, thay vì một công cụ mới phải học.

Có một lựa chọn trung gian đáng nhắc đến. Podman chạy rootful hoạt động khá giống Docker, vẫn giữ command docker thông qua wrapper, đồng thời loại bỏ daemon luôn chạy. Tuy nhiên, lựa chọn này từ bỏ phần rootless, vốn là yếu tố làm thay đổi vị thế bảo mật của bạn, nên hãy xem nó như một bước trung gian.

Nếu bạn vẫn đang xây dựng container host đầu tiên, hướng dẫn thiết lập và hardening Docker trên VPS mới là con đường ngắn hơn, và những kiến thức đó không hề bị lãng phí. Image và volume là cùng một loại object trên cả hai engine, nên việc chuyển đổi sau này chủ yếu chỉ thay đổi cách systemd giám sát các service của bạn, còn lại hầu như không thay đổi.

FAQ

Podman có phải là bản thay thế trực tiếp cho Docker không?

Đối với các lệnh bạn nhập thì gần như vậy. Cài podman-docker sẽ cung cấp wrapper /usr/bin/docker, còn run, ps, build, logsexec hoạt động theo cùng cách. Podman không thay thế daemon. Podman không có tính năng tương đương với Swarm. Các công cụ kết nối đến /var/run/docker.sock phải được trỏ đến Podman socket dành cho từng user. Các image được Docker pull vẫn không hiển thị trong Podman vì hai công cụ dùng storage riêng.

Vì sao các container Podman rootless của tôi dừng khi tôi đăng xuất khỏi SSH?

Vì systemd dừng user session và mọi user service trong session đó khi phiên đăng nhập cuối cùng đóng. Chạy sudo loginctl enable-linger <user>, sau đó kiểm tra để bảo đảm loginctl show-user <user> --property=Linger in ra Linger=yes. Linger giữ systemd instance của user đó chạy khi không có session hoạt động. Đây cũng là cơ chế giúp các container khởi động lại sau reboot.

Vì sao các file trong volume của tôi thuộc sở hữu của UID 100999?

Podman rootless ánh xạ container UID 0 vào user trên host, sau đó ánh xạ container UID 1 trở lên vào dải subuid của user. Với dải bắt đầu từ 100000, container UID 1000 trở thành 100999 trên host. Sửa quyền từ bên trong namespace bằng podman unshare chown 1000:1000 /path/to/data, mount với flag :U trong lần chạy đầu tiên, hoặc dùng --userns=keep-id để UID trong container khớp với UID của bạn.

Tôi có thể tiếp tục dùng docker-compose.yml với Podman không?

Có, theo 2 cách. podman-compose đọc file và điều khiển trực tiếp Podman CLI. Hoặc bật compatibility socket bằng systemctl --user enable --now podman.socket, đặt DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sock, rồi chạy docker compose thực thông qua socket đó. Có thể gặp vấn đề với network_mode: host, với các service mount Docker socket, và với restart: always. Trường hợp này cần một quadlet unit và linger để tiếp tục hoạt động sau reboot.

Rootless có thực sự làm container an toàn hơn không?

Rootless loại bỏ một rủi ro cụ thể: nếu một process thoát khỏi rootless container, process đó chỉ có quyền của user không đặc quyền thay vì quyền root. Đây là một lợi ích đáng kể. Vì vậy rootless Podman không có nhóm docker tương đương với root. Tuy nhiên, rootless không ngăn được các lỗ hổng kernel và không bảo vệ những file mà chính user của bạn có thể đọc. Vì thế, vẫn phải áp dụng các biện pháp hardening khác như trên mọi server.