Podman

Podman is an OCI-compliant container engine. It builds, runs, and manages containers and container images without a central daemon process. Each container runs as a direct child of the process that launched it. The user's shell spawns containers. No background service mediates the operation.

Docker's architecture requires a persistent root-owned daemon (dockerd) to manage all containers on the host. Every Docker command communicates with this daemon over a UNIX socket owned by root. Docker Inc. has repeatedly changed its licensing terms, introduced commercial tiers to previously free tooling, and centralised telemetry collection in its default registry and toolchain. The daemon model is a structural attack surface: a compromised container communicates with a root process by design.

Podman operates without a daemon. Rootless mode runs containers as the invoking user with no elevated privileges. Container processes appear in ps output as regular user processes. Images pull from any OCI-compliant registry without mandatory account creation. The toolchain is FOSS under the Apache 2.0 licence, maintained by Red Hat engineers and the open-source community under the containers project.

Podman is a drop-in CLI replacement for Docker. The command syntax is identical in the vast majority of cases.


Installation

Arch Linux

sudo pacman -S podman

For rootless operation, install the user namespace support tools:

sudo pacman -S fuse-overlayfs slirp4netns

Enable lingering for your user so user-scoped systemd services survive logout:

sudo loginctl enable-linger $USER

Alpine Linux

apk add podman

For rootless support:

apk add fuse-overlayfs slirp4netns shadow-uidmap

Ubuntu / Debian

sudo apt install podman

Ubuntu ships a reasonably current Podman version. For the latest release, use the Kubic repository:

. /etc/os-release
echo "deb https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_${VERSION_ID}/ /" \
  | sudo tee /etc/apt/sources.list.d/kubic.list
curl -fsSL "https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_${VERSION_ID}/Release.key" \
  | sudo gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/kubic.gpg > /dev/null
sudo apt update && sudo apt install podman

Rocky Linux / RHEL

Podman ships in the default repositories on RHEL-family systems. No third-party repository is required.

sudo dnf install podman

Verify Installation

podman --version
podman info
podman info --format json | jq '.host.rootlessNetworkCmd'

Core Architecture

Key Concepts

Component Definition
Image A layered, immutable filesystem assembled from a Dockerfile or Containerfile. Images are stored in a local registry and pulled from remote registries.
Container A running instance of an image. Containers are ephemeral by default. Filesystem changes are discarded on removal unless persisted to a volume.
Volume A named persistent storage location managed by Podman, mounted into containers at runtime.
Network A virtual network connecting containers. Podman uses the CNI (Container Network Interface) or Netavark networking stack.
Pod A group of containers sharing a network namespace, PID namespace, and IPC namespace. Maps directly to the Kubernetes pod concept.
Registry A remote server hosting images. Podman pulls from any OCI-compliant registry. Docker Hub, Quay.io, GitHub Container Registry, and self-hosted registries are all valid sources.

Storage Drivers

Driver Characteristics
overlay Default. Uses overlayfs for copy-on-write layers. Requires kernel 4.0+.
fuse-overlayfs Overlay without kernel privilege. Required for rootless in older kernels.
vfs Full copy of each layer. No copy-on-write. Slow and space-inefficient. Fallback only.
btrfs Native Btrfs subvolumes and snapshots. Requires Btrfs filesystem.
zfs ZFS datasets per layer. Requires ZFS.

Rootless Podman uses fuse-overlayfs when native overlayfs is unavailable in user namespaces. On kernels 5.11+, unprivileged overlayfs is supported natively and fuse-overlayfs is unnecessary.


Images

Searching for Images

podman search {term}
podman search alpine
podman search --filter is-official=true nginx
podman search --format "{{.Name}}\t{{.Description}}" alpine

Podman searches all configured registries by default. The search order and registry list are in /etc/containers/registries.conf.

Pulling Images

Command Action
podman pull {image} Pull latest tag
podman pull {image}:{tag} Pull specific tag
podman pull {registry}/{image}:{tag} Pull from specific registry
podman pull docker.io/library/alpine:3.19 Explicit full reference
podman pull quay.io/fedora/fedora:latest Pull from Quay.io

Listing and Inspecting Images

Command Action
podman images List all local images
podman images -a Include intermediate layers
podman image inspect {image} Full image metadata
podman image history {image} Layer build history
podman image tree {image} Layer tree with sizes

Tagging, Exporting, and Removing

Command Action
podman tag {image} {new-name}:{tag} Tag an image
podman save {image} -o {file}.tar Export image to tarball
podman load -i {file}.tar Import image from tarball
podman rmi {image} Remove image
podman rmi -a Remove all images
podman image prune Remove unused (dangling) images
podman image prune -a Remove all images not referenced by containers

