SSD Nodes Learn 🎉 VPS from $5.50/mo
How to do am Matt ConnorBy Matt Connor · Updated 2026-08-13

Podman vs Docker for VPS: Wetin Really Change?

Podman no get daemon and rootless by default. See how that affects Compose files, Quadlets, ports below 1024, and volume ownership for VPS.

Wetin really 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 for the process model. Docker dey run root daemon wey get control of 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 go 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 need Docker socket.

No daemon: wetin dey actually run when you start container

For Docker host, pstree -a dey show dockerd as root, containerd beside 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, and when 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:8080

ps suppose list conmon wey dey run as your login user, no be as root, and curl suppose print 200. Because no central service dey own the container, sudo apt upgrade podman no dey stop anything wey already dey run. If monitor for one container crash, e no go carry the others along.

The missing daemon still get cost. Nothing go start your containers after reboot. Docker --restart=always na promise wey daemon dey keep during boot, while Podman replace am with systemd. Na wetin the quadlet section below dey explain.

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 longer route. E good make you read am beside giving each service account only the access wey e need. Podman no expose socket unless you request am, and the socket wey e give you 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 rootless

The uidmap package dey provide newuidmap and newgidmap. Dem be setuid helpers wey allow ordinary user claim range of subordinate IDs. 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 version 4.4 or newer, while .pod quadlet files need version 5.0. Run podman --version before you copy example from upstream documentation.

Every rootless user need subordinate ID range:

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

User wey adduser create for Ubuntu go get range automatically. User wey useradd -M create, or wey 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 that user's storage so the new mapping go take effect:

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

Another surprise for first run: Podman no assume say Docker Hub dey. Short image name dey resolve against unqualified-search-registries for /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.

Wetín rootless containers actually give you for rented server

Rootless container dey run inside user namespace, na kernel feature wey give process im own private map of user IDs. Inside the namespace, container superuser na UID (user ID) 0. Outside am, for your VPS, that same process na your ordinary login user. Root for container no be root for host.

Na this be the real size of the benefit. Image wey insist say e must run as root, web application wey get remote code execution bug, or escape wey depend on UID 0 for outside: all of dem go end up with permissions of your unprivileged user instead of permissions for the machine. 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.

Docker fit run rootless too. dockerd-rootless-setuptool.sh install dey set up per-user daemon and e dey work well. The difference na where the default dey point. With Podman, you get rootless without asking for am, 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, then e dey count up from there. If range start from 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"

The container prints 1000. The host listing prints 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 the chown inside the same user namespace, where the numbers get the meaning wey container dey use.
  • -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 matter to you.
  • --userns=keep-id dey 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:/data dey avoid the matter, because Podman dey create am inside your own storage with correct ownership already.

If you don face this problem for Docker, na the same problem one layer up. The PUID and PGID variables wey many images expose dey set the UID wey process inside the 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 get ownership 100999. Choose the numbers with that second mapping for mind, or move the data go named volume and stop to worry about am.

Two more notes 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.

Why rootless Podman dey refuse publish port 80?

Because binding 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 denied

Two answers dey work. You fit 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_start

The last command suppose echo 80 back. Make you 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 one admin dey manage, this trade-off fit acceptable. But for machine wey carry other people accounts, e no acceptable. The other answer na to publish for 8080, then 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 go arrive with source address wey don rewrite, so access log go record every visitor as 10.0.2.100. Podman 5.0 change the default to pasta, wey dey keep the real client address. For 4.x, --network slirp4netns:port_handler=slirp4netns go restore the true source address, but e fit reduce throughput small.

One good surprise dey here. Rootless published port na ordinary listening socket wey normal process own, so your firewall input rules apply to am. Docker dey 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 get the same trap. Rootless no dey do am.

Do my Docker Compose files still work under Podman?

Mostly, through two different methods. The first one na podman-compose, a separate implementation wey dey read the same file and control the Podman CLI:

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

The 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 ps

docker compose ps and podman ps suppose list the same containers, because na only one set of containers dey. Name resolution still dey work: Podman default network backend, netavark, dey run aardvark-dns, so containers for user-defined network fit find each other by name.

But some differences dey. Anything wey dey 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 no get consistent support across podman-compose versions. restart: always no dey survive reboot by itself; the next section go fix this. Compose still good for describing one multi-container stack inside one file, and under Podman e dey act as translation layer. If you plan keep a stack for years, convert am to quadlets and maintain one abstraction instead of two.

Pods: the idea wey Docker no get answer for

