Podman is a daemonless, rootless container engine for running OCI-compliant containers on Linux. Unlike Docker, Podman does not require a background daemon process – containers run as direct child processes of the Podman command, making it more secure and suitable for systemd integration. This guide covers installing and using Podman on Ubuntu 24.04 and Rocky Linux 10, including container management, pods, rootless containers, networking, volumes, Podman Compose, Buildah image builds, Quadlet systemd integration, and generating systemd service units.
Prerequisites
- A server or workstation running Ubuntu 24.04 LTS or Rocky Linux 10
- Root or sudo access
- At least 2 GB RAM and 10 GB free disk space
- Internet connectivity to pull container images
What Makes Podman Different from Docker
Docker uses a client-server architecture where every container is a child process of the Docker daemon. If the daemon crashes, all running containers go down with it. Podman takes a different approach – each container is a direct child process of the Podman command. There is no central daemon. This architecture has several practical benefits:
- No daemon – Podman launches containers directly using the OCI runtime (typically crun or runc). No background service to manage or monitor.
- Rootless by default – Regular users can run containers without root privileges. User namespaces map container UIDs to unprivileged host UIDs.
- Systemd integration – Because containers are regular processes, systemd can manage them directly. Podman can generate systemd unit files and supports Quadlet for declarative container management.
- Pod support – Podman natively supports pods (groups of containers sharing namespaces), matching the Kubernetes pod concept.
- Docker CLI compatible – Most Docker commands work with Podman by replacing
dockerwithpodman.
Podman vs Docker Comparison
| Feature | Podman | Docker |
|---|---|---|
| Architecture | Daemonless, fork-exec model | Client-server with dockerd daemon |
| Rootless containers | Built-in, first-class support | Available but requires extra setup |
| Pod support | Native (like Kubernetes pods) | Not supported |
| Systemd integration | Native with Quadlet and generate systemd | Requires manual unit files |
| Image format | OCI and Docker formats | Docker format (OCI compatible) |
| Compose support | podman-compose or docker-compose with socket | docker compose (built-in plugin) |
| Build tool | Buildah (integrated) or podman build | BuildKit |
| Container runtime | crun (default) or runc | runc (default) or crun |
| Registry support | Multiple registries in registries.conf | Docker Hub as default |
| GUI tool | Podman Desktop | Docker Desktop |
Step 1: Install Podman on Ubuntu 24.04
Podman is available in the default Ubuntu 24.04 repositories. Install it with apt.
$ sudo apt update
$ sudo apt install -y podman
Verify the installation.
$ podman --version
podman version 4.9.3
Check the system configuration for rootless support.
$ podman info --format '{{.Host.Security.Rootless}}'
true
Step 2: Install Podman on Rocky Linux 10
On Rocky Linux 10, Podman ships as part of the container-tools module in the default repositories.
$ sudo dnf install -y podman
Verify the installed version.
$ podman --version
podman version 5.x.x
On Rocky Linux with SELinux enabled, container volumes and custom storage paths require correct SELinux labels. If you use a custom graphroot directory, apply the proper context as described in our guide on setting SELinux context labels for Podman storage.
Step 3: Configure Container Registries
Podman supports pulling images from multiple container registries. The configuration file is /etc/containers/registries.conf. By default, Podman searches Docker Hub, Quay.io, and other registries.
View the current unqualified search registries.
$ podman info --format '{{.Registries.Search}}'
[docker.io quay.io]
To add a private registry, edit the registries configuration file.
$ sudo vim /etc/containers/registries.conf
Add your registry under the unqualified-search-registries list.
unqualified-search-registries = ["docker.io", "quay.io", "registry.example.com"]
For setting up your own container registry, see our guide on installing a secure container registry with Podman.
Step 4: Pull and Manage Container Images
Pull images from a registry using podman pull.
$ podman pull docker.io/library/nginx:latest
$ podman pull docker.io/library/alpine:latest
$ podman pull docker.io/library/ubuntu:24.04
List all downloaded images.
$ podman images
REPOSITORY TAG IMAGE ID CREATED SIZE
docker.io/library/nginx latest a8758716bb6a 2 weeks ago 192 MB
docker.io/library/alpine latest b7b28af77ffe 3 weeks ago 7.8 MB
docker.io/library/ubuntu 24.04 3db8720ecbf5 4 weeks ago 78.1 MB
Tag an image with a custom name.
$ podman tag docker.io/library/nginx:latest mywebserver:v1
Remove an image by name or ID.
$ podman rmi mywebserver:v1
Search for images in configured registries.
$ podman search nginx --limit 5
INDEX NAME DESCRIPTION STARS OFFICIAL
docker.io docker.io/library/nginx Official build of Nginx. 19000 [OK]
docker.io docker.io/nginxinc/nginx-unprivileged Unprivileged Nginx 150
docker.io docker.io/bitnami/nginx Bitnami container for Nginx 180
Step 5: Run Containers with Podman
Run a basic container that prints a message and exits.
$ podman run --rm docker.io/library/alpine echo "Hello from Podman"
Hello from Podman
Run an Nginx container in detached mode with port mapping.
$ podman run -d --name webserver -p 8080:80 docker.io/library/nginx:latest
a3b7f2c9d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1
Verify the container is running.
$ podman ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a3b7f2c9d4e5 docker.io/library/nginx:latest nginx -g daemon o... 5 seconds ago Up 5 seconds ago 0.0.0.0:8080->80/tcp webserver
Test the web server responds.
$ curl -s http://localhost:8080 | head -5
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
Interactive Shell Access
Start a container with an interactive shell using the -it flags.
$ podman run -it --rm docker.io/library/ubuntu:24.04 bash
root@c5d8e2f1a4b7:/# cat /etc/os-release | head -3
PRETTY_NAME="Ubuntu 24.04 LTS"
NAME="Ubuntu"
VERSION_ID="24.04"
root@c5d8e2f1a4b7:/# exit
Execute Commands in a Running Container
Use podman exec to run commands inside a running container.
$ podman exec webserver nginx -v
nginx version: nginx/1.27.x
Open an interactive shell in the running container.
$ podman exec -it webserver bash
View Container Logs
Check container logs with podman logs.
$ podman logs webserver
Follow logs in real time.
$ podman logs -f webserver
Show only the last 20 lines.
$ podman logs --tail 20 webserver
Stop and Remove Containers
Stop a running container.
$ podman stop webserver
List all containers including stopped ones.
$ podman ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a3b7f2c9d4e5 docker.io/library/nginx:latest nginx -g daemon o... 10 minutes ago Exited (0) 5 seconds ago 0.0.0.0:8080->80/tcp webserver
Remove a stopped container.
$ podman rm webserver
Remove all stopped containers at once.
$ podman container prune
Step 6: Rootless Containers
One of Podman’s strongest features is rootless container support. Any regular user can run containers without sudo or root access. This works through user namespaces – the container sees UID 0 (root) inside, but on the host it maps to your unprivileged user.
Verify your user has subordinate UID/GID ranges configured.
$ grep $USER /etc/subuid
josphat:100000:65536
$ grep $USER /etc/subgid
josphat:100000:65536
If these entries are missing, add them as root.
$ sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER
Run a container as a regular user without sudo.
$ podman run --rm docker.io/library/alpine id
uid=0(root) gid=0(root) groups=0(root)
Inside the container, the process runs as root. On the host, check the actual UID.
$ podman run -d --name rootless-test docker.io/library/alpine sleep 3600
$ podman top rootless-test huser
HUSER
1000
The container process runs as your regular user (UID 1000) on the host. No root privileges are required or granted.
Rootless containers have one limitation: binding to ports below 1024 requires either root or adjusting the unprivileged port start value.
$ sudo sysctl net.ipv4.ip_unprivileged_port_start=80
To make this permanent, add the setting to sysctl configuration.
$ echo "net.ipv4.ip_unprivileged_port_start=80" | sudo tee /etc/sysctl.d/podman-ports.conf
$ sudo sysctl --system
Step 7: Working with Podman Pods
Pods group multiple containers that share the same network namespace, similar to Kubernetes pods. Containers in a pod communicate over localhost without port mapping between them.
Create a pod with a published port.
$ podman pod create --name webapp -p 8080:80
e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5
List pods to confirm creation.
$ podman pod list
POD ID NAME STATUS CREATED # OF CONTAINERS INFRA ID
e4f5a6b7c8d9 webapp Created 10 seconds ago 1 f1a2b3c4d5e6
Every pod starts with an infra container that holds the network namespace. Add an Nginx container to the pod.
$ podman run -d --pod webapp --name web-frontend docker.io/library/nginx:latest
Add a second container to the same pod. Since both containers share the network namespace, the app container can reach Nginx on localhost:80.
$ podman run -d --pod webapp --name web-app docker.io/library/alpine sleep 3600
Verify the pod now has three containers (infra + two application containers).
$ podman ps -a --pod --filter pod=webapp
CONTAINER ID IMAGE COMMAND STATUS NAMES POD ID PODNAME
f1a2b3c4d5e6 registry.k8s.io/pause:3.9 Up 2 minutes ago e4f5a6b7-infra e4f5a6b7c8d9 webapp
b2c3d4e5f6a7 docker.io/library/nginx:latest nginx -g daemon o... Up 1 minute ago web-frontend e4f5a6b7c8d9 webapp
c3d4e5f6a7b8 docker.io/library/alpine:latest sleep 3600 Up 30 seconds ago web-app e4f5a6b7c8d9 webapp
Test that the app container can reach Nginx over localhost.
$ podman exec web-app wget -qO- http://localhost:80 | head -3
<!DOCTYPE html>
<html>
<head>
Stop and remove the entire pod with all its containers.
$ podman pod stop webapp
$ podman pod rm webapp
Port mappings must be defined at pod creation time. You cannot add new port mappings after the pod is created.
Step 8: Networking in Podman
Podman supports multiple networking backends depending on whether containers run as root or rootless.
Rootless Networking with slirp4netns and pasta
Rootless containers use pasta (on newer Podman versions) or slirp4netns for network connectivity. These tools create a user-mode network stack without requiring root. Pasta is the default on Podman 5.x and offers better performance than slirp4netns.
Check which network mode is active.
$ podman info --format '{{.Host.NetworkBackendInfo.Backend}}'
netavark
Root Networking with Netavark and CNI
Rootful containers use Netavark (the default on modern Podman) or CNI plugins for networking. Netavark provides bridge networking, DNS resolution between containers, and firewall rule management.
Create a custom network.
$ podman network create mynet
mynet
List available networks.
$ podman network ls
NETWORK ID NAME DRIVER
2f259bab93aa podman bridge
a1b2c3d4e5f6 mynet bridge
Run containers on the custom network. Containers on the same network can resolve each other by name.
$ podman run -d --name db --network mynet docker.io/library/postgres:16 -e POSTGRES_PASSWORD=secret
$ podman run -d --name app --network mynet docker.io/library/alpine sleep 3600
Test DNS resolution between containers.
$ podman exec app ping -c 2 db
PING db (10.89.0.2): 56 data bytes
64 bytes from 10.89.0.2: seq=0 ttl=64 time=0.152 ms
Remove the custom network after stopping containers that use it.
$ podman stop db app
$ podman rm db app
$ podman network rm mynet
Step 9: Volumes and Bind Mounts
Podman supports named volumes and bind mounts for persistent data storage. Named volumes are managed by Podman, while bind mounts map a host directory directly into the container.
Named Volumes
Create and use a named volume.
$ podman volume create appdata
$ podman run -d --name db -v appdata:/var/lib/postgresql/data docker.io/library/postgres:16 -e POSTGRES_PASSWORD=secret
List volumes and inspect the storage location.
$ podman volume ls
DRIVER VOLUME NAME
local appdata
$ podman volume inspect appdata --format '{{.Mountpoint}}'
/home/josphat/.local/share/containers/storage/volumes/appdata/_data
Bind Mounts
Map a host directory into a container using -v with an absolute path.
$ mkdir -p /home/josphat/web-content
$ echo "Hello from Podman" > /home/josphat/web-content/index.html
$ podman run -d --name web -p 8080:80 -v /home/josphat/web-content:/usr/share/nginx/html:Z docker.io/library/nginx:latest
The :Z suffix tells Podman to relabel the volume for SELinux. On Rocky Linux 10 with SELinux enforcing, this is required for bind mounts to work. On Ubuntu, the :Z flag is harmless and can be included for portability.
Verify the bind mount serves the custom content.
$ curl http://localhost:8080
Hello from Podman
Remove unused volumes with the prune command.
$ podman volume prune
Step 10: Build Container Images with Buildah
Buildah is the image-building tool that Podman uses under the hood. It can build images from Dockerfiles (Containerfiles) and also supports scripted builds without a Dockerfile. For a detailed walkthrough, see our guide on building OCI container images with Buildah.
Install Buildah if it is not already present.
# Ubuntu 24.04
$ sudo apt install -y buildah
# Rocky Linux 10
$ sudo dnf install -y buildah
Build from a Containerfile
Create a project directory and a Containerfile.
$ mkdir -p ~/myapp && cd ~/myapp
Create the Containerfile with the following content.
FROM docker.io/library/python:3.12-slim
WORKDIR /app
COPY app.py .
EXPOSE 5000
CMD ["python", "app.py"]
Create a simple Python application file.
$ cat > ~/myapp/app.py <<'PYEOF'
from http.server import HTTPServer, SimpleHTTPRequestHandler
class Handler(SimpleHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.end_headers()
self.wfile.write(b"Hello from Podman container")
HTTPServer(("0.0.0.0", 5000), Handler).serve_forever()
PYEOF
Build the image with podman build (which uses Buildah internally).
$ podman build -t myapp:latest ~/myapp/
STEP 1/5: FROM docker.io/library/python:3.12-slim
STEP 2/5: WORKDIR /app
STEP 3/5: COPY app.py .
STEP 4/5: EXPOSE 5000
STEP 5/5: CMD ["python", "app.py"]
COMMIT myapp:latest
Successfully tagged localhost/myapp:latest
Run the custom image.
$ podman run -d --name myapp -p 5000:5000 myapp:latest
$ curl http://localhost:5000
Hello from Podman container
Scripted Buildah Build
Buildah can build images without a Containerfile using command-line instructions.
$ container=$(buildah from docker.io/library/alpine:latest)
$ buildah run $container apk add --no-cache curl
$ buildah config --cmd "/bin/sh" $container
$ buildah commit $container alpine-curl:latest
$ buildah rm $container
The resulting image is available to both Podman and Buildah.
$ podman images | grep alpine-curl
localhost/alpine-curl latest d4e5f6a7b8c9 10 seconds ago 12.3 MB
Step 11: Podman Compose
Podman Compose is a drop-in replacement for Docker Compose that works with Podman. It reads standard docker-compose.yml files and creates Podman containers and pods.
Install podman-compose using pip.
# Ubuntu 24.04
$ sudo apt install -y podman-compose
# Rocky Linux 10
$ sudo dnf install -y podman-compose
Create a docker-compose.yml file for a WordPress setup with MariaDB.
version: "3.9"
services:
db:
image: docker.io/library/mariadb:11
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: wordpress
MYSQL_USER: wpuser
MYSQL_PASSWORD: wppass
volumes:
- db_data:/var/lib/mysql
wordpress:
image: docker.io/library/wordpress:latest
depends_on:
- db
ports:
- "8080:80"
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: wpuser
WORDPRESS_DB_PASSWORD: wppass
WORDPRESS_DB_NAME: wordpress
volumes:
- wp_data:/var/www/html
volumes:
db_data:
wp_data:
Start the application stack.
$ podman-compose up -d
Creating pod ...
Creating container db ...
Creating container wordpress ...
Check the running containers.
$ podman-compose ps
Stop and remove the stack.
$ podman-compose down
Step 12: Generate Systemd Units with Podman
Podman can generate systemd unit files from running containers. This allows systemd to manage container lifecycle – start on boot, restart on failure, and proper shutdown ordering. For more details on running containers as services, see our guide on running Podman containers as systemd services.
First, create and start a container.
$ podman run -d --name nginx-service -p 8080:80 docker.io/library/nginx:latest
Generate a systemd unit file from the running container.
$ podman generate systemd --name nginx-service --new --files
/home/josphat/container-nginx-service.service
The --new flag creates a unit that recreates the container on each start, which is the recommended approach. Move the generated file to the systemd user directory.
$ mkdir -p ~/.config/systemd/user/
$ mv container-nginx-service.service ~/.config/systemd/user/
$ systemctl --user daemon-reload
$ systemctl --user enable --now container-nginx-service.service
Verify the service is active.
$ systemctl --user status container-nginx-service.service
Active: active (running)
To allow user services to run after logout, enable lingering for your user.
$ loginctl enable-linger $USER
Step 13: Quadlet for Systemd Integration
Quadlet is the modern replacement for podman generate systemd. Instead of generating unit files from running containers, you write declarative .container files and systemd generates the units automatically. Quadlet is included in Podman 4.4 and later.
Create a Quadlet file for a rootless Nginx container.
$ mkdir -p ~/.config/containers/systemd/
Create the container definition file.
$ vim ~/.config/containers/systemd/nginx.container
Add the following content.
[Unit]
Description=Nginx web server container
After=local-fs.target
[Container]
Image=docker.io/library/nginx:latest
PublishPort=8080:80
Volume=web-content.volume:/usr/share/nginx/html:Z
[Service]
Restart=always
TimeoutStartSec=120
[Install]
WantedBy=default.target
Create a matching volume definition.
$ vim ~/.config/containers/systemd/web-content.volume
Add the volume configuration.
[Volume]
Label=app=nginx
Reload systemd to pick up the new Quadlet files and start the service.
$ systemctl --user daemon-reload
$ systemctl --user start nginx.service
$ systemctl --user status nginx.service
Active: active (running)
Quadlet also supports .pod, .network, .kube, and .image file types for managing pods, networks, Kubernetes YAML, and image pulls through systemd.
Step 14: Inspect and Monitor Containers
View detailed information about a container with podman inspect.
$ podman inspect webserver --format '{{.NetworkSettings.IPAddress}}'
10.88.0.5
View running processes inside a container.
$ podman top webserver
USER PID PPID %CPU ELAPSED TTY TIME COMMAND
root 1 0 0.000 12m30.123456s ? 0s nginx: master process nginx -g daemon off;
nginx 29 1 0.000 12m29.654321s ? 0s nginx: worker process
Monitor resource usage across all running containers.
$ podman stats --no-stream
ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
a3b7f2c9d4e5 webserver 0.05% 5.234MiB / 3.842GiB 0.13% 1.23kB / 456B 0B / 0B 3
To scan container images for known security vulnerabilities before deploying them, consider using Trivy for container image scanning.
Podman Desktop
Podman Desktop is an open-source graphical application for managing containers, images, pods, and Kubernetes environments from a desktop interface. It supports Podman, Docker, and Kubernetes, and runs on Linux, macOS, and Windows.
Key features include container and pod management, image building, Kubernetes integration, extension support, and migration from Docker Desktop. Download it from the official site at podman-desktop.io. For command-line workflows covered in this guide, the Podman CLI is all you need – Podman Desktop is optional for users who prefer a GUI.
Useful Podman Commands Reference
| Command | Description |
|---|---|
podman pull image:tag | Download a container image from a registry |
podman run -d --name n image | Run a container in detached mode |
podman ps -a | List all containers (running and stopped) |
podman exec -it name cmd | Execute a command in a running container |
podman logs -f name | Follow container log output |
podman stop name | Stop a running container |
podman rm name | Remove a stopped container |
podman rmi image | Remove a container image |
podman pod create --name p | Create a new pod |
podman pod list | List all pods |
podman network create net | Create a custom network |
podman volume create vol | Create a named volume |
podman system prune -a | Remove all unused data (containers, images, volumes) |
podman generate systemd --new | Generate systemd unit from a container |
Conclusion
Podman provides a secure, daemonless container runtime that works as a direct replacement for Docker on Ubuntu 24.04 and Rocky Linux 10. With rootless containers, native pod support, Quadlet systemd integration, and Buildah for image builds, it covers the full container workflow from development to production. For production deployments, enable automatic updates with podman auto-update, configure resource limits on containers, use named volumes for persistent data, and set up monitoring for container health checks.
Related Guides
- Building OCI and Docker Container Images With Buildah
- Run Docker/Podman Containers as Systemd Service
- Scan Docker Container Images for Vulnerabilities with Trivy
- Installing Secure Container Registry with Podman
- Set SELinux Context Label for Podman Custom graphroot Directory


































