Pushing Images

podman push {image} {registry}/{repo}:{tag}

# log in first if required
podman login quay.io
podman push myimage quay.io/username/myimage:latest

Containers — Running

podman run Syntax

podman run [options] {image} [command] [args]

Essential Run Flags

Flag Action
-d Run in background (detached)
-it Allocate TTY and keep stdin open (interactive)
--rm Remove container automatically on exit
--name {name} Assign a name to the container
-p {host}:{container} Publish container port to host
-v {host-path}:{container-path} Bind-mount a host directory
-v {volume-name}:{container-path} Mount a named volume
-e KEY=VALUE Set environment variable
--env-file {path} Load environment from file
--network {network} Connect to specified network
--user {uid}:{gid} Run as specific user
--userns=keep-id Map host UID to same UID inside container (rootless bind mounts)
--workdir {path} Set working directory
--entrypoint {cmd} Override image entrypoint
--restart {policy} Restart policy (always, on-failure, unless-stopped)
--cpus {N} CPU limit
--memory {size} Memory limit (e.g. 512m, 2g)
--read-only Mount root filesystem read-only
--tmpfs {path} Mount tmpfs at path
--cap-drop ALL Drop all Linux capabilities
--cap-add {cap} Add specific capability
--security-opt no-new-privileges Prevent privilege escalation
--hostname {name} Set container hostname
--label {key}={value} Attach metadata label
--pull always Always pull fresh image before running
--secret {name} Mount a Podman secret into the container

Common Run Patterns

Command Pattern
podman run --rm -it alpine sh Ephemeral interactive shell
podman run -d --name web -p 80:80 nginx Background web server
podman run -d -v mydata:/data --name db postgres Background database with volume
podman run --rm -v $(pwd):/work -w /work alpine make One-shot build container
podman run -d --restart=always --name app myimage Auto-restart application
podman run --rm --read-only --tmpfs /tmp alpine sh Hardened read-only container
podman run --userns=keep-id -v $(pwd):/work -w /work myimage Rootless bind mount preserving host UID

Containers — Managing

Lifecycle Commands

Command Action
podman ps List running containers
podman ps -a List all containers (including stopped)
podman start {name} Start a stopped container
podman stop {name} Stop container (SIGTERM, then SIGKILL after timeout)
podman stop -t 5 {name} Stop with 5-second timeout
podman kill {name} Send SIGKILL immediately
podman kill -s SIGHUP {name} Send specific signal
podman restart {name} Restart container
podman pause {name} Freeze container processes
podman unpause {name} Resume frozen container
podman rm {name} Remove stopped container
podman rm -f {name} Force remove running container
podman rm -a Remove all stopped containers

Filtering Container Lists

# filter by status
podman ps --filter status=exited
podman ps --filter status=running

# filter by image
podman ps --filter ancestor=alpine

# filter by label
podman ps --filter label=env=production

# custom format output
podman ps --format "{{.Names}}\t{{.Status}}\t{{.Ports}}"

Cleaning Up

Command Action
podman system prune Remove stopped containers, unused networks, dangling images
podman system prune -a Remove everything not currently in use
podman system prune --volumes Include volumes in prune
podman container prune Remove all stopped containers
podman image prune Remove dangling images
podman volume prune Remove unused volumes
podman network prune Remove unused networks

Exec and Inspection

Executing Commands in Running Containers

Command Action
podman exec {name} {cmd} Run command in container
podman exec -it {name} bash Interactive bash shell
podman exec -it {name} sh Interactive sh shell (Alpine)
podman exec -u root {name} bash Execute as root
podman exec -e KEY=VALUE {name} {cmd} Execute with environment variable
podman exec -w /opt {name} {cmd} Execute in specific directory

Inspecting Containers

Command Action
podman inspect {name} Full container metadata JSON
podman inspect -f '{{.State.Status}}' {name} Extract specific field
podman inspect -f '{{.NetworkSettings.IPAddress}}' {name} Container IP address
podman top {name} Process table of container
podman stats Live resource usage of all running containers
podman stats {name} Live resource usage of specific container
podman diff {name} Filesystem changes since image
podman port {name} Port mapping table

Logs

Command Action
podman logs {name} All container log output
podman logs -f {name} Follow log output
podman logs --tail 50 {name} Last 50 lines
podman logs --since 1h {name} Logs from last hour
podman logs --until 30m {name} Logs up to 30 minutes ago
podman logs -t {name} Include timestamps

Copying Files

