Podman vs Docker for VPS: wetin really change?
Podman no get daemon and e run rootless by default. See wetin that means for compose files, quadlets, ports below 1024 and volume ownership on VPS.
What actually different between Podman and Docker
Podman and Docker dey run the same OCI (open container initiative) images for VPS, so the choice no be about which software you fit run. The difference na the process model. Docker dey run a root daemon wey dey own every container, and the docker command na small client wey dey ask that daemon to do the work. Podman no get daemon: podman run dey start the container as child process of anything wey call am, under your own unprivileged user.
Everything else follow from that one fact. systemd na e dey handle auto-start instead of the daemon. Volume ownership dey pass through user namespace, so the owner wey you see with ls -l for host no be the owner wey container dey see. Ports below 1024 go refuse to bind until you change kernel setting. The docker CLI (command line interface) still dey work through wrapper, up to the point wey something want Docker socket.
No daemon: wetin dey actually run when you start container
For Docker host, pstree -a dey show dockerd as root, containerd near am, and one containerd-shim-runc-v2 for every container wey dey run. Your application na child of that shim, and the shim na child of PID 1. Nothing connect the container to the shell wey start am. If you stop the daemon, you lose control plane for every container for that machine. If default live-restore setting dey off, systemctl restart docker go restart your containers too.
Podman no get equivalent process. Start container and you go get one conmon (container monitor) process wey dey hold the container main process. The user wey run the command own am.
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:8080ps suppose list conmon wey dey run as your login user, no be as root. curl suppose print 200. Because no central service dey own the container, sudo apt upgrade podman no dey stop anything wey don already dey run. If monitor for one container crash, e no fit carry the other containers go down.
The missing daemon still get cost. Nothing go start your containers after reboot. Docker --restart=always na promise wey daemon dey keep for boot time. Podman replace am with systemd. Na for this reason the quadlet section below dey.
The socket na the other part of the matter. /var/run/docker.sock na root-owned API (application programming interface) endpoint. Any process wey fit write to am fit start privileged container wey mount host filesystem. If you add user to docker group, you give that user root access through another, slower route. E good make you read am together with giving each service account only the access wey e need. Podman no expose socket unless you request am. The socket wey you get belong to one user for /run/user/<uid>/podman/podman.sock.
Install Podman for Ubuntu 24.04 and confirm say rootless dey work
sudo apt update
sudo apt install -y podman uidmap
podman --version
podman info | grep -i rootlessThe uidmap package dey provide newuidmap and newgidmap. Na setuid helpers be these ones wey allow ordinary user claim subordinate ID range. Without dem, rootless containers no go start. podman info suppose print rootless: true.
Ubuntu 24.04 dey ship Podman 4.9, while Debian 13 dey ship Podman 5.x, based on check wey happen for August 2026. This difference matter because quadlet files need 4.4 or newer, and .pod quadlet files need 5.0. Run podman --version before you copy example from upstream documentation.
Every rootless user need subordinate ID range:
grep "$USER" /etc/subuid /etc/subgidUser wey adduser create for Ubuntu go get range automatically. User wey useradd -M create, or configuration tool create, often no get am. The error go show this:
Error: cannot find UID/GID for user deploy: no subuid ranges found for user "deploy" in /etc/subuid - check rootless mode in man pages.Assign range, then reset the user storage so e go use the new mapping:
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 deploy
podman system migrateAnother surprise for first run be say Podman no assume Docker Hub. Short image name dey resolve against unqualified-search-registries inside /etc/containers/registries.conf. For script wey no get terminal attached, the pull go fail with short-name resolution enforced but cannot prompt without a TTY. Write the full name every time. Use docker.io/library/nginx:1.27 instead of nginx.
Wetin rootless containers really give you for rented server
Rootless container dey run inside user namespace, wey be kernel feature wey give process im own private map of user IDs. Inside the namespace, the container superuser na UID (user ID) 0. Outside am, for your VPS, that same process na your ordinary login user. Root for the container no be root for the host.
Na this be the honest size of the benefit. If image insist say e must run as root, web application get remote code execution bug, or escape depend on process being UID 0 outside, all of dem go end up with your unprivileged user's permissions instead of the machine own. Rootless no protect you from kernel bugs. E no protect your own files too, because the escaped process dey run as you and fit read anything wey you fit read. The unit wey isolation cover matter at least as much as the UID mapping. You fit see this more clearly next door for FreeBSD jail, wey wrap complete userland wey you administer like small machine instead of layered image wey you pull from registry.
Docker fit run rootless too. dockerd-rootless-setuptool.sh install dey set up per-user daemon and e dey work well. The difference na which option be the default. With Podman, you get rootless without asking. So your first failure na container wey no fit bind port 80, instead of service wey quietly run as root for two years.
Why volume files dey owned by UID 100999?
Na because of that same user namespace. Container UID 0 dey map to your host UID. Container UID 1 dey map to the first ID for your subuid range, and e dey count up from there. If range start for 100000, container UID 1000 go land for host as 100999.
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 go print 1000. Host listing go print owner 100999, because 100000 plus 1000 minus 1 na 100999. Nothing spoil, and plain chown no go fix am, because your unprivileged user no fit change file ownership at all outside the namespace.
Four ways dey:
podman unshare chown 1000:1000 "$PWD/data"dey run chown inside the same user namespace, where the numbers get the same meaning wey dem get for container.-v "$PWD/data:/data:U"dey ask Podman to correct ownership of the source directory for you. Use am for fresh directory, no be data wey you care about.--userns=keep-iddey map your host UID to the same UID inside the container, so new files go come out with your ownership.- Named volume like
-v appdata:/datago avoid the question, because Podman dey create am inside your own storage with correct ownership already.
If you don face this issue for Docker, na the same problem one layer up. The PUID and PGID variables wey plenty images dey expose set the UID wey process inside container dey use, and for rootless Podman, that UID go map again one more time. PUID=1000 inside rootless container still dey write host files wey owner na 100999. Choose the numbers with that second mapping for mind, or move the data enter named volume and stop to worry about am.
Two more notes dey about mounts. The :z and :Z flags wey you dey see for Fedora and RHEL examples na SELinux relabel options, and Ubuntu dey use AppArmor, so dem no do anything there. Rootless Podman still no fit mount host directory wey your user no fit read, and na the expected behaviour, no be fault.
Rootless Podman refuse to publish port 80 because wetin?
Because to bind port wey dey below 1024 need privilege wey your user no get. The error message show the fix:
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 deniedTwo answers dey work. Lower the threshold for the whole 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_startThe last command suppose echo 80 back. Understand wetin this setting dey do: every user for the machine fit now bind 80 and 443, no be only the person wey dey run containers. For VPS wey na one admin dey manage, this trade-off fit acceptable. For machine wey dey carry other people's accounts, e no acceptable. The other answer na to publish for 8080 and put reverse proxy for front. Na there you want certificates wey certbot issue and renew for nginx anyway.
Rootless publishing also change wetin your application dey see. Podman 4.x dey use slirp4netns with the rootlesskit port handler by default. Forwarded connections arrive with source address wey don change, so access log record every visitor as 10.0.2.100. Podman 5.0 change the default to pasta, wey preserve the real client address. For 4.x, --network slirp4netns:port_handler=slirp4netns restore the true source address, but e fit reduce throughput small.
One good thing dey here. Rootless published port na ordinary listening socket wey normal process own, so your firewall input rules apply to am. Docker publish ports by writing NAT (network address translation) rules plus its own forwarding accepts. Na exactly why published Docker port dey ignore the ufw rule wey you think say dey block am. Rootful Podman use similar plumbing and inherit the same trap. Rootless no dey.
Docker Compose files wey still work under Podman?
Mostly, dem fit work through two different ways. The first one na podman-compose, a separate implementation wey dey read the same file and control Podman CLI:
sudo apt install -y podman-compose
podman-compose up -d
podman psThe second one na real Docker Compose wey dey talk to Podman Docker-compatible API through per-user socket:
systemctl --user enable --now podman.socket
export DOCKER_HOST="unix:///run/user/$(id -u)/podman/podman.sock"
docker compose up -d
podman psdocker compose ps and podman ps suppose list the same containers, because na only one set of containers dey. Name resolution dey work too: Podman default network backend, netavark, dey run aardvark-dns, so containers for user-defined network fit find each other by name.
But some limitations dey. Anything wey mount /var/run/docker.sock must point to Podman socket, or you must remove am. network_mode: host dey behave differently under user namespace. depends_on with condition: service_healthy get uneven support across podman-compose versions. restart: always no dey survive reboot by itself; the next section go fix this. Compose still dey good for describing multi-container stack inside one file, and under Podman, e dey serve as translation layer. For stack wey you plan to maintain for years, convert am to quadlets and maintain one abstraction instead of two.
Pods: di idea wey Docker no get answer for
Pod na group of containers wey share one network namespace. Podman dey start small infra container to keep that namespace open, then the members fit reach each other for 127.0.0.1 without any user-defined network or 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 --podpodman pod ps suppose show the pod Running with three containers, including the infra container. The web container now fit reach Redis for 127.0.0.1:6379 instead of app-cache:6379. Two rules dey come from the shared namespace: publish ports for the pod, never for a member; and no two members fit listen on the same port.
Na this be the Kubernetes model, and Podman dey use am well. podman kube generate app > app.yaml dey write Kubernetes manifest from wetin dey run (older packages dey write am as podman generate kube), while podman kube play app.yaml dey recreate am for another host. Quadlet get .kube unit type wey dey run this kind file as systemd service. This na genuinely different way to group services, and na the strongest reason to choose Podman if Kubernetes fit enter your future plans.
Auto-start without a daemon: quadlet units
Quadlet na systemd generator. E dey turn short file wey describe container into real systemd service for boot time. Put files for ~/.config/containers/systemd/ if na rootless user, or /etc/containers/systemd/ if na root.
~/.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 fit almost empty, because the section header na wetin dey create the volume:
[Volume]systemctl --user daemon-reload
systemctl --user start caddy
systemctl --user status caddy
journalctl --user -u caddy -n 50Service name come from filename: caddy.container go become caddy.service. No run systemctl --user enable caddy. You no fit enable generated units, and systemd go answer Failed to enable unit: Unit /run/user/1000/systemd/generator/caddy.service is transient or generated.. Na [Install] section dey start the container for boot time, while daemon-reload dey regenerate the unit after you edit the file.
Now, na this setting dey catch almost everybody:
sudo loginctl enable-linger deploy
loginctl show-user deploy --property=LingerExpect Linger=yes. If linger no dey on, systemd go tear down the whole user session when your last SSH connection close. All rootless containers go stop with am, and none go come back for boot. If containers dey disappear whenever you log out, na always this one cause am.
Because the container na the main process of ordinary service unit, systemd controls apply directly. MemoryMax= and CPUQuota= inside [Service] section behave exactly as dem dey behave for any other service wey you control with systemd. This one need cgroup v2 (control group version 2). Ubuntu don dey use am by default since 22.04. Confirm am with podman info | grep -i cgroup.
Updates get matching mechanism. AutoUpdate=registry plus systemctl --user enable --now podman-auto-update.timer go check registry for newer image with the same tag, restart the unit, and roll back to the previous image if the new container no start successfully. Run podman auto-update --dry-run first to see wetin e go change. The older podman generate systemd command still dey, but e don deprecated, so use quadlets for anything new.
Docker alias dey work for where, and e no dey work for where
sudo apt install -y podman-docker
sudo touch /etc/containers/nodocker
docker pspodman-docker dey install one /usr/bin/docker wrapper wey dey call Podman. If nodocker file no dey, every call go first print Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg. The wrapper cover commands wey you dey type every day: run, ps, logs, exec, build, pull, push, inspect, cp, volume, network.
Wetin no carry over na shorter list, and e clear pass. Swarm mode no get equivalent, so Swarm stack no get where to run. Tools wey dey talk to Docker socket need make dem export Podman socket, and some of dem still notice the difference; Traefik Docker provider dey work when you point am to /run/user/<uid>/podman/podman.sock, but Watchtower no get use at all, because podman auto-update dey do that work. Storage dey separate, so Podman no fit see images wey you don already pull with Docker, and podman images for Docker host wey dey busy go start empty.
Move stack wey dey run, step by step
- Create or choose unprivileged user wey go own the containers, then confirm say e get range for
/etc/subuid. - Pull again anything wey come from registry, and use fully qualified names. Podman get im own image store and e no go read Docker own.
- Move images wey you build locally across with
docker save app:1.4 | podman load. - Stop the Docker container, copy the content of each volume comot from
/var/lib/docker/volumes/<name>/_data, then fix ownership withpodman unshare chown -R 1000:1000 <path>. - Decide how to handle the port: publish above 1024 behind reverse proxy, or set
net.ipv4.ip_unprivileged_port_start. - Write one quadlet file for each container, run
systemctl --user daemon-reload, then start each service. - Run
sudo loginctl enable-linger <user>, reboot the VPS, log in again, and check saypodman pslist every service again.
The two engines no share anything: dem get separate image storage and separate networks. So you fit run both while you dey migrate, and the only thing wey fit cause conflict na host port number. Move one service, monitor am for one day, then move the next one.
Podman vs Docker: which one belong for your VPS?
Remain with Docker if your stack dey inside compose files wey other people dey maintain too, or if you depend on tools wey dey communicate with the Docker socket. Compatibility with wetin everybody else dey write na real feature, and Docker get more of am. Team wey all their laptops dey run Docker go also gain something concrete by running the same engine for production.
Change to Podman if your VPS dey run small number of services wey you control from beginning to end, or if you want make every application dey under im own unprivileged user, with no docker group at all for the machine. Distribution alignment matter too: RHEL and im rebuilds release Podman as the supported engine, so for those systems Podman na the path wey get fewer surprises. If you still want Docker for any of those hosts, the dnf route for Rocky Linux and AlmaLinux dey start by removing the podman-docker wrapper wey already own the docker command there. If you already dey supervise everything else with systemd units, quadlets go feel like one missing piece wey don finally arrive, instead of another new tool to learn.
One middle option deserve mention. Rootful Podman dey behave almost like Docker, keep the docker command through the wrapper, and still remove the daemon wey dey run all the time. E also give up the rootless part, and na that part dey change your security position, so treat am as one stop along the way.
If you still dey build your first container host, the Docker setup and hardening path for a fresh VPS na the shorter road, and none of that knowledge go waste. Images and volumes na the same objects for both engines, so moving later go change how you supervise your services, and almost nothing else.
FAQ
Podman na drop-in replacement for Docker?
For the commands wey you dey type, e near am. If you install podman-docker, e go give you /usr/bin/docker wrapper, and run, ps, build, logs and exec go behave the same way. But e no replace the daemon. Swarm no get equivalent, tools wey dey connect to /var/run/docker.sock must point to the per-user Podman socket instead, and images wey Docker pull stay invisible to Podman because both dey use separate storage.
Why my rootless Podman containers dey stop when I log out of SSH?
Na because systemd dey stop the user session, together with every user service, when your last login close. Run sudo loginctl enable-linger <user>, then check say loginctl show-user <user> --property=Linger print Linger=yes. Linger keep that user's systemd instance running even when no active session dey. Na this one also make the containers start again after reboot.
Why files for my volume dey owned by UID 100999?
Rootless Podman dey map container UID 0 to your host user. Then e dey map container UID 1 and upward onto your subuid range. If the range start at 100000, container UID 1000 go become 100999 for the host. Correct am from inside the namespace with podman unshare chown 1000:1000 /path/to/data, mount with the :U flag for the first run, or use --userns=keep-id so container UIDs match your own.
I fit continue to use docker-compose.yml with Podman?
Yes, for two ways. podman-compose dey read the file and drive the Podman CLI directly. Or enable the compatibility socket with systemctl --user enable --now podman.socket, set DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sock, then run real docker compose against am. Expect some friction with network_mode: host, with services wey mount the Docker socket, and with restart: always. restart: always need a quadlet unit and linger to survive reboot.
Rootless really make containers more secure?
E remove one specific risk: if process break out from rootless container, e go get your unprivileged user's permissions instead of root permissions. This benefit dey important, and na why the root-equivalent docker group no get counterpart under rootless Podman. But e no stop kernel vulnerabilities, and e no protect files wey your own user fit read. So keep the other hardening steps wey you go do for any server.