Podman
Run rootless containers and pods with Podman, mount volumes past SELinux, and manage them as systemd services with Quadlet and auto-update.
On this page
Cheatsheet#
| Task | Command |
|---|---|
| Rootless, cgroup and network facts | podman info --format '{{.Host.Security.Rootless}} {{.Host.CgroupsVersion}} {{.Host.NetworkBackend}}' |
| Run detached with a published port | podman run -d --name my-app -p 8080:8080 registry.example.com/my-app:1.4 |
| Bind mount with a private SELinux label | podman run -v ./data:/data:Z ... |
| Run as your own UID inside the container | podman run --userns=keep-id ... |
| Shell in a running container | podman exec -it my-app sh |
| Follow logs | podman logs -f --since 10m my-app |
| Inspect a field | podman inspect -f '{{.State.Health.Status}}' my-app |
| Create a network with DNS | podman network create my-net |
| Create a pod that publishes a port | podman pod create --name my-pod -p 8080:80 |
| Run in that pod | podman run -d --pod my-pod nginx:1.29 |
| Reload Quadlet units | systemctl --user daemon-reload |
| Start a Quadlet service | systemctl --user start my-app.service |
| Show what Quadlet generated | /usr/lib/systemd/system-generators/podman-system-generator --user --dryrun |
| Keep user services running after logout | loginctl enable-linger "$USER" |
| Check for image updates without applying | podman auto-update --dry-run |
| Login to a registry | podman login registry.example.com |
| Run a command as the user-namespace root | podman unshare chown -R 1000:1000 ./data |
| Disk usage by images, containers, volumes | podman system df -v |
| Remove stopped containers, dangling images | podman system prune |
| Health check now | podman healthcheck run my-app |
| Kubernetes YAML from a pod | podman kube generate my-pod > my-pod.yaml |
| Run Kubernetes YAML | podman kube play my-pod.yaml |
Behaviour below is Podman 5.x on Fedora or RHEL 9 with netavark, aardvark-dns, pasta and crun. Check with podman version and podman info. Reference: docs.podman.io.
Rootless and how it differs from Docker#
Podman has no daemon. Each podman invocation forks the container as a child process (via conmon, which stays behind to hold the PTY and exit code), so containers belong to the user who started them, appear in that user’s process tree and die with the user session unless systemd keeps them alive. The CLI is deliberately Docker-compatible: alias docker=podman covers nearly everything on this page, and podman.socket serves the Docker REST API for tools that need a socket. The differences that matter in operation:
| Docker | Podman |
|---|---|
Root daemon; docker group is root-equivalent | No daemon; rootless by default, root only with sudo podman |
One image store in /var/lib/docker | One store per user: ~/.local/share/containers/storage; root uses /var/lib/containers/storage. sudo podman ps does not show your containers |
| Bridge networking with iptables as root | Rootless: pasta user-mode stack; rootful: netavark bridge with nftables |
| Compose built in | podman compose delegates to podman-compose or docker-compose |
restart: always handled by the daemon | Restarts are systemd’s job: Quadlet units |
| Container UID 0 is host root | Container UID 0 is your UID; other UIDs map into your subordinate range |
Rootless containers run inside a user namespace. Your UID maps to 0 in the container and the ranges in /etc/subuid and /etc/subgid supply the remaining IDs, so container UID 1 becomes the first ID in your range. useradd on Fedora and RHEL allocates 65536 IDs per new user; accounts created by other means (IdM, adduser on older systems, cloud images) often have none.
grep "^$USER:" /etc/subuid /etc/subgid # user:start:count, for example jfields:100000:65536
podman unshare cat /proc/self/uid_map # the mapping podman actually uses
# 0 1000 1
# 1 100000 65536
sudo usermod --add-subuids 200000-265535 --add-subgids 200000-265535 my-user # allocate a range
podman system migrate # required after changing subuid/subgid so the pause process restartspodman unshare runs a command inside that user namespace, which is how you fix file ownership for volumes: a file owned by container UID 1000 appears on the host as UID 100999, and podman unshare chown 1000:1000 file sets that without needing root. --userns=keep-id maps your host UID to the same number inside the container instead, so a container process running as your UID reads and writes bind mounts as you; --userns=keep-id:uid=1000,gid=1000 maps you onto a specific in-container ID for images that hard-code one. --userns=auto (rootful only) gives every container a disjoint range from /etc/subuid entries for containers.
Rootless networking uses pasta (default since Podman 5.0; slirp4netns in 4.x and still selectable with --network=slirp4netns). pasta copies the host’s addresses and routes into the namespace, so the container sees the host’s IP rather than a 10.0.2.x one, and preserves client source addresses on published ports, which slirp4netns did not. The host is reachable from a container as host.containers.internal. Containers on the default network cannot resolve each other by name; see Networks and DNS between containers. Publishing a port below 1024 needs net.ipv4.ip_unprivileged_port_start lowered (see Troubleshooting).
Resource limits under rootless need cgroups v2 with the controllers delegated to the user slice. Fedora and RHEL 9 delegate cpu, memory, pids and io by default; podman run --memory 512m with an “OCI runtime error: … controller not available” means delegation is missing, fixed with a user@.service drop-in ([Service] Delegate=memory pids cpu cpuset io) and systemctl daemon-reload.
Images and registries#
Podman rejects short names it cannot resolve unambiguously. podman pull nginx consults unqualified-search-registries in /etc/containers/registries.conf and prompts on a terminal; in a script it fails. Use fully qualified names everywhere so the image you run is the image you meant.
podman pull docker.io/library/nginx:1.29 # fully qualified: no prompt, no ambiguity
podman pull --platform linux/arm64 registry.example.com/my-app:1.4
podman images --format '{{.Repository}}:{{.Tag}} {{.Size}} {{.Created}}'
podman image inspect -f '{{.Digest}} {{.Architecture}}' registry.example.com/my-app:1.4
podman image tree registry.example.com/my-app:1.4 # layers and what shares them
podman login registry.example.com # credentials in ${XDG_RUNTIME_DIR}/containers/auth.json
podman login --authfile ~/.config/containers/auth.json registry.example.com # persistent location
podman build -t registry.example.com/my-app:1.4 -f Containerfile .
podman build --platform linux/amd64,linux/arm64 --manifest registry.example.com/my-app:1.4 .
podman manifest push --all registry.example.com/my-app:1.4 # multi-arch manifest list
podman push registry.example.com/my-app:1.4
podman save -o my-app.tar registry.example.com/my-app:1.4 && podman load -i my-app.tar
podman image prune -a # removes every image without a container: rebuilds and re-pulls follow/etc/containers/registries.conf.d/ takes drop-ins for mirrors, insecure registries and short-name aliases, and /etc/containers/policy.json decides which signatures a pull requires. Containerfile syntax is Dockerfile syntax; see Docker for the build-stage patterns.
# /etc/containers/registries.conf.d/mirror.conf
[[registry]]
prefix = "docker.io"
location = "docker.io"
[[registry.mirror]]
location = "mirror.example.com/docker" # tried first; falls back to docker.io on failure
[[registry]]
location = "registry.internal.example.com"
insecure = true # plain HTTP or untrusted TLS; lab use onlyRunning containers#
podman run -d --name my-app \
-p 127.0.0.1:8080:8080 \ # bind to loopback; without an address, every interface
-e TZ=Australia/Melbourne --env-file ./my-app.env \
-v my-app-data:/var/lib/my-app \ # named volume; created on first use
-v ./config:/etc/my-app:ro,Z \ # read-only bind mount with a private SELinux label
--secret my-app-token,type=env,target=API_TOKEN \
--memory 512m --pids-limit 256 \
--read-only --tmpfs /tmp \ # immutable rootfs; writable scratch
--cap-drop ALL --security-opt no-new-privileges \
--health-cmd 'curl -fsS http://localhost:8080/healthz || exit 1' --health-interval 30s \
--label io.containers.autoupdate=registry \
registry.example.com/my-app:1.4
podman ps -a --format '{{.Names}}\t{{.Status}}\t{{.Ports}}'
podman stop -t 20 my-app # SIGTERM, SIGKILL after 20 s (default 10)
podman rm -f my-app # stop and remove; anonymous volumes stay unless -v
podman stats --no-stream # CPU, memory, network per container
podman top my-app # processes with host and container PIDs
podman port my-app
podman cp my-app:/var/log/my-app/app.log .
podman diff my-app # files changed against the image layer
podman commit my-app registry.example.com/my-app:debug # snapshot; for debugging, not for releasepodman secret create my-app-token - reads the value from stdin and stores it in the user’s secret store, so it never appears in podman inspect output or the process list; type=env exposes it as an environment variable and the default type=mount places it at /run/secrets/<name>. --restart=always works only while the process that created the container is alive to restart it; on a server the restart policy belongs in the Quadlet unit.
Health checks in Podman run from systemd transient timers, not from a daemon. Rootless health checks therefore need a systemd user session (XDG_RUNTIME_DIR set, loginctl enable-linger). --health-on-failure=restart (4.3+) restarts an unhealthy container, --health-startup-cmd adds a separate start-up probe, and podman healthcheck run my-app runs the check once and prints the result.
Volumes and SELinux labels#
Named volumes live in the storage directory (~/.local/share/containers/storage/volumes/<name>/_data) and inherit a label that containers can use. Bind mounts keep the label the host directory already has, typically user_home_t or default_t, which container processes (container_t) may not read or write. The volume suffixes fix that, and the ownership suffix fixes the UID mismatch that user namespaces create.
| Suffix | Effect |
|---|---|
:Z | Relabel to container_file_t with a category unique to this container. One container per directory |
:z | Relabel to container_file_t shared; several containers can use the directory |
:U | chown the mount recursively to the container’s user, translated through the user namespace |
:ro | Read-only mount |
:O | Overlay: the container sees the directory but writes go to a discarded upper layer |
:nocopy | Do not copy image content into an empty named volume on first mount |
podman run -v ./site:/usr/share/nginx/html:ro,Z docker.io/library/nginx:1.29
podman run -v ./pgdata:/var/lib/postgresql/data:Z,U docker.io/library/postgres:18 # relabel and chown to the postgres user
podman volume create my-app-data
podman volume inspect my-app-data -f '{{.Mountpoint}}'
podman volume export my-app-data -o my-app-data.tar # tarball of the contents
podman volume import my-app-data my-app-data.tar
podman volume prune # deletes every volume not attached to a container
podman unshare ls -ln "$(podman volume inspect my-app-data -f '{{.Mountpoint}}')" # view with container-side UIDsRelabelling system directories
:Z and :z run chcon on the host path recursively. Mounting /home/user or /var/lib with :Z relabels everything beneath it and breaks the processes that depend on the old labels. Mount the smallest directory that does the job, and restorecon -Rv path restores the policy default.
The Users, permissions and SELinux page covers reading denials with ausearch -m avc -ts recent.
Networks and DNS between containers#
Rootful Podman uses netavark bridges with nftables rules; rootless containers get pasta for the host side, and netavark still builds the bridge inside the user namespace when a container joins a named network. Name resolution comes from aardvark-dns, which runs only for user-defined networks: the default podman network has DNS disabled, so containers on it must use published ports or IP addresses to reach each other.
podman network create my-net # bridge, DNS enabled, subnet chosen automatically
podman network create --subnet 10.89.10.0/24 --gateway 10.89.10.1 my-net
podman network create --internal my-backend # no route out: for databases
podman network ls; podman network inspect my-net
podman run -d --name my-db --network my-net docker.io/library/postgres:18
podman run -d --name my-app --network my-net,my-backend -p 8080:8080 registry.example.com/my-app:1.4 # two networks
podman run --rm --network my-net docker.io/library/busybox:1.37 nslookup my-db # resolves through aardvark-dns
podman network connect my-net my-existing-container; podman network disconnect my-net my-existing-container
podman run --network host ... # host namespace; rootless still cannot bind below 1024Containers on the same user-defined network resolve each other by container name and by --network-alias. Within a pod they share one network namespace and reach each other on localhost. A container reaches the host as host.containers.internal (pasta maps it to 169.254.1.2). Because pasta gives the container the host’s own address, a service the host binds to 127.0.0.1 only is not reachable from inside; bind it to another address or to 0.0.0.0 and firewall it.
Pods#
A pod is a group of containers sharing the network namespace (and optionally PID and IPC namespaces) anchored by a small infra container that holds them open. Ports are published on the pod, not on member containers, and members talk over localhost. This is the same model as a Kubernetes pod, which is why podman kube generate and podman kube play translate cleanly.
podman pod create --name my-pod -p 8080:80 --network my-net
podman run -d --pod my-pod --name my-pod-web docker.io/library/nginx:1.29
podman run -d --pod my-pod --name my-pod-app registry.example.com/my-app:1.4 # nginx proxies to localhost:8080
podman pod ps; podman pod inspect my-pod
podman pod stop my-pod; podman pod start my-pod
podman pod rm -f my-pod # removes the pod and every container in it
podman kube generate my-pod > my-pod.yaml # Kubernetes Pod manifest with the running configuration
podman kube play my-pod.yaml # creates the pod from YAML; accepts Deployment, PersistentVolumeClaim, ConfigMap, Secret
podman kube play --replace my-pod.yaml # recreate; --down tears it down
podman kube down my-pod.yamlPublished ports cannot be added to a pod after creation; recreate the pod. Containers that join a pod cannot use --network or -p of their own.
Quadlet systemd units#
Quadlet is a systemd generator shipped with Podman 4.4+ that turns short unit files with [Container], [Pod], [Network], [Volume], [Image], [Build] and [Kube] sections into full podman run service units at daemon-reload time. It replaces podman generate systemd, which is deprecated. Rootless files go in ~/.config/containers/systemd/, rootful files in /etc/containers/systemd/ (or /usr/share/containers/systemd/ for packaged units). A file my-app.container becomes my-app.service; my-net.network becomes my-net-network.service, my-data.volume becomes my-data-volume.service and my-pod.pod becomes my-pod-pod.service. Referencing one Quadlet file from another (Network=my-net.network) adds the Requires= and After= dependencies for you.
# ~/.config/containers/systemd/my-net.network
[Network]
NetworkName=my-net
Subnet=10.89.10.0/24
Gateway=10.89.10.1
DNS=192.0.2.53 # upstream resolver for aardvark-dns; omit to use the host's# ~/.config/containers/systemd/my-app-data.volume
[Volume]
VolumeName=my-app-data# ~/.config/containers/systemd/my-app.container
[Unit]
Description=my-app API
Wants=network-online.target
After=network-online.target
[Container]
Image=registry.example.com/my-app:1.4
ContainerName=my-app
AutoUpdate=registry # podman auto-update pulls a newer image and restarts this unit
Network=my-net.network # depends on my-net-network.service
Volume=my-app-data.volume:/var/lib/my-app
Volume=%h/my-app/config:/etc/my-app:ro,Z # %h is the user's home; systemd specifiers work
PublishPort=127.0.0.1:8080:8080
Environment=TZ=Australia/Melbourne
EnvironmentFile=%h/my-app/my-app.env
Secret=my-app-token,type=env,target=API_TOKEN
User=1000
UserNS=keep-id:uid=1000,gid=1000
ReadOnly=true
Tmpfs=/tmp
DropCapability=ALL
NoNewPrivileges=true
HealthCmd=curl -fsS http://localhost:8080/healthz || exit 1
HealthInterval=30s
HealthOnFailure=kill # systemd then restarts the unit under Restart=
Notify=healthy # unit is "started" only once the first health check passes
LogDriver=journald
[Service]
Restart=always
RestartSec=5
TimeoutStartSec=900 # the first start pulls the image; 90 s default is too short on slow links
[Install]
WantedBy=default.target # multi-user.target for rootful units# ~/.config/containers/systemd/my-pod.pod
[Pod]
PodName=my-pod
PublishPort=8080:80
Network=my-net.network# ~/.config/containers/systemd/my-pod-web.container
[Container]
Image=docker.io/library/nginx:1.29
Pod=my-pod.pod # joins the pod; ports are on the pod
Volume=%h/my-pod/nginx.conf:/etc/nginx/nginx.conf:ro,Z
[Service]
Restart=always
[Install]
WantedBy=default.targetsystemctl --user daemon-reload # runs the generator; errors are in the journal
/usr/lib/systemd/system-generators/podman-system-generator --user --dryrun # print the generated units, exits non-zero on a bad key
systemctl --user start my-app.service
systemctl --user status my-app.service my-net-network.service
journalctl --user -u my-app.service -f
loginctl enable-linger "$USER" # user manager runs at boot and survives logout
systemctl --user enable my-app.service # only needed if the [Install] section is absent; Quadlet enables via WantedByQuadlet units have no enable step: [Install] WantedBy=default.target is enough, and the unit starts at boot once lingering is enabled. Any podman run flag without a dedicated key goes in PodmanArgs=. Rootful units under /etc/containers/systemd/ use systemctl without --user and WantedBy=multi-user.target. podman generate systemd output from older hosts converts almost line for line: --name to ContainerName, -p to PublishPort, -v to Volume.
For services that must start before login on a workstation, or that other system units depend on, run them as rootful Quadlet units under a dedicated User= and UserNS=auto rather than in a user session. systemctl --user targets need XDG_RUNTIME_DIR=/run/user/$(id -u) when invoked from cron or over ssh with a non-login shell; see systemd.
Auto-update#
podman auto-update checks every container labelled io.containers.autoupdate=registry (or AutoUpdate=registry in Quadlet) for a newer image digest, pulls it and restarts the owning systemd unit. With local instead of registry it restarts when the local image tag points to a new ID, which suits images built on the host. A unit that fails to start after the update is rolled back to the previous image.
podman auto-update --dry-run --format '{{.Unit}} {{.Image}} {{.Updated}}' # "pending" marks units with a newer image
podman auto-update # pull, restart, roll back on failure
systemctl --user enable --now podman-auto-update.timer # daily by default; override OnCalendar with a drop-in
systemctl --user list-timers podman-auto-update.timer
journalctl --user -u podman-auto-update.service --since yesterdayAuto-update follows tags, so my-app:1.4 is only ever updated to a rebuilt 1.4. Pin to a floating tag such as my-app:1 if minor releases should roll out automatically, and combine with Notify=healthy so a version that starts but never becomes healthy triggers the rollback.
podman compose#
podman compose is a thin wrapper that executes the first provider found from compose_providers in containers.conf: podman-compose (Python, dnf install podman-compose) or Docker’s docker-compose plugin talking to the Podman socket. The compose.yaml semantics are those of Docker Compose, with the caveats that podman-compose implements the specification independently and lags on newer keys, and that restart: is honoured only while the compose process lives.
systemctl --user enable --now podman.socket # Docker-compatible API for docker-compose and other clients
export DOCKER_HOST="unix://$XDG_RUNTIME_DIR/podman/podman.sock" # point Docker-API clients at it
podman compose up -d
podman compose ps; podman compose logs -f my-app
podman compose down -v # removes containers, networks and named volumesFor anything that has to survive reboots, convert the Compose services to Quadlet units, or run podman kube play on a manifest generated from a working pod. Both give you systemctl semantics that Compose on Podman cannot.
Oneliners#
# Containers not running, with their exit code
podman ps -a --filter status=exited --format '{{.Names}}\t{{.Status}}'
# Health of every container that has a health check
podman ps --format '{{.Names}}' | xargs -I{} sh -c 'printf "%s\t%s\n" {} "$(podman inspect -f "{{.State.Health.Status}}" {})"'
# Memory usage per container, sorted
podman stats --no-stream --format '{{.MemUsage}}\t{{.Name}}' | sort -h
# Image and digest each container was created from
podman ps -a --format '{{.Names}}\t{{.Image}}\t{{.ImageID}}'
# Containers whose image has been replaced by a newer pull (dangling parent)
podman ps -a --format '{{.Names}} {{.ImageID}}' | while read -r n id; do podman image exists "$id" || echo "$n"; done
# Which container publishes a port
podman ps --format '{{.Names}}\t{{.Ports}}' | grep ':8080'
# Run a throwaway shell with the current directory mounted and relabelled
podman run --rm -it -v "$PWD":/work:Z -w /work docker.io/library/alpine:3.22 sh
# Enter a container's network namespace tools without changing the image
podman run --rm -it --network container:my-app docker.io/nicolaka/netshoot ss -ltnp
# The user-namespace UID a host file appears as inside containers
podman unshare stat -c '%u %g %n' ./data/*
# Fix ownership of a bind mount for container UID 999 (postgres) without sudo
podman unshare chown -R 999:999 ./pgdata
# Explain a denial for the last container started
sudo ausearch -m avc -ts recent -c podman -c my-app 2>/dev/null | tail -5
# Regenerate and validate Quadlet units without starting anything
/usr/lib/systemd/system-generators/podman-system-generator --user --dryrun >/dev/null && echo ok
# Units Quadlet generated for this user
systemctl --user list-units 'my-*' --all
# Pending image updates
podman auto-update --dry-run --format '{{.Unit}}\t{{.Updated}}' | grep -w pending
# Reclaim: stopped containers, unused networks, dangling images and build cache
podman system prune -f
# Space by category, then the biggest images
podman system df && podman images --sort size --format '{{.Size}}\t{{.Repository}}:{{.Tag}}' | head
# Export a container's rootfs for offline inspection
podman export my-app | tar -tvf - | sort -k3 -n | tail
# Check whether a registry needs login before a scripted pull
podman search --list-tags --limit 5 registry.example.com/my-app
# Copy an image between registries without a local pull
skopeo copy --all docker://registry.example.com/my-app:1.4 docker://mirror.example.com/my-app:1.4
# Wait for a container to become healthy in a script
until [ "$(podman inspect -f '{{.State.Health.Status}}' my-app)" = healthy ]; do sleep 2; done
# Every environment variable a container was started with (values included: treat as sensitive)
podman inspect -f '{{range .Config.Env}}{{println .}}{{end}}' my-appScripts#
Health report across every container on the host, exit non-zero if any container is unhealthy or has restarted in the last hour. Suits a systemd timer with OnFailure= pointing at a notification unit.
#!/usr/bin/env bash
# usage: podman-health.sh (runs against the invoking user's containers)
set -euo pipefail
rc=0
while IFS=$'\t' read -r name state health started; do
since=$(( $(date +%s) - $(date -d "$started" +%s) ))
flag=ok
if [[ $state != running ]]; then flag="state=$state"; rc=1
elif [[ $health != healthy && $health != '' ]]; then flag="health=$health"; rc=1
elif (( since < 3600 )); then flag="restarted ${since}s ago"; rc=1
fi
printf '%-24s %-8s %-10s %s\n' "$name" "$state" "${health:-none}" "$flag"
done < <(podman ps -a --format '{{.Names}}' | while read -r n; do
podman inspect -f $'{{.Name}}\t{{.State.Status}}\t{{.State.Health.Status}}\t{{.State.StartedAt}}' "$n"
done)
exit "$rc"Storage reclaim that stops at a threshold: prunes in stages until the storage filesystem is below a percentage, so a full disk gets fixed without removing every image on a healthy host. Deletes stopped containers and unused images and volumes.
#!/usr/bin/env bash
# usage: podman-reclaim.sh [max-used-percent] default 80
set -euo pipefail
limit=${1:-80}
root=$(podman info --format '{{.Store.GraphRoot}}')
used() { df --output=pcent "$root" | tail -1 | tr -dc '0-9'; }
step() { printf '%s (%s%% used)\n' "$1" "$(used)"; shift; "$@" >/dev/null; }
printf 'storage %s at %s%% (limit %s%%)\n' "$root" "$(used)" "$limit"
(( $(used) > limit )) || exit 0
step 'pruning stopped containers' podman container prune -f
(( $(used) > limit )) || exit 0
step 'pruning dangling images' podman image prune -f
(( $(used) > limit )) || exit 0
step 'pruning build cache and networks' podman system prune -f
(( $(used) > limit )) || exit 0
step 'pruning unused images' podman image prune -a -f
(( $(used) > limit )) || exit 0
step 'pruning unattached volumes' podman volume prune -f
(( $(used) > limit )) && { printf 'still %s%% used: inspect %s manually\n' "$(used)" "$root" >&2; exit 1; }Convert a running container into a Quadlet unit file by reading its configuration, so the hand-written unit starts from what actually works. Prints the unit; review before installing.
#!/usr/bin/env bash
# usage: container-to-quadlet.sh NAME > ~/.config/containers/systemd/NAME.container
set -euo pipefail
c=${1:?container name}
podman container exists "$c"
json=$(podman inspect "$c")
{
echo '[Container]'
jq -r '.[0] | "Image=\(.ImageName)\nContainerName=\(.Name)"' <<<"$json"
jq -r '.[0].HostConfig.PortBindings // {} | to_entries[] | .key as $k | .value[]
| "PublishPort=\(if .HostIp != "" then .HostIp + ":" else "" end)\(.HostPort):\($k | split("/")[0])"' <<<"$json"
jq -r '.[0].Mounts[] | "Volume=\(.Source):\(.Destination)\(if .RW then "" else ":ro" end)"' <<<"$json"
jq -r '.[0].Config.Env[] | select(startswith("PATH=") or startswith("HOME=") | not) | "Environment=\(.)"' <<<"$json"
jq -r '.[0].NetworkSettings.Networks // {} | keys[] | select(. != "podman") | "Network=\(.)"' <<<"$json"
printf '\n[Service]\nRestart=always\n\n[Install]\nWantedBy=default.target\n'
}Troubleshooting#
| Symptom | Cause | Fix |
|---|---|---|
Permission denied writing to a bind mount, ls -Z shows user_home_t | SELinux: container_t cannot write that label | Add :Z (one container) or :z (shared); confirm with sudo ausearch -m avc -ts recent |
Permission denied on a bind mount with the right label | UID mismatch: container user maps to a subuid the host directory does not allow | --userns=keep-id, :U, or podman unshare chown UID:GID dir |
rootlessport cannot expose privileged port 80 | Unprivileged users cannot bind below 1024 | sudo sysctl -w net.ipv4.ip_unprivileged_port_start=80 and persist in /etc/sysctl.d/; or publish 8080 and proxy with Caddy |
| Container cannot resolve another by name | Both on the default podman network, which has no DNS | podman network create my-net and run both with --network my-net; in a pod use localhost |
Error: cannot setup namespace using "/usr/bin/newuidmap": ... write to uid_map failed | No /etc/subuid entry, or newuidmap lacks its file capability | usermod --add-subuids ... --add-subgids ..., then podman system migrate; getcap /usr/bin/newuidmap |
--memory or --cpus ignored, or OCI runtime error: ... controller not available | cgroups v1, or controllers not delegated to the user slice | podman info --format '{{.Host.CgroupsVersion}}'; add Delegate=memory pids cpu cpuset io to a user@.service drop-in |
no space left on device on pull or build | Storage filled by images, stopped containers or volumes | podman system df -v, podman system prune, then podman image prune -a |
| Containers stop when the SSH session ends | No lingering; user manager and its children exit at logout | loginctl enable-linger "$USER" and run under Quadlet |
systemctl --user says Failed to connect to bus | No user session (cron, sudo -u, su) | export XDG_RUNTIME_DIR=/run/user/$(id -u), or machinectl shell user@ |
Quadlet unit missing after daemon-reload | Generator rejected a key or the file is in the wrong directory | podman-system-generator --user --dryrun; journalctl --user -b _COMM=podman-system-g |
Health status stuck at starting or never runs | Rootless health checks need systemd transient timers; no user session | Run the container from a Quadlet unit; systemctl --user list-timers should show the check |
short-name resolution enforced but cannot prompt | Unqualified image name in a non-interactive run | Use docker.io/library/nginx:1.29 or add an alias in registries.conf.d |
sudo podman ps shows nothing | Rootful and rootless stores are separate | Run as the user who owns the containers; podman ps with the same UID |
Error: OCI runtime error: crun: ... executable file not found | Exec=/command path wrong for the image, or the image is for another architecture | podman run --rm --entrypoint sh image -c 'command -v app'; podman image inspect -f '{{.Architecture}}' |
Slow start and WARN ... "/" is not a shared mount | Running inside another mount namespace (unshare, some CI) | sudo mount --make-rshared /, or run outside the namespace |
Error: ... storage.conf ... database is locked or store corruption after a config change | Storage driver or graphroot changed with existing content | podman system reset removes every image, container and volume for that user; back up volumes first |
For a container that starts and exits, read podman logs my-app, then podman inspect -f '{{.State.ExitCode}} {{.State.Error}}' my-app; 137 is SIGKILL (usually the memory limit, podman inspect -f '{{.State.OOMKilled}}'), 139 a segfault, 143 a clean SIGTERM. The Docker troubleshooting table applies to image, port and PID 1 problems.