Command Action
podman cp {name}:{path} {local} Copy from container to host
podman cp {local} {name}:{path} Copy from host to container
podman cp {name}:{path} - Stream tar of path to stdout

Volumes

Volumes persist container data beyond the container lifecycle. They are managed by Podman and stored under the Podman storage root.

Volume Commands

Command Action
podman volume create {name} Create named volume
podman volume list List all volumes
podman volume inspect {name} Volume details
podman volume rm {name} Remove volume
podman volume prune Remove all unused volumes
podman volume export {name} -o {file}.tar Export volume contents
podman volume import {name} {file}.tar Import into volume

Mounting Volumes at Runtime

# named volume
podman run -v mydata:/data alpine

# bind mount (host directory)
podman run -v /host/path:/container/path alpine

# bind mount read-only
podman run -v /host/path:/container/path:ro alpine

# tmpfs (in-memory filesystem)
podman run --tmpfs /tmp:size=100m alpine

# volume with specific options
podman run -v mydata:/data:z alpine   # SELinux shared label
podman run -v mydata:/data:Z alpine   # SELinux private label

Volume Inspection and Backup

# find volume mount point on host
podman volume inspect mydata --format '{{.Mountpoint}}'

# back up a volume to a tarball
podman run --rm \
  -v mydata:/source:ro \
  -v $(pwd):/backup \
  alpine tar -czf /backup/mydata-$(date +%Y%m%d).tar.gz -C /source .

# restore volume from backup
podman run --rm \
  -v mydata:/target \
  -v $(pwd):/backup \
  alpine tar -xzf /backup/mydata-20240101.tar.gz -C /target

Networks

Default Networks

Network Type Description
podman bridge Default bridge network. Containers connect here unless specified.
host host Container shares host network stack. No isolation.
none null Container has no network interface. Fully isolated.
slirp4netns user-mode Legacy rootless network default. User-space NAT.
pasta user-mode Modern rootless network default (Podman 5+). Replaces slirp4netns; better performance, native IPv6.

Network Commands

Command Action
podman network list List all networks
podman network inspect {name} Network details
podman network create {name} Create bridge network
podman network create {name} --subnet 10.88.0.0/24 Create with specific subnet
podman network rm {name} Remove network
podman network prune Remove unused networks
podman network connect {network} {container} Connect container to network
podman network disconnect {network} {container} Disconnect container from network

Container DNS

Containers on the same Podman network resolve each other by container name as a hostname. This enables service-to-service communication by name rather than IP address.

# create a network
podman network create appnet

# start containers on the network
podman run -d --name db --network appnet postgres
podman run -d --name api --network appnet myapi

# the api container resolves "db" as the postgres container's address
# connection string: postgresql://db:5432/mydb

Port Publishing

# publish single port
podman run -p 8080:80 nginx

# publish to specific host interface
podman run -p 127.0.0.1:8080:80 nginx

# publish multiple ports
podman run -p 80:80 -p 443:443 nginx

# publish all exposed ports to random host ports
podman run -P nginx

# view port mappings
podman port mycontainer

Pods

Pods group containers that share network and IPC namespaces. All containers in a pod communicate via localhost. The pod model maps directly to Kubernetes pods, enabling local development that mirrors production Kubernetes deployments.

Pod Commands

Command Action
podman pod create --name {name} Create empty pod
podman pod create --name {name} -p 8080:80 Create pod with port mapping
podman pod list List pods
podman pod inspect {name} Pod details
podman pod start {name} Start all containers in pod
podman pod stop {name} Stop all containers in pod
podman pod restart {name} Restart all containers in pod
podman pod rm {name} Remove stopped pod
podman pod rm -f {name} Force remove pod and containers
podman pod stats {name} Resource usage of pod
podman pod top {name} Process table of all pod containers

Adding Containers to Pods

# add container to existing pod
podman run -d --pod {pod-name} --name {container} {image}

# example: web application pod
podman pod create --name webapp -p 80:80 -p 443:443

podman run -d \
  --pod webapp \
  --name nginx \
  -v /etc/nginx:/etc/nginx:ro \
  nginx

podman run -d \
  --pod webapp \
  --name app \
  -e DATABASE_URL=postgresql://db:5432/mydb \
  myapp

podman run -d \
  --pod webapp \
  --name db \
  -v pgdata:/var/lib/postgresql/data \
  -e POSTGRES_PASSWORD=secret \
  postgres

All three containers share the pod's network namespace. nginx proxies to app on localhost:3000. app connects to db on localhost:5432.

Kubernetes YAML — Generate and Play

# generate kubernetes YAML from a running pod
podman kube generate {pod-name} > pod.yaml

