Podman is a daemonless, OCI-compatible container engine developed by Red Hat. Its command line mirrors Docker's, but there is no long-running root daemon: each container runs as a child process of the user who started it, and by default that user does not need root at all. In this tutorial you will install Podman on Ubuntu 24.04, run rootless containers, use pods and Compose files, and make a container start at boot with systemd Quadlet units.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS.
  • A non-root user with sudo privileges. You will run containers as this user, so log in as it directly over SSH (not through su or sudo -u) so that a proper systemd user session exists.
  • Basic familiarity with Docker commands (run, ps, logs, exec).

Podman and Docker can be installed on the same server, but they keep separate image stores and do not see each other's containers.

Podman vs Docker at a glance

AspectDockerPodman
ArchitectureClient talking to the dockerd daemonNo daemon, containers are child processes
Default privilegesDaemon runs as rootRootless by default for normal users
CLIdockerpodman, same subcommands and flags for the common cases
Multi-container groupsComposePods (shared network namespace), plus Compose via podman-compose
Start at boot--restart policy in the daemonsystemd units, generated from Quadlet files
Kubernetes YAMLNot supportedpodman kube generate and podman kube play

Step 1 - Installing Podman

Podman is in the Ubuntu 24.04 repositories. Update the package index and install it together with podman-compose, which runs Compose files on top of Podman:

sudo apt update
sudo apt install -y podman podman-compose

Check the installed version:

podman --version
podman version 4.9.3

Rootless containers map your user to a range of subordinate UIDs and GIDs. Ubuntu assigns these automatically when a user is created with adduser. Confirm your user has an entry:

grep "^$USER:" /etc/subuid /etc/subgid
/etc/subuid:your_user:100000:65536
/etc/subgid:your_user:100000:65536

If nothing is printed (for example, for a user created with useradd), add a range and apply it:

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

Replace your_user with your user name and pick a range that does not overlap with other users in /etc/subuid.

Step 2 - Configuring image registries

Unlike Docker, Podman does not assume that an image name without a registry comes from Docker Hub. On Ubuntu no default search registry is configured, so a short name like nginx fails. The safest habit is to always use fully qualified names such as docker.io/library/nginx:alpine.

If you prefer Docker-style short names, create a per-user registries file:

mkdir -p ~/.config/containers
nano ~/.config/containers/registries.conf

Add this line:

unqualified-search-registries = ["docker.io"]

Save the file. Podman now resolves short names against Docker Hub for your user.

Step 3 - Running your first rootless container

Run an Nginx container as your normal user, publishing port 8080 on the host:

podman run -d --name web -p 8080:80 docker.io/library/nginx:alpine

List running containers and test the web server:

podman ps
curl -I http://localhost:8080
CONTAINER ID  IMAGE                           COMMAND               CREATED        STATUS        PORTS                 NAMES
3f1c2a9b7d10  docker.io/library/nginx:alpine  nginx -g daemon o...  5 seconds ago  Up 5 seconds  0.0.0.0:8080->80/tcp  web

HTTP/1.1 200 OK

Everyday Docker commands work the same way: podman logs web, podman exec -it web sh, podman stop web, podman rm web, podman images.

To see that the container is really unprivileged, compare the user inside and outside it:

podman top web user huser
USER    HUSER
root    1000
nginx   100100

The processes are root and nginx inside the container, but on the host they belong to your UID (1000) and to a subordinate UID. A container escape would land in an unprivileged account.

Images and containers for your user are stored under ~/.local/share/containers/, not in /var/lib/docker.

Publishing ports below 1024

Rootless containers cannot bind ports below 1024 by default. Either publish a high port and put a reverse proxy in front, or lower the unprivileged port threshold for the whole system:

echo "net.ipv4.ip_unprivileged_port_start=80" | sudo tee /etc/sysctl.d/99-unprivileged-ports.conf
sudo sysctl --system

This allows any local user to bind ports 80 and above, so only do it on servers where you control all accounts.

Step 4 - Using the Docker command name

If you have scripts or muscle memory that call docker, the podman-docker package installs a docker command that runs Podman. Skip this step if Docker Engine is also installed, because the two packages conflict.

sudo apt install -y podman-docker
docker ps
Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg.
CONTAINER ID  IMAGE                           COMMAND               CREATED        STATUS        PORTS                 NAMES
3f1c2a9b7d10  docker.io/library/nginx:alpine  nginx -g daemon o...  2 minutes ago  Up 2 minutes  0.0.0.0:8080->80/tcp  web

To hide the notice, create the file it mentions:

sudo touch /etc/containers/nodocker

Tools that talk to the Docker API over a socket (for example Docker Compose v2 or some IDE plugins) need the Podman API service. Start it for your user and point DOCKER_HOST at it:

systemctl --user enable --now podman.socket
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock

Add the export line to ~/.bashrc to make it permanent.

Step 5 - Grouping containers in a pod

A pod is a group of containers that share a network namespace, the same concept as a Kubernetes pod. Containers in a pod reach each other on localhost, and ports are published on the pod rather than on each container.

Remove the earlier test container and create a pod that publishes port 8080:

podman rm -f web
podman pod create --name webapp -p 8080:80

Start Nginx and Redis inside the pod:

podman run -d --pod webapp --name webapp-nginx docker.io/library/nginx:alpine
podman run -d --pod webapp --name webapp-redis docker.io/library/redis:7-alpine

Check the pod and verify that the Redis container can reach Nginx on localhost:

podman pod ps
podman exec webapp-redis wget -qO- http://127.0.0.1 | grep title
POD ID        NAME        STATUS      CREATED         INFRA ID      # OF CONTAINERS
8a2d61c4e0f3  webapp      Running     30 seconds ago  c61b0f9a2e11  3

<title>Welcome to nginx!</title>

The third container is the small infra container that holds the shared namespaces.

Podman can export a running pod as Kubernetes YAML and recreate it from that file, which is handy for moving a workload to a cluster later:

podman kube generate webapp > webapp.yaml
podman pod rm -f webapp
podman kube play webapp.yaml

Step 6 - Running Compose files

Existing compose.yaml files usually work unchanged with podman-compose. Create a small project to try it:

mkdir ~/compose-demo && cd ~/compose-demo
nano compose.yaml
services:
  web:
    image: docker.io/library/nginx:alpine
    ports:
      - "8081:80"
  cache:
    image: docker.io/library/redis:7-alpine

Start the stack and check it:

podman-compose up -d
podman-compose ps
curl -I http://localhost:8081
HTTP/1.1 200 OK

Stop and remove it with podman-compose down. Features that depend on the Docker daemon itself, such as Swarm deploy: settings, are ignored.

Step 7 - Starting containers at boot with Quadlet

Podman has no daemon to restart containers after a reboot, so it uses systemd instead. Quadlet lets you describe a container in a small unit-like file, and systemd generates the full service from it.

First allow your user's services to run without an active login session:

sudo loginctl enable-linger $USER

Create the Quadlet directory and a .container file:

mkdir -p ~/.config/containers/systemd
nano ~/.config/containers/systemd/web.container
[Unit]
Description=Nginx web server (rootless Podman)

[Container]
Image=docker.io/library/nginx:alpine
ContainerName=web
PublishPort=8080:80

[Service]
Restart=always

[Install]
WantedBy=default.target

Remove any container still using port 8080, reload systemd so it generates web.service, and start it:

podman pod rm -f webapp
systemctl --user daemon-reload
systemctl --user start web.service
systemctl --user status web.service
● web.service - Nginx web server (rootless Podman)
     Loaded: loaded (/home/your_user/.config/containers/systemd/web.container; generated)
     Active: active (running) since ...

Generated units cannot be enabled with systemctl enable; the [Install] section in the Quadlet file already makes the service start at boot. Reboot the server and run curl -I http://localhost:8080 to confirm it came back. Logs are available with journalctl --user -u web.service.

Step 8 - Moving images from Docker

If Docker is installed on the same machine, you can copy a local image to Podman without a registry:

docker save your_image:tag | podman load

From another server, save to a file, copy it over and load it:

docker save -o your_image.tar your_image:tag
podman load -i your_image.tar

Named Docker volumes are not converted automatically. Copy their data into a Podman volume with a tar archive, as you would when restoring a Docker volume backup.

Troubleshooting

  • Error: short-name "nginx" did not resolve to an alias: use the full name docker.io/library/nginx or configure unqualified-search-registries as in Step 2.
  • cannot find newuidmap or UID mapping errors: make sure the uidmap package is installed (it is a Podman dependency) and that your user has entries in /etc/subuid and /etc/subgid, then run podman system migrate.
  • Failed to connect to bus with systemctl --user: you are in a shell opened with su or sudo -u. Log in as the user over SSH instead.
  • Permission denied on bind-mounted directories: the container user maps to a subordinate UID on the host. Use podman unshare chown to change ownership inside your user namespace, for example podman unshare chown -R 101:101 ~/site.

Conclusion

You installed Podman on Ubuntu 24.04, ran rootless containers with Docker-compatible commands, grouped containers in a pod, ran a Compose file and made a container survive reboots with a Quadlet unit. For most single-host workloads Podman can replace Docker with little more than a change of command name. As next steps, convert your remaining services to Quadlet files, enable automatic image updates with podman auto-update, or use podman kube generate to prepare workloads for Kubernetes.