Covers images, containers, Containerfile, build, volumes, networks, pods, podman-compose, systemd integration, and Quadlet files.
9.8 KiB
9.8 KiB
Podman
Daemonless container engine — Docker-compatible, rootless by default.
Core Concepts
| Concept | Description |
|---|---|
| Image | Read-only template for creating containers |
| Container | Running (or stopped) instance of an image |
| Pod | Group of containers sharing network/IPC namespace |
| Volume | Persistent storage decoupled from container life |
| Registry | Remote image store (docker.io, ghcr.io, quay.io) |
| Containerfile | Build instructions (alias: Dockerfile) |
Images
# Search
podman search nginx
podman search --list-tags docker.io/library/python
# Pull
podman pull nginx
podman pull docker.io/library/python:3.12-slim
# List
podman images
podman images --format "{{.Repository}}:{{.Tag}} {{.Size}}"
# Inspect
podman inspect nginx:latest
podman image inspect nginx:latest --format '{{.Config.ExposedPorts}}'
# Remove
podman rmi nginx:latest
podman rmi -f <image-id> # force
podman image prune # remove dangling
podman image prune -a # remove all unused
Containers
# Run
podman run nginx # foreground
podman run -d nginx # detached
podman run --rm nginx cat /etc/os-release # run + remove after exit
podman run -it ubuntu bash # interactive terminal
# Common run flags
podman run -d \
--name myapp \
-p 8080:80 \ # host:container port
-v ./data:/app/data:Z \ # bind mount (:Z for SELinux)
-e APP_ENV=production \ # environment variable
--env-file .env \ # env from file
-w /app \ # working directory
--restart unless-stopped \ # restart policy
--memory 512m \ # memory limit
--cpus 1.5 \ # CPU limit
myimage:latest
# List
podman ps # running
podman ps -a # all (including stopped)
podman ps --format "{{.Names}} {{.Status}} {{.Ports}}"
# Lifecycle
podman start myapp
podman stop myapp
podman stop -t 5 myapp # 5s grace period
podman restart myapp
podman kill myapp # SIGKILL
podman rm myapp
podman rm -f myapp # force (stop + remove)
# Exec into running container
podman exec -it myapp bash
podman exec myapp cat /etc/hostname
# Logs
podman logs myapp
podman logs -f myapp # follow
podman logs --tail 50 myapp # last 50 lines
podman logs --since 1h myapp # last hour
# Copy files
podman cp myapp:/app/log.txt ./log.txt
podman cp ./config.yml myapp:/app/config.yml
# Stats
podman stats # all containers
podman stats myapp # specific
# Inspect
podman inspect myapp
podman inspect myapp --format '{{.NetworkSettings.IPAddress}}'
# Cleanup
podman container prune # remove all stopped
Port Mapping
-p 8080:80 # host 8080 -> container 80
-p 127.0.0.1:8080:80 # bind to localhost only
-p 8080:80/udp # UDP
-p 8080-8090:80-90 # port range
--network host # use host network directly
Restart Policies
| Policy | Behavior |
|---|---|
no |
Never restart (default) |
on-failure[:N] |
Restart on non-zero exit [max N times] |
always |
Always restart, including on boot |
unless-stopped |
Like always, but not if manually stopped |
Building Images
Containerfile
FROM python:3.12-slim
ARG APP_VERSION=1.0.0
ENV APP_ENV=production
WORKDIR /app
# Dependencies first (layer caching)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Application code
COPY src/ ./src/
EXPOSE 8080
RUN useradd -r appuser
USER appuser
ENTRYPOINT ["python", "-m", "src.app"]
CMD ["--port", "8080"]
Build Commands
# Build
podman build -t myapp .
podman build -t myapp:v1.2 -f Containerfile .
# Build args
podman build --build-arg APP_VERSION=2.0 -t myapp .
# No cache
podman build --no-cache -t myapp .
# Multi-stage (use target)
podman build --target builder -t myapp-build .
# Build from stdin
cat Containerfile | podman build -t myapp -
# Tag
podman tag myapp:latest myapp:v1.2
podman tag myapp registry.example.com/myapp:v1.2
# Push to registry
podman login registry.example.com
podman push registry.example.com/myapp:v1.2
# Save / load (offline transfer)
podman save myapp:latest -o myapp.tar
podman load -i myapp.tar
# History (show layers)
podman history myapp:latest
Containerfile Best Practices
| Practice | Reason |
|---|---|
| Pin base image versions | Reproducible builds |
COPY deps before code |
Maximize layer cache |
--no-cache-dir on pip |
Smaller image |
Use -slim base images |
Smaller attack surface |
USER nonroot |
Don't run as root |
One RUN per logical step |
Readable + cacheable |
.containerignore |
Exclude .git, node_modules, etc. |
Volumes
# Create named volume
podman volume create mydata
# List / inspect
podman volume ls
podman volume inspect mydata
# Use in container
podman run -d -v mydata:/app/data myapp
# Bind mount (host directory)
podman run -d -v /host/path:/container/path:Z myapp
podman run -d -v ./local:/app/data:Z myapp # relative path
# Read-only mount
podman run -d -v ./config:/app/config:ro,Z myapp
# Tmpfs (in-memory)
podman run -d --tmpfs /tmp:size=100m myapp
# Remove
podman volume rm mydata
podman volume prune # remove unused
Mount Flags
| Flag | Purpose |
|---|---|
:Z |
Private SELinux label (single container) |
:z |
Shared SELinux label (multiple containers) |
:ro |
Read-only |
:U |
Chown to container user (rootless permission fix) |
Networks
# List
podman network ls
# Create
podman network create mynet
podman network create --subnet 10.89.0.0/24 mynet
# Run on network
podman run -d --network mynet --name web myapp
podman run -d --network mynet --name db postgres
# Containers on same network resolve by name
# web can reach db at hostname "db"
# Connect/disconnect running container
podman network connect mynet myapp
podman network disconnect mynet myapp
# Inspect
podman network inspect mynet
# Remove
podman network rm mynet
podman network prune
Pods
# Create pod (shared network namespace)
podman pod create --name mypod -p 8080:80
# Run containers in pod
podman run -d --pod mypod --name web nginx
podman run -d --pod mypod --name app myapp
# Containers in same pod share localhost
# Pod management
podman pod ls
podman pod start mypod
podman pod stop mypod
podman pod rm mypod
podman pod rm -f mypod # force
# Generate kube YAML from pod
podman generate kube mypod > pod.yml
# Create pod from kube YAML
podman play kube pod.yml
podman play kube --down pod.yml # tear down
Podman Compose
# compose.yml
services:
web:
build: .
ports:
- "8080:80"
volumes:
- ./src:/app/src:Z
environment:
- APP_ENV=development
depends_on:
- db
restart: unless-stopped
db:
image: postgres:16
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
volumes:
pgdata:
podman-compose up -d
podman-compose down
podman-compose logs -f web
podman-compose ps
podman-compose exec web bash
podman-compose build
podman-compose pull
Systemd Integration
# Generate systemd unit from container
podman generate systemd --new --name myapp > ~/.config/systemd/user/myapp.service
# For rootless, enable lingering
loginctl enable-linger $USER
# Enable as user service
systemctl --user daemon-reload
systemctl --user enable --now myapp.service
# Quadlet (modern, preferred over generate systemd)
# Place .container file in ~/.config/containers/systemd/
Quadlet File
# ~/.config/containers/systemd/myapp.container
[Container]
Image=myapp:latest
PublishPort=8080:80
Volume=./data:/app/data:Z
Environment=APP_ENV=production
[Service]
Restart=on-failure
[Install]
WantedBy=default.target
systemctl --user daemon-reload
systemctl --user start myapp.service
Cleanup
# Remove stopped containers
podman container prune
# Remove unused images
podman image prune -a
# Remove unused volumes
podman volume prune
# Remove unused networks
podman network prune
# Nuclear option — everything unused
podman system prune -a --volumes
Gotchas
:Z/:zis required on SELinux systems (Fedora, RHEL) or mounts fail silently- Rootless containers can't bind to ports < 1024 — use higher ports or
sysctl net.ipv4.ip_unprivileged_port_start podman-composeis a separate package — not bundled with podman- Rootless volume mounts may have UID mapping issues — use
:Uflag or--userns=keep-id podman generate systemdis deprecated — use Quadlet.containerfiles instead- Container DNS resolution by name only works on user-created networks, not the default
podmannetwork podman system prunedoes not remove named volumes unless--volumesis passed--restartpolicies only work withpodman start, not across reboots — use systemd for boot persistence
See Also
systemd— service management for container persistence- Podman docs
- Quadlet docs