# deploy a kubernetes YAML manifest with podman
podman kube play pod.yaml

# tear down resources created by a manifest
podman kube play --down pod.yaml

podman kube generate (formerly podman generate kube) produces a Kubernetes-compatible manifest. podman kube play is the inverse: it spins up pods and containers from a YAML manifest without requiring a Kubernetes cluster.


Building Images

Podman builds OCI images from a Containerfile (identical syntax to Dockerfile).

Build Commands

Command Action
podman build -t {name}:{tag} . Build from Containerfile in current directory
podman build -t {name} -f {path} Build from specific file
podman build --no-cache -t {name} . Build without layer cache
podman build --build-arg KEY=VALUE -t {name} . Pass build argument
podman build --target {stage} -t {name} . Build specific multi-stage target
podman build --squash -t {name} . Merge all layers into one
podman build --format docker -t {name} . Build Docker-format image

Containerfile Reference

# BASE IMAGE
FROM alpine:3.19

# BUILD ARGUMENTS
ARG APP_VERSION=1.0.0
ARG BUILD_DATE

# METADATA LABELS
LABEL maintainer="you@domain.tld"
LABEL version="${APP_VERSION}"
LABEL build-date="${BUILD_DATE}"

# ENVIRONMENT VARIABLES
ENV APP_HOME=/opt/app
ENV PORT=8080

# DEPENDENCIES
RUN apk add --no-cache \
    ca-certificates \
    tzdata \
    curl

# WORKING DIRECTORY
WORKDIR ${APP_HOME}

# COPY SOURCE
COPY --chown=1000:1000 . .

# NON-ROOT USER
RUN adduser -D -u 1000 appuser
USER appuser

# EXPOSE PORT
EXPOSE ${PORT}

# HEALTHCHECK
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD curl -f http://localhost:${PORT}/health || exit 1

# ENTRYPOINT AND DEFAULT COMMAND
ENTRYPOINT ["/opt/app/myapp"]
CMD ["--config", "/opt/app/config.yaml"]

Multi-Stage Builds

Multi-stage builds produce minimal final images by compiling in one stage and copying only the output to the final image.

# BUILD STAGE
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /bin/myapp ./cmd/myapp

# RUNTIME STAGE
FROM alpine:3.19
RUN apk add --no-cache ca-certificates tzdata
COPY --from=builder /bin/myapp /usr/local/bin/myapp
RUN adduser -D -u 1000 appuser
USER appuser
ENTRYPOINT ["/usr/local/bin/myapp"]

The final image contains only the compiled binary and runtime dependencies. No Go toolchain, no source code, no build cache.

BuildKit and Buildah

Podman uses Buildah as its build backend. For more granular image construction, invoke Buildah directly:

# build from a working container rather than a Dockerfile
ctr=$(buildah from alpine:3.19)
buildah run $ctr -- apk add --no-cache nginx
buildah config --port 80 $ctr
buildah config --entrypoint '["nginx", "-g", "daemon off;"]' $ctr
buildah commit $ctr mynginx:latest
buildah rm $ctr

Rootless Operation

Rootless mode is Podman's default when invoked as a non-root user. Containers run within user namespaces, mapping the container's root (UID 0) to the invoking user's UID on the host.

Prerequisites

Rootless Podman requires subUID and subGID ranges assigned to your user:

# verify ranges exist
grep $USER /etc/subuid
grep $USER /etc/subgid

# add ranges if missing
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER

Rootless Storage Location

Path Contents
~/.local/share/containers/storage Images and container layers
~/.config/containers/storage.conf Storage configuration
~/.config/containers/registries.conf Registry configuration

Rootless Networking

Rootless containers use pasta (Podman 5+) or slirp4netns for outbound connectivity. Both provide user-space NAT without kernel privileges. Port numbers below 1024 require explicit configuration:

# publishing a privileged port in rootless mode
# the host port must be >= 1024 unless sysctl net.ipv4.ip_unprivileged_port_start is lowered
podman run -p 8080:80 nginx    # host port 8080 maps to container 80

# lower the unprivileged port threshold (persist in /etc/sysctl.d/)
echo "net.ipv4.ip_unprivileged_port_start=80" | sudo tee /etc/sysctl.d/99-podman-ports.conf
sudo sysctl -p /etc/sysctl.d/99-podman-ports.conf
# afterwards, rootless containers can bind host port 80

Systemd User Scope Environment

User-scoped systemd services require these environment variables to be correctly set. Lingering ensures they survive after logout.

# enable lingering (run once per user)
sudo loginctl enable-linger $USER