A pod na group of containers wey dey 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 user-defined network and without 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 suppose show the pod Running with three containers, including the infra container. The web container don dey reach Redis for 127.0.0.1:6379 instead of app-cache:6379. Two rules come from the shared namespace: publish ports for the pod, never for member; and no two members fit listen on the same port.

Na this be the Kubernetes model, and Podman dey follow am well. podman kube generate app > app.yaml dey write Kubernetes manifest from wetin dey run (older packages dey spell am 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 dey your future.

Auto-start without a daemon: quadlet units

Quadlet na systemd generator. E dey turn short file wey describe container into real systemd service when system boot. Files dey go ~/.config/containers/systemd/ for rootless user, or /etc/containers/systemd/ for 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 50

Service name come from filename: caddy.container become caddy.service. No run systemctl --user enable caddy. Generated units no fit enable, 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 container when system boot, and daemon-reload na wetin 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=Linger

Expect Linger=yes. If linger no dey, systemd go tear down the whole user session when your last SSH connection close. So every rootless container go stop with am, and none go come back when system boot. If containers dey disappear whenever you log out, na always this be the cause.

Because container na the main process for ordinary service unit, systemd controls apply directly. MemoryMax= and CPUQuota= inside [Service] section dey behave exactly as dem dey behave for any other service wey you cap with systemd. This one need cgroup v2 (control group version 2), wey Ubuntu don use 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 fit start. Run podman auto-update --dry-run first to see wetin e go change. The older podman generate systemd command still dey, but e deprecated, so use quadlets for anything new.

Wey docker alias dey work, and wey e no work

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

podman-docker dey install a /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.

But some things no carry over. The list short, and e clear. Swarm mode get no equivalent, so Swarm stack no get anywhere to run. Tools wey dey talk to Docker socket need make dem export Podman socket, and some still notice the difference; Traefik Docker provider dey work when you point am to /run/user/<uid>/podman/podman.sock, but Watchtower no get any place 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 busy Docker host dey start empty.

Stack wey dey run, migrate am one step after another

  1. Create or choose the unprivileged user wey go own the containers, then confirm say e get range for /etc/subuid.
  2. 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.
  3. Move images wey you build locally across with docker save app:1.4 | podman load.
  4. Stop the Docker container, copy each volume content comot from /var/lib/docker/volumes/<name>/_data, then fix ownership with podman unshare chown -R 1000:1000 <path>.
  5. Settle the port matter: publish above 1024 behind reverse proxy, or set net.ipv4.ip_unprivileged_port_start.
  6. Write one quadlet file for each container, run systemctl --user daemon-reload, then start each service.
  7. Run sudo loginctl enable-linger <user>, reboot the VPS, log in again, and check say podman ps list 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 fit stay for your VPS?

Stay with Docker if your stack dey inside compose files wey other people dey maintain too, or if you depend on tools wey dey talk to the Docker socket. Compatibility with wetin everybody dey write na real feature, and Docker get more of am. Team wey all their laptops dey run Docker go also gain something practical by running the same engine for production.

Switch to Podman if your VPS dey run small number of services wey you dey control from start to finish, or if you want make every application run under its own unprivileged user, with no docker group at all for the server. Distribution alignment matter too: RHEL and its rebuilds dey release Podman as the supported engine, so for those systems Podman na the option wey get fewer surprises. If you already dey supervise everything else with systemd units, quadlets go feel like missing part wey finally show up, instead of another new tool to learn.

One middle option deserve mention. Rootful Podman behave almost like Docker, preserve the docker command through the wrapper, and still remove the daemon wey must dey run all the time. But e also give up the rootless part, and na that part dey change your security position, so treat am as one step for the way.

If you still dey build your first container host, the Docker setup and hardening path on 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 your services dey get supervision, and almost nothing else.

FAQ

Podman be drop-in replacement for Docker?

For the commands wey you type, e nearly be the same. Installing podman-docker gives you a /usr/bin/docker wrapper, and run, ps, build, logs and exec dey behave the same way. E no be replacement for the daemon. Swarm get no equivalent, tools wey connect to /var/run/docker.sock must point to the per-user Podman socket instead, and images wey Docker pull remain invisible to Podman because both dey keep separate storage.

Why 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. E then map container UID 1 and above onto your subuid range. With range wey start at 100000, container UID 1000 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, services wey mount the Docker socket, and restart: always, wey need a quadlet unit and linger to survive reboot.

Rootless really make containers more secure?

E remove one specific risk: if process break out of a rootless container, e get your unprivileged user's permissions instead of root permissions. This one dey useful, and na why the root-equivalent docker group get no counterpart under rootless Podman. E no stop kernel vulnerabilities, and e no protect files wey your own user fit read. So keep the other hardening measures wey you go apply for any server.