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 |