# verify XDG_RUNTIME_DIR is set (required for the podman socket)
echo $XDG_RUNTIME_DIR    # should be /run/user/$(id -u)

# the rootless podman socket path
echo $XDG_RUNTIME_DIR/podman/podman.sock

# DBUS is required for systemctl --user commands
echo $DBUS_SESSION_BUS_ADDRESS

If XDG_RUNTIME_DIR is unset in a non-login shell (e.g. SSH without PAM), export it manually:

export XDG_RUNTIME_DIR=/run/user/$(id -u)

Rootless vs Root Comparison

Feature Rootless Root
Container UID 0 Maps to invoking user UID Maps to actual root
Host process visibility Visible as user process Visible as root process
Port < 1024 Requires sysctl adjustment Available directly
Storage location ~/.local/share/containers /var/lib/containers
Network driver pasta / slirp4netns Netavark (full bridge support)
Privilege escalation risk None Root socket ownership

Secrets

Podman manages secrets as named, encrypted blobs stored in the local secret store. Secrets are injected into containers at runtime and never appear in image layers, environment variable dumps, or podman inspect output.

Secret Commands

Command Action
podman secret create {name} {file} Create secret from file
echo "value" \| podman secret create {name} - Create secret from stdin
podman secret list List all secrets
podman secret inspect {name} Secret metadata (not the value)
podman secret rm {name} Remove secret

Using Secrets at Runtime

# create a secret from stdin
echo "hunter2" | podman secret create db_password -

# mount the secret into a container (appears at /run/secrets/{name})
podman run -d \
  --name myapp \
  --secret db_password \
  myimage

# the container reads the secret from the filesystem
# cat /run/secrets/db_password

Custom Secret Mount Target

# mount secret to a specific path
podman run -d \
  --secret db_password,target=/etc/myapp/db.passwd \
  myimage

Secrets in Quadlet units use the Secret= key. See the Quadlet section for .container examples.


Podman Compose

Podman Compose processes Docker Compose YAML files and translates them to Podman commands. It provides compatibility with projects using docker-compose.yml without requiring Docker.

Installation

Platform Command
Arch Linux sudo pacman -S podman-compose
pip (universal) pip install podman-compose --break-system-packages

Commands

Command Action
podman-compose up -d Start services in background
podman-compose down Stop and remove containers
podman-compose ps List service containers
podman-compose logs -f Follow all service logs
podman-compose logs -f {service} Follow specific service logs
podman-compose exec {service} bash Shell into service container
podman-compose build Build all service images
podman-compose pull Pull all service images
podman-compose restart {service} Restart a service

Example Compose File

# COMPOSE CONFIGURATION
# three-service web application stack

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certs:/etc/nginx/certs:ro
    depends_on:
      - app
    restart: unless-stopped
  app:
    build:
      context: .
      dockerfile: Containerfile
    environment:
      - DATABASE_URL=postgresql://appuser:${DB_PASSWORD}@db:5432/appdb
      - PORT=3000
    depends_on:
      - db
    restart: unless-stopped
  db:
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=appuser
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - POSTGRES_DB=appdb
    volumes:
      - pgdata:/var/lib/postgresql/data
    restart: unless-stopped
volumes:
  pgdata:

Systemd Integration

Generating Unit Files (Legacy)

podman generate systemd produces a systemd unit file from a running container or pod. It is the older approach, retained for compatibility. Quadlet supersedes it for new deployments.

# generate unit for a container
podman generate systemd --name {container} --files --new

# generate units for a pod
podman generate systemd --name {pod} --files --new

The --new flag generates units that recreate the container on start rather than attaching to an existing stopped container.

Installing User-Scoped Units

mkdir -p ~/.config/systemd/user
cp container-{name}.service ~/.config/systemd/user/

systemctl --user daemon-reload
systemctl --user enable --now container-{name}.service
journalctl --user -u container-{name}.service -f

Installing System-Scoped Units

sudo cp container-{name}.service /etc/systemd/system/

sudo systemctl daemon-reload
sudo systemctl enable --now container-{name}.service
journalctl -u container-{name}.service -f

Quadlet

Quadlet is the native, declarative approach to running Podman containers under systemd. Unit files with Podman-specific sections (.container, .volume, .network, .pod, .kube) are placed in a watched directory. systemd-generator compiles them into standard .service and .mount units at boot or on daemon reload. No podman generate systemd required. No imperative setup scripts.

Quadlet is the correct approach for production container services on any systemd-based host.

Unit File Locations

Scope Path
User (rootless) ~/.config/containers/systemd/
System (root) /etc/containers/systemd/

