# Podman

> Run rootless containers and pods with Podman, mount volumes past SELinux, and manage them as systemd services with Quadlet and auto-update.

Canonical: https://www.wiki.jodisand.me/podman/
Reviewed: 2026-09-24
Related: [Docker](https://www.wiki.jodisand.me/docker/index.md), [Docker Compose](https://www.wiki.jodisand.me/docker-compose/index.md), [systemd](https://www.wiki.jodisand.me/systemd/index.md), [Users, permissions and SELinux](https://www.wiki.jodisand.me/users/index.md), [Caddy](https://www.wiki.jodisand.me/caddy/index.md)


## 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](https://docs.podman.io/en/latest/).

## 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.

```sh
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 restarts
```

`podman 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](#networks-and-dns-between-containers). Publishing a port below 1024 needs `net.ipv4.ip_unprivileged_port_start` lowered (see [Troubleshooting](#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.

```sh
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](https://www.wiki.jodisand.me/docker/#dockerfile) for the build-stage patterns.

```toml
# /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 only
```

## Running containers

```sh
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 release
```

`podman 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](#quadlet-systemd-units) 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 |

```sh
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 UIDs
```

> [!WARNING] Relabelling 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](https://www.wiki.jodisand.me/users/#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.

```sh
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 1024
```

Containers 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.

```sh
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.yaml
```

Published 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.

```ini
# ~/.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
```

```ini
# ~/.config/containers/systemd/my-app-data.volume
[Volume]
VolumeName=my-app-data
```

```ini
# ~/.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
```

```ini
# ~/.config/containers/systemd/my-pod.pod
[Pod]
PodName=my-pod
PublishPort=8080:80
Network=my-net.network
```

```ini
# ~/.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.target
```

```sh
systemctl --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 WantedBy
```

Quadlet 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](https://www.wiki.jodisand.me/systemd/#user-units).

## 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.

```sh
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 yesterday
```

Auto-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](https://www.wiki.jodisand.me/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.

```sh
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 volumes
```

For 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

```sh
# 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-app
```

## Scripts

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.

```sh
#!/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.

```sh
#!/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.

```sh
#!/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](https://www.wiki.jodisand.me/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](https://www.wiki.jodisand.me/docker/#troubleshooting) applies to image, port and PID 1 problems.

## Further reading

- [Podman documentation](https://docs.podman.io/en/latest/)
- [podman-systemd.unit(5): Quadlet keys](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html)
- [podman-auto-update(1)](https://docs.podman.io/en/latest/markdown/podman-auto-update.1.html)
- [Rootless Podman tutorial and shortcomings](https://github.com/containers/podman/blob/main/docs/tutorials/rootless_tutorial.md)
- [Basic networking guide](https://github.com/containers/podman/blob/main/docs/tutorials/basic_networking.md)
- [containers-registries.conf(5)](https://github.com/containers/image/blob/main/docs/containers-registries.conf.5.md)