After placing or editing unit files, reload the systemd user daemon to compile them:

systemctl --user daemon-reload
# quadlet generates service units under ~/.config/systemd/user/ automatically

Verify the generated unit was created:

systemctl --user status {name}.service

.container Unit

The core unit type. Defines a single container as a systemd service.

~/.config/containers/systemd/myapp.container

[Unit]
Description=My Application
After=network-online.target
Wants=network-online.target

[Container]
Image=docker.io/library/myapp:latest
ContainerName=myapp
PublishPort=8080:80
Volume=mydata.volume:/data
Network=appnet.network
Environment=APP_ENV=production
Secret=db_password,target=/run/secrets/db_password
HealthCmd=curl -f http://localhost:80/health
HealthInterval=30s
HealthTimeout=5s
HealthRetries=3
Label=app=myapp
Label=env=production

[Service]
Restart=always
TimeoutStartSec=60

[Install]
WantedBy=default.target

Reference other Quadlet units by filename (e.g. mydata.volume, appnet.network). Systemd resolves the dependency order automatically.

.volume Unit

Declares a named Podman volume managed by systemd. Containers referencing {name}.volume will wait for this unit to be active before starting.

~/.config/containers/systemd/mydata.volume

[Unit]
Description=Persistent data volume for myapp

[Volume]
VolumeName=mydata
# optional: label the volume
Label=managed-by=quadlet

[Install]
WantedBy=default.target

The generated service is mydata-volume.service. Containers reference it as mydata.volume:/mountpoint in their [Container] section.

.network Unit

Declares a Podman network managed by systemd. All containers in the network reference the .network filename.

~/.config/containers/systemd/appnet.network

[Unit]
Description=Application bridge network

[Network]
NetworkName=appnet
Subnet=10.89.1.0/24
Gateway=10.89.1.1
IPRange=10.89.1.128/25

[Install]
WantedBy=default.target

Containers join this network with Network=appnet.network in their [Container] section. Name resolution between containers works by container name.

.pod Unit

Defines a Podman pod. Containers referencing the pod share network and IPC namespaces and communicate over localhost.

~/.config/containers/systemd/webapp.pod

[Unit]
Description=Web application pod

[Pod]
PodName=webapp
PublishPort=80:80
PublishPort=443:443

[Install]
WantedBy=default.target

~/.config/containers/systemd/webapp-nginx.container

[Unit]
Description=Nginx frontend
After=webapp-pod.service
Requires=webapp-pod.service

[Container]
Image=docker.io/library/nginx:alpine
ContainerName=webapp-nginx
Pod=webapp.pod
Volume=/etc/nginx:/etc/nginx:ro

[Service]
Restart=always

[Install]
WantedBy=default.target

~/.config/containers/systemd/webapp-app.container

[Unit]
Description=Application backend
After=webapp-pod.service
Requires=webapp-pod.service

[Container]
Image=docker.io/library/myapp:latest
ContainerName=webapp-app
Pod=webapp.pod
Environment=PORT=3000
Environment=DATABASE_URL=postgresql://localhost:5432/appdb
Secret=db_password

[Service]
Restart=always

[Install]
WantedBy=default.target

~/.config/containers/systemd/webapp-db.container

[Unit]
Description=PostgreSQL database
After=webapp-pod.service
Requires=webapp-pod.service

[Container]
Image=docker.io/library/postgres:16-alpine
ContainerName=webapp-db
Pod=webapp.pod
Volume=pgdata.volume:/var/lib/postgresql/data
Secret=db_password,target=/run/secrets/db_password
Environment=POSTGRES_USER=appuser
Environment=POSTGRES_DB=appdb
Environment=POSTGRES_PASSWORD_FILE=/run/secrets/db_password

[Service]
Restart=always

[Install]
WantedBy=default.target

All three containers communicate over localhost within the pod. Ports 80 and 443 are published from the pod unit, not from individual containers.

.kube Unit

Runs a Kubernetes YAML manifest directly under systemd via podman kube play.

~/.config/containers/systemd/myapp.kube

[Unit]
Description=Deploy myapp from Kubernetes manifest
After=network-online.target
Wants=network-online.target

[Kube]
Yaml=/opt/manifests/myapp-pod.yaml

[Service]
Restart=always

[Install]
WantedBy=default.target

Quadlet calls podman kube play on start and podman kube play --down on stop. The manifest is reapplied on each service restart.

Quadlet Common Keys Reference

[Container] keys

Key Description
Image= Full image reference (use fully-qualified path)
ContainerName= Name assigned to the container
PublishPort= Port mapping host:container; repeat for multiple ports
Volume= Volume mount; reference .volume units by filename
Network= Network to join; reference .network units by filename
Environment= Environment variable KEY=VALUE; repeat for multiple
EnvironmentFile= Path to env file
Secret= Secret name; optional ,target=path to set mount location
Pod= Pod to join; reference .pod units by filename
HealthCmd= Healthcheck command
HealthInterval= Healthcheck polling interval
HealthTimeout= Healthcheck timeout
HealthRetries= Healthcheck failure threshold before unhealthy
Label= Container label key=value; repeat for multiple
AddCapability= Add Linux capability
DropCapability= Drop Linux capability
ReadOnly= Mount root filesystem read-only (true/false)
Tmpfs= Mount tmpfs at path
User= Run as uid:gid
UserNS= User namespace mode (e.g. keep-id)
AutoUpdate= Enable auto-update (registry or local)

[Volume] keys

Key Description
VolumeName= Name of the Podman volume
Label= Label attached to the volume
Driver= Volume driver
Options= Driver options

[Network] keys

Key Description
NetworkName= Name of the network
Subnet= CIDR subnet
Gateway= Gateway address
IPRange= DHCP range within subnet
IPv6= Enable IPv6 (true/false)
Label= Network label

[Pod] keys

Key Description
PodName= Name of the pod
PublishPort= Port mapping from pod to host
Network= Network to attach the pod to
Volume= Volume to attach to the pod

Auto-Update with Quadlet

Quadlet integrates with podman-auto-update to pull fresh images and restart services automatically.

Enable auto-update on a container unit:

[Container]
Image=docker.io/library/myapp:latest
AutoUpdate=registry

The AutoUpdate=registry flag tells Podman to check the remote registry for a newer digest on the latest tag. AutoUpdate=local checks the local image store (useful after a CI push to localhost).

Run the update manually:

podman auto-update

Enable the systemd timer for automatic periodic updates:

# user scope
systemctl --user enable --now podman-auto-update.timer

# system scope
sudo systemctl enable --now podman-auto-update.timer

Check update status and which containers were restarted:

podman auto-update --dry-run

Quadlet Workflow Summary

# 1. write unit files
mkdir -p ~/.config/containers/systemd/
vim ~/.config/containers/systemd/myapp.container
vim ~/.config/containers/systemd/mydata.volume
vim ~/.config/containers/systemd/appnet.network

# 2. reload systemd to compile quadlet units
systemctl --user daemon-reload

# 3. verify the generated service unit
systemctl --user cat myapp.service

# 4. start the service
systemctl --user enable --now myapp.service

# 5. check status
systemctl --user status myapp.service

# 6. follow logs
journalctl --user -u myapp.service -f

# 7. update image and restart
podman auto-update
# or manually:
podman pull docker.io/library/myapp:latest
systemctl --user restart myapp.service

Registries and Authentication

Configuring Registries

Registry search order and default prefixes are configured in /etc/containers/registries.conf (system) or ~/.config/containers/registries.conf (user).

# REGISTRIES CONFIGURATION

[registries.search]
# search order for unqualified image names
registries = ['docker.io', 'quay.io']

[registries.block]
# block pulls from specific registries
registries = []

Short image names (alpine, nginx) resolve according to this search order. Using fully-qualified names (docker.io/library/alpine) is unambiguous and preferred for reproducible environments.

Authentication

# log in to a registry
podman login docker.io
podman login quay.io
podman login registry.yourhost.tld

# log in with explicit credentials (for scripting; prefer --password-stdin)
echo "$REGISTRY_PASSWORD" | podman login -u username --password-stdin registry.yourhost.tld

# log out
podman logout docker.io

# view stored credentials
cat ~/.docker/config.json   # podman shares docker's credential store

Self-Hosted Registry

# run a local OCI registry
podman run -d \
  --name registry \
  -p 5000:5000 \
  -v registrydata:/var/lib/registry \
  --restart always \
  docker.io/library/registry:2

# push an image to the local registry
podman tag myimage localhost:5000/myimage:latest
podman push localhost:5000/myimage:latest

# pull from the local registry
podman pull localhost:5000/myimage:latest

Configure Podman to treat the local registry as insecure (no TLS) in /etc/containers/registries.conf:

[[registry]]
location = "localhost:5000"
insecure = true

Configuration

Configuration Files

File Scope Purpose
/etc/containers/registries.conf System Registry search order and blocks
~/.config/containers/registries.conf User User override of registry config
/etc/containers/storage.conf System Storage driver and path configuration
~/.config/containers/storage.conf User User storage configuration
/etc/containers/containers.conf System Container defaults
~/.config/containers/containers.conf User User container defaults
~/.config/containers/systemd/ User Quadlet unit files
/etc/containers/systemd/ System Quadlet unit files (system scope)

containers.conf Key Options

[containers]
# default capabilities to drop
default_capabilities = [
  "CHOWN",
  "DAC_OVERRIDE",
  "FOWNER",
  "FSETID",
  "KILL",
  "NET_BIND_SERVICE",
  "SETGID",
  "SETPCAP",
  "SETUID",
  "SYS_CHROOT"
]

# default ulimits
default_ulimits = [
  "nofile=1048576:1048576"
]

# log driver
log_driver = "journald"

# timezone to set inside containers
tz = "UTC"

[network]
# default network name
default_network = "podman"

# network backend: netavark or cni
network_backend = "netavark"

Environment Variables

Variable Effect
CONTAINERS_CONF Override containers.conf path
CONTAINERS_STORAGE_CONF Override storage.conf path
CONTAINER_HOST Remote Podman socket URI
CONTAINER_SSHKEY SSH key for remote Podman
BUILDAH_FORMAT Default image format (docker or oci)
XDG_RUNTIME_DIR Runtime directory; must be set for rootless systemd socket

Cheatsheet

Images

Command Action
podman pull {image} Pull image
podman images List images
podman rmi {image} Remove image
podman image prune Remove dangling images
podman image prune -a Remove all unused images
podman save {image} -o file.tar Export image
podman load -i file.tar Import image
podman tag {image} {new}:{tag} Tag image

Running Containers

Command Action
podman run --rm -it alpine sh Ephemeral shell
podman run -d --name {n} {img} Background container
podman run -p {h}:{c} {img} Publish port
podman run -v {name}:{path} {img} Mount volume
podman run -v {host}:{path} {img} Bind mount
podman run -e KEY=VALUE {img} Set environment variable
podman run --network {net} {img} Connect to network
podman run --rm {img} {cmd} One-shot command
podman run --userns=keep-id -v {host}:{path} {img} Rootless bind mount with host UID
podman run --secret {name} {img} Inject secret

Container Lifecycle

Command Action
podman ps List running containers
podman ps -a List all containers
podman start {name} Start container
podman stop {name} Stop container
podman restart {name} Restart container
podman rm {name} Remove stopped container
podman rm -f {name} Force remove
podman rm -a Remove all stopped

Exec and Logs

Command Action
podman exec -it {name} bash Interactive shell
podman exec -it {name} sh Sh shell
podman logs {name} View logs
podman logs -f {name} Follow logs
podman logs --tail 50 {name} Last 50 lines
podman top {name} Process table
podman stats Live resource usage
podman inspect {name} Full metadata

Volumes

Command Action
podman volume create {name} Create volume
podman volume list List volumes
podman volume inspect {name} Volume details
podman volume rm {name} Remove volume
podman volume prune Remove unused volumes

Networks

Command Action
podman network list List networks
podman network create {name} Create network
podman network inspect {name} Network details
podman network rm {name} Remove network
podman network connect {net} {ctr} Connect container

Pods

Command Action
podman pod create --name {n} -p {h}:{c} Create pod
podman pod list List pods
podman run -d --pod {name} {image} Add container to pod
podman pod start {name} Start pod
podman pod stop {name} Stop pod
podman pod rm {name} Remove pod
podman kube generate {name} Export pod as Kubernetes YAML
podman kube play {file}.yaml Deploy manifest

Secrets

Command Action
echo "val" \| podman secret create {name} - Create secret from stdin
podman secret create {name} {file} Create secret from file
podman secret list List secrets
podman secret rm {name} Remove secret

Building

Command Action
podman build -t {name} . Build from Containerfile
podman build --no-cache -t {name} . Build without cache
podman build --target {stage} -t {name} . Multi-stage target
podman push {image} {registry}/{repo}:{tag} Push image
podman login {registry} Authenticate to registry

Quadlet

Task Command
Place unit files ~/.config/containers/systemd/*.container
Reload and compile systemctl --user daemon-reload
Start service systemctl --user enable --now {name}.service
View generated unit systemctl --user cat {name}.service
Follow logs journalctl --user -u {name}.service -f
Dry-run auto-update podman auto-update --dry-run
Run auto-update podman auto-update
Enable update timer systemctl --user enable --now podman-auto-update.timer

Cleanup

Command Action
podman system prune Remove unused resources
podman system prune -a Remove all unused resources
podman system prune --volumes Include volumes
podman container prune Remove stopped containers
podman image prune -a Remove unused images