Containers as systemd services (Podman + Quadlet) standards
Criteria verified as of August 2026. Re-verify on the web before committing to anything (§8).
Hard premise: here a container is not a schedulable workload: it is a
systemd service that happens to be packaged as an OCI image. Everything that follows derives
from that. If you need something to decide which host it runs on, you are not in this document:
you are in kubernetes-standards. And if the answer to that need is a homegrown script that
spreads containers across hosts, you have written an orchestrator worse than the ones that already exist.
1. Scope and triggers
Applies to running containers on one host, under systemd, without an orchestrator: Quadlet as the
canonical unit format, rootless mode, Podman networking on the host, volumes, image policy
and signing, automated updates, real systemd integration (notify, health
checks, cgroups, journald, credentials), Docker compatibility and the two migrations that
matter (Compose → Quadlet, Quadlet → orchestrator).
Triggers: podman, podman-remote, quadlet, /usr/libexec/podman/quadlet -dryrun,
podman quadlet list|install|rm|print, *.container, *.pod, *.volume,
*.network, *.kube, *.build, *.image, *.artifact files, /etc/containers/systemd/,
~/.config/containers/systemd/, /run/containers/systemd/,
/usr/share/containers/systemd/, containers.conf, storage.conf, registries.conf,
policy.json, podman generate systemd, podman auto-update,
io.containers.autoupdate, AutoUpdate=, podman-auto-update.timer, podman secret,
--sdnotify=container, Notify=, HealthCmd=, HealthOnFailure=, --userns=keep-id,
loginctl enable-linger, /etc/subuid, netavark, aardvark-dns, pasta/passt,
default_rootless_network_cmd, podman.socket, DOCKER_HOST, podman compose,
podman-compose, podman system prune, podlet, "the container does not start after a reboot",
"the Quadlet unit does not show up in systemctl".
Internal arbitration rule: if the answer is written in a unit file
(*.container and siblings) or in a host's containers.conf/registries.conf, it belongs to this
skill. If it is written in a manifest with apiVersion/kind that a scheduler consumes, it belongs
to kubernetes-standards.
Not applicable: see kubernetes-standards (agreed boundary: there the image
build —Containerfile/Dockerfile, multi-stage, minimal base, pinning by digest, SBOM and signing
with cosign—, the manifests and their admission, Helm/Kustomize and GitOps; here how that same
image runs as a service on a host without a scheduler. Quadlet's .kube does not turn
this into Kubernetes: it is an input format, not a cluster),
container-runtime-security-standards (the security of the already-running container is theirs,
without exception: seccomp, choice and pinning of the OCI runtime —runc/crun/gVisor/Kata—,
--privileged and capabilities, escape paths, mounting runtime sockets, runtime detection
with Falco/Tetragon, drift and forensics with CRIU; here rootless as the default operating
model of the service —linger, keep-id, what is lost and how it is compensated—, not as an
isolation control), selinux-standards (MAC is theirs: container_t, MCS, exact
semantics of :z/:Z, udica; here only the obligation to label the mount),
linux-administration-standards (generic systemd: Type=, Restart=, dependencies,
journald, cgroups v2, timers, systemd-analyze security — the unit generated by Quadlet is
an ordinary systemd unit and its semantics are decided there; here only the keys of the
[Container]/[Pod]/… section and what Quadlet generates),
rhel-fedora-standards (that Podman is the family default and Docker the exception to
justify; here the detailed criteria for how it is operated), homelab-standards (the boundary is
the rigour demanded, not the tool: there a personal lab where untranslated upstream Compose
is a legitimate answer; here a service with an owner, a tested cold start and governed
updates), backup-recovery-standards (backing up the volumes is theirs: mechanics,
retention, immutability and tested restore; here only what must be backed up and that the volume
is stopped or quiesced before copying it), secrets-management-standards (origin, rotation and
custody of the secret —Vault/OpenBao, SOPS, ESO—; here only how it reaches the container:
podman secret and LoadCredential=), cryptography-pki-standards (PKI and signing keys; here
only that policy.json requires a signature), networking-standards and firewall-policy-standards
(network design and filtering policy as a governed artifact; here only Podman networking on the
host and the trap of published ports versus the firewall), observability-standards
(metrics and alerting design; here what to export), iac-standards (Ansible/Terraform that
deploy the unit files; here what the unit must contain), cicd-standards (the
pipeline that builds and signs the image), ha-clustering-standards (cross-reference, no
overlap: if the question is "how does this container survive the failure of the host", the
honest answer is almost never with Pacemaker —see §7—, but with an orchestrator or by accepting
the downtime; Pacemaker managing containers on a host is complexity that buys nothing).
2. Default decisions / Toolchain
Verify the latest version on the web before pinning it in a real project (§8).
| Decision |
Default (Aug 2026) |
Reason |
| Engine |
Podman 6.0.2 (6.0 line, GA 2026-06-24); previous line maintained at 5.8.5. 6.1.0-rc1 published 2026-07-31 — not in prod |
No daemon, no root, a real systemd unit per container. It is the model that fits a host |
| Absolute minimum version |
≥ 5.8.4 or ≥ 6.0.0 |
CVE-2026-57231 (GHSA-4hq8-gpf5-8p68): a malicious image with malformed Env entries leaks environment variables from the host into the container, with a * wildcard to exfiltrate without knowing the names. Affects 1.8.1 → 5.8.3. Any host below that line running third-party images is compromisable by design |
| Unit format |
Quadlet, always |
It is the supported declarative format. podman generate systemd is not an alternative (see below) |
| OCI runtime |
crun 1.28 |
Default of the RHEL/Fedora family, smaller footprint. The choice and pinning of the runtime as a security control is set by container-runtime-security-standards |
| Mode |
Rootless |
Rootful requires written justification in the repo (§3.3) |
| Network |
netavark 2.0.0 + aardvark-dns 2.0.0 |
Podman 6 requires exactly these versions. CNI removed in Podman 6 |
| Rootless network |
pasta (passt) |
Upstream default since Podman 5.0; on RHEL/Oracle Linux since 9.5. In Podman 6 slirp4netns is removed: it is not an option, not even with default_rootless_network_cmd |
| Firewall backend |
nftables |
iptables removed in Podman 6 |
| cgroups |
v2 mandatory |
Podman 6 removes cgroups v1 support. A host with cgroups v1 does not upgrade |
| Internal database |
SQLite |
BoltDB removed; Podman 6 attempts automatic migration on first start — make a copy of ~/.local/share/containers / /var/lib/containers first |
| Compose |
docker compose v2 against podman.socket if an upstream Compose must be consumed; Quadlet for everything of your own |
podman-compose (v1.6.0, Jun 2026) is a reimplementation with known gaps (external secrets, configs, networking) |
| Updates |
podman auto-update with the registry policy + podman-auto-update.timer |
See §3.5: it is only safe with --sdnotify=container and rollback |
| Conversion tool |
podlet for the first draft of a unit |
Its output is reviewed by hand: it generates neither hardening nor correct dependencies |
Mandatory companion pieces with Podman 6.0.x (the release note says so, it is not
up for debate): Buildah 1.44.0, Skopeo 1.23, netavark and aardvark-dns 2.0.0, and
container-libs configuration files common/v0.68.0. Mixing versions here produces failures that
look like networking or storage failures and are not.
Governance change to keep in mind: Podman is a CNCF project (Sandbox since
2025-01-21) and the repository has moved to the podman-container-tools organisation; the
import path became go.podman.io/podman/v6. Practical consequence: any automation,
CI action pin or download URL pointing at github.com/containers/podman gets reviewed —
the redirect works today, but a supply artifact is not anchored on a redirect.
2.1 When this and when an orchestrator (the honest cut-off point)
| Situation |
Answer |
| One host, few services, no need for scheduling or horizontal scaling, host downtime acceptable |
Quadlet. It is the correct answer, not the poor answer |
| Several hosts and you need to decide where each thing runs |
Orchestrator. Kubernetes (or k3s/Talos if the size calls for it) |
| You need to scale replicas by load, or rolling updates without a window |
Orchestrator |
| You need a service to survive its host going down automatically |
Orchestrator. Not Pacemaker, not scripts |
| Two hosts and "let one take over from the other" with containers |
Almost always: accept the downtime or a load balancer in front of two active instances with state kept outside. See §7 |
| Plenty of services but all on one host and no availability SLO |
Quadlet, with the note that the host is the SPOF and that is accepted in writing |
The antipattern: reimplementing an orchestrator by hand — scripts that copy units between
hosts, homegrown health checks that ssh to start the container on the other node, a
"service registry" in a file. That is not simplicity, it is an orchestrator with no community,
no tests and nobody who knows how to debug it at 3 in the morning. If you need scheduling,
use a scheduler.
The opposite antipattern, just as expensive: standing up Kubernetes for four services on one host.
It multiplies pieces, upgrades and failure modes without giving anything back.
3. Structure and conventions
3.1 Quadlet is the canonical format
Quadlet is a systemd generator: it reads descriptive files and generates .service units
at boot time and on daemon-reload. Operational consequences that must be internalised:
- The generated unit does not exist on disk persistently: it is not edited, not versioned, not
copied. You edit the
.container and reload.
- Reload:
systemctl daemon-reload (rootful) / systemctl --user daemon-reload (rootless).
There is no podman quadlet reload that replaces this.
- Validation before reloading: run the generator dry —
/usr/libexec/podman/quadlet -dryrun -user — and read the unit it produces. A Quadlet
file with a syntax error does not fail loudly: it simply does not generate the unit, and
systemctl start answers "unit not found". It is the format's failure no. 1.
- The service name derives from the file:
web.container → web.service.
podman quadlet list|install|rm|print exists as a management interface. In Podman 6 the model
of podman quadlet install changed: the associated files move to subdirectories instead
of the old .app tracking file. If you automated against that behaviour,
review it before moving up to 6.
Quadlet unit types in force (8, verified in the podman-systemd.unit(5) manual):
.container, .pod, .volume, .network, .kube, .build, .image, .artifact.
| Type |
What for, and criteria |
.container |
The normal case. One per service |
.pod |
Several containers that share a network namespace and lifecycle. Use it only if they really share the network; otherwise two .container files and a .network |
.volume |
A declared named volume. Preferred over creating volumes by hand |
.network |
A declared user network. Mandatory for DNS between containers (§3.4) |
.image |
Pre-pull of an image as a dependency of another unit. Useful for .container units that must not pull from the registry at boot |
.build |
Builds an image on the host from a Containerfile. Vetoed in production: building on the execution host breaks artifact immutability and mixes build toolchain with runtime. The image is built in CI (cicd-standards) |
.kube |
Runs a Kubernetes-style YAML on the host. Only as a migration bridge towards a real cluster, never as a destination. Warning: podman kube play had a symlink traversal (GHSA-wp3j-xq48-xpjw, Apr 2026, low severity) — do not run untrusted YAML |
.artifact |
OCI artifacts. Niche; verify the behaviour before depending on it |
Search paths (verbatim from the manual, in order of precedence; /run wins over /etc,
which wins over /usr):
- Rootful:
/run/containers/systemd/ (temporary/testing) → /etc/containers/systemd/
(defined by the administrator) → /usr/share/containers/systemd/ (defined by the
distribution).
- Rootless:
$XDG_RUNTIME_DIR/containers/systemd/ → $XDG_CONFIG_HOME/containers/systemd/ (or
~/.config/containers/systemd/) → /etc/containers/systemd/users/${UID} →
/etc/containers/systemd/users/ → /usr/share/containers/systemd/users/${UID} →
/usr/share/containers/systemd/users/.
Criteria: your own stuff goes in /etc/containers/systemd/ (rootful) or
~/.config/containers/systemd/ (rootless). /run/... only for testing. /usr/share/... is
packager territory — do not write there.
podman generate systemd is DEPRECATED. Verbatim quote from the manual: "podman generate
systemd is deprecated. We recommend using Quadlet files when running Podman containers or pods
under systemd. There are no plans to remove the command. It will receive urgent bug fixes but no
new features." Correct reading: it is not removed, it is frozen. Therefore:
- ❌ Forbidden in any new unit.
- Existing units generated that way work, but they are debt: they receive no functionality,
and their model (generating a
.service from an already-created container) is imperative —
the real state lives in the container, not in the file, and that is exactly what breaks
rebuildability from scratch.
- Migration:
podlet for the draft, review by hand, and a cold start test (§4).
3.2 Repository layout
containers/
├─ quadlet/
│ ├─ system/ # → /etc/containers/systemd/
│ │ ├─ app.network
│ │ ├─ app-data.volume
│ │ └─ app.container
│ └─ users/<svc>/ # → ~<svc>/.config/containers/systemd/
├─ containers.conf.d/ # drop-in overrides, never the base file
├─ registries.conf.d/
├─ policy.json
└─ README.md # owner, purpose, and the justification for each rootful
- Everything under version control, deployed by Ansible/
iac-standards. A Quadlet file
edited by hand on the host is a snowflake.
- systemd overrides on top of the generated unit:
/etc/systemd/system/<svc>.service.d/*.conf
when something Quadlet does not expose is needed. Document why.
3.3 Rootless by default
- Requirements: ranges in
/etc/subuid and /etc/subgid for the user (one per user, without
overlaps) and loginctl enable-linger <user>. Without linger, user units do not
start at boot and die when the session closes — it is cause no. 1 of "it worked and after
the reboot it is gone".
- One service user per service, without an interactive shell. Do not share a user between
unrelated services: they share storage, networking and the user namespace.
- What is lost and how it is solved:
| Rootless limitation |
Resolution |
| Cannot listen on ports < 1024 |
Publish on a high port and put a reverse proxy in front, or lower net.ipv4.ip_unprivileged_port_start via sysctl.d (documented). Never grant CAP_NET_BIND_SERVICE to the rootless process as a shortcut |
| Limited capabilities |
If the service really needs them, it is a candidate for rootful — with justification |
| UID inside ≠ UID outside on volumes |
--userns=keep-id (or UserNS=keep-id in Quadlet) so the user's UID maps 1:1. Without this, the bind mount's files show up as nobody |
I/O performance on overlay without native fuse-overlayfs |
Verify that the kernel supports overlay in a user namespace (normal today); if not, measure before assuming |
| Some tools expect the root socket |
User socket at $XDG_RUNTIME_DIR/podman/podman.sock |
- Rootful only with written justification in the repo, and even then:
NoNewPrivileges, no
--privileged, minimum capabilities. The detail of that hardening belongs to
container-runtime-security-standards.
3.4 Networking
- netavark is the backend (CNI removed in Podman 6); aardvark-dns provides name
resolution between containers.
- DNS between containers only works on explicitly created user networks. On the
default
podman network there is no name resolution. Therefore: one .network per
stack, and the stack's containers talk to each other by service name. This is not optional,
it is the difference between a maintainable configuration and a file full of IPs.
- Rootless uses pasta. A difference from the late
slirp4netns that must be known: pasta
copies the host's network configuration instead of creating a separate NAT network, so from
the container the host's main IP is not reachable by default and host.containers.internal
may not behave as you expect. If you need to talk to the host, --map-gw or another explicit
route — and it gets documented.
- The firewall trap: publishing a port (
PublishPort=) inserts rules into the
host's ruleset and can open the service to the whole network even if your filtering policy says
otherwise. Rule: PublishPort= always with an explicit listen IP (127.0.0.1:8080:8080
or the correct interface's IP), never the bare port. The host policy belongs to
firewall-policy-standards; the obligation not to bypass it belongs here.
- IPv6: if the host has it, the stack's network declares it explicitly. A half-done dual-stack
network produces intermittent connection failures that diagnose terribly.
- Podman 6: "Network isolation now defaults to enabled" — new behaviour relative to 5.x
that may break stacks that relied on containers on different networks seeing each other.
Verify when migrating.
3.5 Storage
- Named volume (
.volume) by default. Bind mount only when the data has to be
visible and manipulable from the host (config, certificates, an existing data directory).
- Every mount with SELinux labelling:
:z (shared between containers) or :Z
(exclusive). The exact semantics and their risks —:Z on a system directory
relabels recursively and can break the host— belong to selinux-standards. Here only:
a bind mount without :z/:Z on a host with SELinux in enforcing fails, and "disable SELinux"
is not the solution.
- Permissions: with rootless,
UserNS=keep-id so the UIDs line up. With named volumes,
Quadlet .volume accepts, since Podman 6, the keys UID=, GID= and Options=, which avoid
the classic manual chown in an ExecStartPre.
Mount= without a source creates anonymous volumes (supported since Podman 6). Avoid it in
services: an anonymous volume is data with no name, no backup and no owner.
- Backup: the volume is backed up by stopping the container or quiescing the application; a
hot
tar of a database directory is a corrupt copy that looks fine. The
mechanics, retention and tested restore belong to backup-recovery-standards. What this
skill sets: every .volume of a service has an entry in the backup plan, or an explicit
line saying it is disposable.
3.6 Images, registries and updates
registries.conf: unqualified-search-registries is a security hazard, not a
convenience. An unqualified podman run nginx resolves against the list and may pull the
wrong image from the wrong registry. Rule: all image references in the
units are fully qualified (registry.example.com/ns/img:tag), and the unqualified
search list is left empty on servers.
- Mirrors and
blocked/insecure: insecure = true is forbidden in production. An
internal registry without TLS is a registry that anyone on the network can impersonate.
policy.json: the default policy on many distros is insecureAcceptAnything.
It must be hardened to require a signature on your own registries (signedBy/sigstore). The
management of the signing keys belongs to cryptography-pki-standards; signing in the pipeline, to
cicd-standards. Here: the host verifies; a host that accepts anything nullifies the whole
supply chain built upstream.
- Digest versus tag — the real tension:
- Digest (
img@sha256:…) is the contract: reproducible, auditable. It is the right thing for a
critical service and for everything IaC deploys.
- Tag is what
podman auto-update --policy registry needs, since it compares against the
registry: a reference pinned by digest does not update itself (by definition: the
digest is already exact).
- Criteria: critical services → digest + deliberate updates through the pipeline.
Low-risk services with a trusted vendor image → stable tag +
auto-update + rollback. Never
:latest.
podman auto-update (verified in the manual):
- Two policies:
registry (queries the registry) and local (compares against the image
already in local storage). They are declared with the io.containers.autoupdate label or the
Quadlet AutoUpdate= key.
- It is triggered by
podman-auto-update.service, activated daily at midnight by
podman-auto-update.timer. Change that schedule to the agreed window and add
RandomizedDelaySec if there are several hosts: otherwise the whole fleet hits the registry at once.
- Rollback is enabled by default ("Default is true"): if the unit fails to restart with
the new image, it goes back to the previous one. But real failure detection requires the
container to send
READY via SDNOTIFY, created with --sdnotify=container. Without that, "the
process started" counts as success and the rollback never fires: you have automatic updates
without a safety net, which is worse than not having them.
- Governance requirement: auto-update only over images from a registry whose signature
policy.json verifies. Auto-update against a public registry without verification is remote
code execution scheduled by cron.
- Hygiene: periodic
podman system prune (and podman image prune) on its own timer. Careful:
in Podman 6 podman volume prune only prunes unused anonymous volumes (a change to
match Docker); the previous behaviour requires --all. An --all inherited from a script
of the 5.x era deletes named volumes: review before migrating.
3.7 Real integration with systemd
What separates "a container that runs" from "a service":
Notify=true / --sdnotify=container: the unit is declared active when the application
says so, not when the process exists. A requirement for reliable dependencies and for the rollback of
auto-update. If the application does not speak sdnotify, Notify=healthy (which ties READY to the health
check) is the alternative — verify availability in the installed version.
- Health checks:
HealthCmd=, HealthInterval=, HealthRetries=, HealthStartPeriod= and
HealthOnFailure= (kill/restart/stop/none). A health check that only paints
"unhealthy" and does not act is decoration. Criteria: HealthOnFailure=kill + Restart=on-failure
in the unit, so systemd closes the loop with its backoff, which is where it belongs.
- Dependencies:
After=/Requires= on other units; Quadlet's .network and .volume
generate implicit dependencies. For external network dependencies,
Wants=network-online.target. The semantics of systemd dependencies belong to
linux-administration-standards — do not reinvent them here.
- Resources per cgroup:
MemoryMax=, CPUQuota=, IOWeight= in the [Service] section of the
Quadlet unit. Every service carries at least a memory limit: without it, a container
with a leak takes the host down with it. Podman 6 exposes OOMKilled in the died event — useful
for telling "the kernel killed it" from "it crashed on its own".
- Logs:
journald driver (default). The container's logs go to the host's journal and from there
to the aggregator. Retention and limits, in linux-administration-standards. Forbidden: the
json-file driver on a host with systemd: it duplicates storage and dodges journal rotation.
- Secrets: the order of preference is
LoadCredential=/systemd-creds in the unit, mounting the credential on a tmpfs only
for that service;
podman secret (Secret= in Quadlet) mounted as a file;
- — and nothing else.
Environment= with the secret in clear text is forbidden: it ends up in
systemctl show, in podman inspect and in the journal. The origin of the secret and its rotation
belong to secrets-management-standards.
- Unit sandboxing: the generated unit accepts systemd's hardening
directives. Apply the criteria of
linux-administration-standards and measure with
systemd-analyze security <unit>.
3.8 Docker versus Podman
- On a host with systemd, the default answer is Podman. Docker adds a daemon with a
socket that is equivalent to root, a startup model parallel to systemd's, and containers
that are not units. Podman produces services that are operated with
systemctl.
- CLI compatibility: high but not total. A
docker=podman alias is convenient in
interactive use and a source of surprises in scripts. In automation you write podman.
podman.socket exposes a Docker-compatible API; with DOCKER_HOST pointing there,
docker compose v2 works for many flows. Known limits:
docker compose build sends the entire context to the socket — you lose the daemonless model.
Build in CI.
- Partial API coverage: recent Compose features may not exist.
- A TCP socket (
podman system service tcp://…) has no authentication. Forbidden to
expose it; if remote access is needed, SSH.
- The rootful socket (
/run/podman/podman.sock) is equivalent to root on the host: the
permissions on that socket are a first-order security control.
podman compose is a wrapper that delegates to an external provider: docker-compose
takes precedence if installed, otherwise podman-compose. It is pinned explicitly with
compose_providers in containers.conf or PODMAN_COMPOSE_PROVIDER — pin it, do not let it
depend on which package happens to be installed on each host.
podman-compose (v1.6.0, Jun 2026): a Python reimplementation, daemonless, oriented to
rootless. Gaps versus Compose: external and environment-sourced secrets, configs, networking nuances.
Criteria: it is not the basis of a production stack.
3.9 Migrations
From docker-compose to Quadlet — one service per .container, and:
| In Compose |
In Quadlet |
services: (each one) |
One .container file |
networks: |
A .network (mandatory for DNS by name) |
volumes: |
A .volume |
depends_on |
After=/Requires= — and Compose's depends_on does not wait for the service to be ready; Notify= does. The migration is an improvement, not a translation |
restart: always |
Restart=always in [Service] — but on-failure is a better default |
healthcheck |
HealthCmd= + HealthOnFailure= |
env_file with secrets |
LoadCredential=/Secret=. It is not translated as is |
ports: "8080:8080" |
PublishPort=127.0.0.1:8080:8080 — with an explicit IP |
Procedure: podlet generates the draft → manual review of networking, secrets, limits and
SELinux labelling → cold start test (§4). An upstream Compose that
changes every release is not migrated: there you consume the original Compose via the socket, or you accept the debt of
translating it on every version (and note down who does it).
From Quadlet to Kubernetes — when and how:
- The trigger is from §2.1 (scheduling, scaling, surviving a host going down), not
"we have grown".
podman kube generate produces a starting YAML. It is a starting point, not a production
manifest: it lacks securityContext, probes, resources, network policies and everything
kubernetes-standards demands.
- The real work of the migration is not the manifests: it is the state. Local volumes
turn into network storage or a managed service, and that is decided before
writing the first YAML.
4. Quality and gates
In order of increasing cost. The first three break the deployment.
- Unit lint:
/usr/libexec/podman/quadlet -dryrun (with -user if applicable) produces
a unit and emits no warnings. An invalid Quadlet generates nothing and fails silently.
- Qualified references and signature: no image without an explicit registry, none with
:latest, policy.json is not insecureAcceptAnything for your own registries.
- No secrets in clear text:
grep the unit for Environment=/PodmanArgs= with
sensitive material. A gitleaks over the unit repo (secrets-management-standards).
- MAIN GATE — cold start after a reboot. Not
systemctl start: reboot, and the
service ends up active and healthy without intervention. It is the only test that counts, and the one that catches:
- a missing
loginctl enable-linger on user units;
- badly declared network dependencies;
- mounts that do not exist yet when the unit starts;
- images that are no longer in local storage and an unreachable registry at boot;
- volume permissions that only worked because someone did a manual
chown.
- A health check that fails and recovers: the failure is induced inside the container (stopping the
process, saturating the dependency) and it is checked that the unit goes to
unhealthy, that
HealthOnFailure= acts and that systemd brings it back up. A health check never tested in failure
is not tested.
- Automated updates that do not break: in a test environment, an image deliberately
broken is published with the tag auto-update watches and it is checked that the rollback
happens and the service ends up serving with the previous version. If it does not happen, almost always
--sdnotify=container is missing.
- Volume restore: a copy is restored on a clean host and the service starts with
it. The mechanics belong to
backup-recovery-standards; that it has been done at least once per
service is a gate from here.
- Non-privilege test: the service runs rootless unless justified in writing; it is
checked with
podman info / systemctl --user status that this is indeed the case.
5. Stack security
- Minimum version ≥ 5.8.4 / ≥ 6.0.0 because of CVE-2026-57231 (§2). Non-negotiable on hosts that
run third-party images.
- Supply chain: qualified image + digest or tag verified by signature +
policy.json
requiring that signature. Without the last link, the first two are theatre.
unqualified-search-registries empty on servers. It is the typosquatting vector for
images.
- Podman socket: the rootful one is equivalent to root. It is not exposed over TCP, not mounted inside
a container (that is
container-runtime-security-standards territory, and there it is
forbidden for the same reason).
- Rootless as damage reduction, not as isolation: an escape from rootless leaves you as
the service user, not as root — that is a lot, but it is not a complete security
boundary. The isolation model and its real limits are set by
container-runtime-security-standards.
- Podman Desktop: if it is in the estate, ≥ 1.26.2 because of CVE-2026-34045 (unauthenticated
HTTP server: DoS and information disclosure). It has no place on servers, but it tends to be on
the team's laptops.
podman kube play with YAML from an untrusted source: vetoed (GHSA-wp3j-xq48-xpjw).
- Updates to Podman itself: cadence with the rest of the OS (
rhel-fedora-standards /
linux-administration-standards), and CVE triage via vulnerability-management-standards.
- Migration to Podman 6: make a copy of the storage before the first start — the
BoltDB → SQLite migration is automatic and is not designed to go back.
6. Performance and operability
- Metrics:
podman exposes per-container statistics; on a host with systemd, the
natural route is the cadvisor/exporter the fleet already uses plus the unit's cgroup metrics.
The design of what gets alerted belongs to observability-standards. What this skill requires
be watched: unit active, health check status, restarts in a window, OOM kills, age of
the running image versus the registry tag, and failures of podman-auto-update.service
(an auto-update that has been failing silently for weeks is the worst of both worlds).
- Boot:
systemd-analyze blame includes your units. A .container that pulls a large image
from the registry on every boot is a slow, fragile boot — use .image as a dependency or
keep the image locally.
- Clean shutdown:
StopTimeout=/TimeoutStopSec= consistent with how long the application takes
to close. The default cuts brutally and corrupts data in stateful services.
- Capacity: cgroup limits per service and headroom on the host. Image storage
grows uncontrolled without
prune on a timer.
- Failure mode accepted in writing: the host is the SPOF. If the service cannot tolerate it,
the conversation is the one in §2.1, not tuning the unit.
7. Sustainability and prohibitions
Cadence
- Podman follows the distribution's cycle; a new major line does not enter production until
the distro packages it and it has been tested on a test host. Podman 6 is a migration, not
an update: it breaks cgroups v1, CNI, iptables, slirp4netns and the database.
- Half-yearly review of: units still coming from
podman generate systemd, images with a
moving tag, rootful services, and volumes outside the backup plan.
- Every unit has an owner in the repo's README. Without an owner, it gets shut down.
FORBIDDEN
- ❌
podman generate systemd in anything new (deprecated and frozen). Migrate what exists.
- ❌
:latest or any moving tag without a verified signature in a service.
- ❌ Non-empty
unqualified-search-registries on a server; image references without a registry.
- ❌
policy.json at insecureAcceptAnything for your own registries; insecure = true.
- ❌ Secrets in
Environment= or on the container's command line.
- ❌
PublishPort= without an explicit listen IP.
- ❌ A bind mount without
:z/:Z on a host with SELinux; and never "disable SELinux so it
starts".
- ❌ A rootful service without written justification;
--privileged (and its detail, vetoed in
container-runtime-security-standards).
- ❌ A user unit without
loginctl enable-linger.
- ❌ Quadlet
.build in production: the image is built in CI, not on the host that runs it.
- ❌ Auto-update without
--sdnotify=container (blind rollback) or against a registry without a verified
signature.
- ❌ The
json-file log driver on a host with journald.
- ❌ Exposing the Podman socket over TCP, or mounting it inside a container.
- ❌ A
docker=podman alias inside scripts and automation.
- ❌
podman-compose as the basis of a production stack.
- ❌ Editing by hand the
.service unit generated by Quadlet, or versioning it.
- ❌ A container without a cgroup memory limit.
- ❌ Anonymous volumes (
Mount= without a source) in a stateful service.
- ❌ Reimplementing an orchestrator with scripts (cross-host startup over SSH, homegrown "failover",
a service registry in a file). If scheduling is needed, a scheduler is used.
- ❌ Putting Pacemaker in charge of a host's containers to "give them HA": cluster complexity
without any of its guarantees. See
ha-clustering-standards: the answer is an
orchestrator, a load balancer in front of two instances with state kept outside, or accepting the
downtime in writing.
- ❌ Kubernetes for four services on one host "because it will scale some day".
8. Mandatory web verification
Before pinning anything in a real project:
- Podman version on the target distro (not upstream) and its relation to the security
minimum ≥ 5.8.4 / ≥ 6.0.0 (CVE-2026-57231, GHSA-4hq8-gpf5-8p68). Source:
api.github.com/repositories/109145553/releases and the advisory, not a summary.
- Companion versions required by the installed Podman line (Buildah, Skopeo,
netavark, aardvark-dns,
container-libs/common). They change with every major.
- Current list of Quadlet unit types and of search paths in
podman-systemd.unit(5) for the installed version: both have grown and keep growing.
- Status of
podman generate systemd: today deprecated and frozen, not removed.
Check whether any new line drops it.
- Rootless network default in the installed version and in the distro: pasta since Podman 5.0
upstream and RHEL/OL 9.5; slirp4netns removed in Podman 6.
- Modes and behaviour of
podman auto-update (registry/local, rollback, timer) in the
manual of the installed version.
- Open CVEs for
podman, crun, netavark, aardvark-dns, passt/pasta, buildah and
skopeo, and for the base images you run.
- Supply-chain incidents in the container ecosystem before pinning a new
tool or a CI action (recent precedent in the catalogue: the compromise of
trivy-action of March 2026 documented in kubernetes-standards).
- Move to CNCF: repository at
podman-container-tools, import path
go.podman.io/podman/v6. Review URLs pinned in automations.
Declared gaps — DO NOT fill from memory, verify before using:
- Exact behaviour of
podman auto-update with references pinned by digest (it is
assumed here that it does not update, by definition of the digest, but the verbatim sentence
from the manual has not been read). Verify before designing a mixed digest/tag policy.
- Availability and exact semantics of
Notify=healthy in the installed version: it is mentioned
as an alternative to sdnotify but it has not been verified on the web in this revision.
- Podman version packaged in RHEL 10 / CentOS Stream 10, Debian 13 and Ubuntu 26.04 LTS:
not verified. It determines whether the security minimum is reached with the distro package or
a module/backport is needed.
- Real maintenance status of
podman-compose beyond the date of its v1.6.0
(2026-06-03): neither cadence nor team size has been assessed.
Pesto / rootless_port_forwarder=pasta: it appears in the Podman 6.0.1 notes as a
rootless port-forwarding tool with a fixed rule-cleanup bug. What it is, whether it is the
default and its implications have not been verified. Do not depend on it without reading the
manual of your version.
- Support and status of the
.artifact unit type: listed in the manual, but its maturity has
not been verified.
crun-specific CVEs in 2026: the search returned none, but the repository's advisory
feed was not consulted directly. Confirm before pinning the runtime
…(truncated)
1---2name: podman-systemd-containers-standards3description: Containers as systemd services on a single host with Podman and Quadlet, without an orchestrator. Use when writing or debugging Quadlet unit files (.container, .pod, .volume, .network, .kube, .build, .image, .artifact) under /etc/containers/systemd or ~/.config/containers/systemd, running quadlet -dryrun or podman quadlet list/install/rm, migrating off the deprecated podman generate systemd, podman auto-update with io.containers.autoupdate=registry and podman-auto-update.timer, netavark and aardvark-dns or pasta rootless networking and default_rootless_network_cmd, containers.conf, storage.conf, registries.conf unqualified-search-registries and policy.json image trust, podman secret versus systemd LoadCredential=, --userns=keep-id, named volumes versus bind mounts, podman.socket with DOCKER_HOST for docker compose, podman-compose, podman system prune housekeeping, or deciding between a single-host Quadlet stack and a real orchestrator.4---56# Containers as systemd services (Podman + Quadlet) standards78Criteria verified as of **August 2026**. Re-verify on the web before committing to anything (§8).910> **Hard premise**: here a container **is not a schedulable workload**: it is a11> **systemd service** that happens to be packaged as an OCI image. Everything that follows derives12> from that. If you need something to *decide which host it runs on*, you are not in this document:13> you are in `kubernetes-standards`. And if the answer to that need is a homegrown script that14> spreads containers across hosts, you have written an orchestrator worse than the ones that already exist.1516## 1. Scope and triggers1718Applies to **running containers on one host, under systemd, without an orchestrator**: Quadlet as the19canonical unit format, rootless mode, Podman networking on the host, volumes, image policy20and signing, automated updates, real systemd integration (notify, health21checks, cgroups, journald, credentials), Docker compatibility and the two migrations that22matter (Compose → Quadlet, Quadlet → orchestrator).2324Triggers: `podman`, `podman-remote`, `quadlet`, `/usr/libexec/podman/quadlet -dryrun`,25`podman quadlet list|install|rm|print`, `*.container`, `*.pod`, `*.volume`,26`*.network`, `*.kube`, `*.build`, `*.image`, `*.artifact` files, `/etc/containers/systemd/`,27`~/.config/containers/systemd/`, `/run/containers/systemd/`,28`/usr/share/containers/systemd/`, `containers.conf`, `storage.conf`, `registries.conf`,29`policy.json`, `podman generate systemd`, `podman auto-update`,30`io.containers.autoupdate`, `AutoUpdate=`, `podman-auto-update.timer`, `podman secret`,31`--sdnotify=container`, `Notify=`, `HealthCmd=`, `HealthOnFailure=`, `--userns=keep-id`,32`loginctl enable-linger`, `/etc/subuid`, `netavark`, `aardvark-dns`, `pasta`/`passt`,33`default_rootless_network_cmd`, `podman.socket`, `DOCKER_HOST`, `podman compose`,34`podman-compose`, `podman system prune`, `podlet`, "the container does not start after a reboot",35"the Quadlet unit does not show up in systemctl".3637**Internal arbitration rule**: if the answer is written in a **unit file38(`*.container` and siblings) or in a host's `containers.conf`/`registries.conf`**, it belongs to this39skill. If it is written in a **manifest with `apiVersion`/`kind` that a scheduler consumes**, it belongs40to `kubernetes-standards`.4142**Not applicable**: see `kubernetes-standards` (**agreed boundary**: there the **image43build** —`Containerfile`/`Dockerfile`, multi-stage, minimal base, pinning by digest, SBOM and signing44with cosign—, the **manifests** and their admission, Helm/Kustomize and GitOps; **here** how that same45image runs as a service on a host without a scheduler. Quadlet's `.kube` **does not turn46this into Kubernetes**: it is an input format, not a cluster),47`container-runtime-security-standards` (**the security of the already-running container is theirs,48without exception**: seccomp, choice and pinning of the OCI runtime —`runc`/`crun`/gVisor/Kata—,49`--privileged` and capabilities, escape paths, mounting runtime sockets, runtime detection50with Falco/Tetragon, drift and forensics with CRIU; **here** rootless as the **default operating51model of the service** —linger, `keep-id`, what is lost and how it is compensated—, not as an52isolation control), `selinux-standards` (**MAC is theirs**: `container_t`, MCS, exact53semantics of `:z`/`:Z`, `udica`; here only the **obligation** to label the mount),54`linux-administration-standards` (**generic systemd**: `Type=`, `Restart=`, dependencies,55journald, cgroups v2, timers, `systemd-analyze security` — **the unit generated by Quadlet is56an ordinary systemd unit and its semantics are decided there**; here only the keys of the57`[Container]`/`[Pod]`/… section and what Quadlet generates),58`rhel-fedora-standards` (that Podman is the family default and Docker the exception to59justify; here the detailed criteria for how it is operated), `homelab-standards` (**the boundary is60the rigour demanded, not the tool**: there a personal lab where untranslated upstream Compose61is a legitimate answer; here a service with an owner, a tested cold start and governed62updates), `backup-recovery-standards` (**backing up the volumes is theirs**: mechanics,63retention, immutability and tested restore; here only **what** must be backed up and that the volume64is stopped or quiesced before copying it), `secrets-management-standards` (origin, rotation and65custody of the secret —Vault/OpenBao, SOPS, ESO—; here only **how it reaches** the container:66`podman secret` and `LoadCredential=`), `cryptography-pki-standards` (PKI and signing keys; here67only that `policy.json` **requires** a signature), `networking-standards` and `firewall-policy-standards`68(network design and filtering policy as a governed artifact; here only Podman networking on the69host and the **trap** of published ports versus the firewall), `observability-standards`70(metrics and alerting design; here what to export), `iac-standards` (Ansible/Terraform that71**deploy** the unit files; here what the unit must contain), `cicd-standards` (the72pipeline that builds and signs the image), `ha-clustering-standards` (**cross-reference, no73overlap**: if the question is "how does this container survive the failure of the **host**", the74honest answer is **almost never with Pacemaker** —see §7—, but with an orchestrator or by accepting75the downtime; Pacemaker managing containers on a host is complexity that buys nothing).7677## 2. Default decisions / Toolchain7879> Verify the latest version on the web before pinning it in a real project (§8).8081| Decision | Default (Aug 2026) | Reason |82|---|---|---|83| Engine | **Podman 6.0.2** (6.0 line, GA 2026-06-24); previous line maintained at **5.8.5**. `6.1.0-rc1` published 2026-07-31 — **not in prod** | No daemon, no root, a real systemd unit per container. It is the model that fits a host |84| **Absolute minimum version** | **≥ 5.8.4** or **≥ 6.0.0** | **CVE-2026-57231** (GHSA-4hq8-gpf5-8p68): a malicious image with malformed `Env` entries leaks environment variables **from the host** into the container, with a `*` wildcard to exfiltrate without knowing the names. Affects **1.8.1 → 5.8.3**. Any host below that line running third-party images is compromisable by design |85| Unit format | **Quadlet**, always | It is the supported declarative format. `podman generate systemd` is not an alternative (see below) |86| OCI runtime | **`crun` 1.28** | Default of the RHEL/Fedora family, smaller footprint. The choice and pinning of the runtime as a security control is set by `container-runtime-security-standards` |87| Mode | **Rootless** | Rootful requires **written justification** in the repo (§3.3) |88| Network | **netavark 2.0.0** + **aardvark-dns 2.0.0** | Podman 6 **requires** exactly these versions. CNI **removed** in Podman 6 |89| Rootless network | **pasta** (`passt`) | Upstream default since **Podman 5.0**; on RHEL/Oracle Linux since **9.5**. In **Podman 6 `slirp4netns` is removed**: it is not an option, not even with `default_rootless_network_cmd` |90| Firewall backend | **nftables** | `iptables` **removed** in Podman 6 |91| cgroups | **v2 mandatory** | Podman 6 **removes** cgroups v1 support. A host with cgroups v1 does not upgrade |92| Internal database | **SQLite** | BoltDB **removed**; Podman 6 attempts automatic migration on first start — **make a copy of `~/.local/share/containers` / `/var/lib/containers` first** |93| Compose | **`docker compose` v2 against `podman.socket`** if an upstream Compose must be consumed; **Quadlet** for everything of your own | `podman-compose` (v1.6.0, Jun 2026) is a reimplementation with known gaps (external secrets, configs, networking) |94| Updates | `podman auto-update` with the **`registry`** policy + `podman-auto-update.timer` | See §3.5: it is only safe with `--sdnotify=container` and rollback |95| Conversion tool | `podlet` for the **first draft** of a unit | Its output is reviewed by hand: it generates neither hardening nor correct dependencies |9697**Mandatory companion pieces with Podman 6.0.x** (the release note says so, it is not98up for debate): **Buildah 1.44.0**, **Skopeo 1.23**, **netavark and aardvark-dns 2.0.0**, and99`container-libs` configuration files **common/v0.68.0**. Mixing versions here produces failures that100look like networking or storage failures and are not.101102**Governance change to keep in mind**: Podman is a **CNCF** project (**Sandbox since1032025-01-21**) and the repository has moved to the **`podman-container-tools`** organisation; the104import path became `go.podman.io/podman/v6`. Practical consequence: **any automation,105CI action pin or download URL pointing at `github.com/containers/podman` gets reviewed** —106the redirect works today, but a supply artifact is not anchored on a redirect.107108### 2.1 When this and when an orchestrator (the honest cut-off point)109110| Situation | Answer |111|---|---|112| **One host**, few services, no need for scheduling or horizontal scaling, host downtime acceptable | **Quadlet.** It is the correct answer, not the poor answer |113| Several hosts and you need to **decide where each thing runs** | **Orchestrator.** Kubernetes (or k3s/Talos if the size calls for it) |114| You need to **scale replicas** by load, or rolling updates without a window | **Orchestrator** |115| You need a service to **survive its host going down** automatically | **Orchestrator.** Not Pacemaker, not scripts |116| Two hosts and "let one take over from the other" with containers | Almost always: **accept the downtime** or **a load balancer in front of two active instances** with state kept outside. See §7 |117| Plenty of services but all on one host and no availability SLO | Quadlet, with the note that the host **is** the SPOF and that is accepted in writing |118119**The antipattern**: reimplementing an orchestrator by hand — scripts that copy units between120hosts, homegrown health checks that `ssh` to start the container on the other node, a121"service registry" in a file. That is not simplicity, it is an orchestrator with no community,122no tests and nobody who knows how to debug it at 3 in the morning. **If you need scheduling,123use a scheduler.**124125**The opposite antipattern, just as expensive**: standing up Kubernetes for four services on one host.126It multiplies pieces, upgrades and failure modes without giving anything back.127128## 3. Structure and conventions129130### 3.1 Quadlet is the canonical format131132Quadlet is a **systemd generator**: it reads descriptive files and **generates `.service` units133at boot time and on `daemon-reload`**. Operational consequences that must be internalised:134135- The generated unit **does not exist on disk persistently**: it is not edited, not versioned, not136 copied. You edit the `.container` and reload.137- **Reload**: `systemctl daemon-reload` (rootful) / `systemctl --user daemon-reload` (rootless).138 There is no `podman quadlet reload` that replaces this.139- **Validation before reloading**: run the generator dry —140 `/usr/libexec/podman/quadlet -dryrun -user` — and read the unit it produces. **A Quadlet141 file with a syntax error does not fail loudly: it simply does not generate the unit**, and142 `systemctl start` answers "unit not found". It is the format's failure no. 1.143- The service name derives from the file: `web.container` → `web.service`.144- `podman quadlet list|install|rm|print` exists as a management interface. **In Podman 6 the model145 of `podman quadlet install` changed**: the associated files move to **subdirectories** instead146 of the old `.app` tracking file. If you automated against that behaviour,147 review it before moving up to 6.148149**Quadlet unit types in force (8, verified in the `podman-systemd.unit(5)` manual)**:150`.container`, `.pod`, `.volume`, `.network`, `.kube`, `.build`, `.image`, `.artifact`.151152| Type | What for, and criteria |153|---|---|154| `.container` | The normal case. **One per service** |155| `.pod` | Several containers that **share a network namespace** and lifecycle. Use it only if they really share the network; otherwise two `.container` files and a `.network` |156| `.volume` | A declared named volume. **Preferred over creating volumes by hand** |157| `.network` | A declared user network. Mandatory for DNS between containers (§3.4) |158| `.image` | Pre-pull of an image as a dependency of another unit. Useful for `.container` units that must not pull from the registry at boot |159| `.build` | Builds an image on the host from a `Containerfile`. **Vetoed in production**: building on the execution host breaks artifact immutability and mixes build toolchain with runtime. The image is built in CI (`cicd-standards`) |160| `.kube` | Runs a Kubernetes-style YAML on the host. Only as a **migration bridge** towards a real cluster, never as a destination. Warning: `podman kube play` had a symlink traversal (GHSA-wp3j-xq48-xpjw, Apr 2026, low severity) — do not run untrusted YAML |161| `.artifact` | OCI artifacts. Niche; verify the behaviour before depending on it |162163**Search paths** (verbatim from the manual, in order of precedence; `/run` wins over `/etc`,164which wins over `/usr`):165166- Rootful: `/run/containers/systemd/` (temporary/testing) → `/etc/containers/systemd/`167 (defined by the administrator) → `/usr/share/containers/systemd/` (defined by the168 distribution).169- Rootless: `$XDG_RUNTIME_DIR/containers/systemd/` → `$XDG_CONFIG_HOME/containers/systemd/` (or170 `~/.config/containers/systemd/`) → `/etc/containers/systemd/users/${UID}` →171 `/etc/containers/systemd/users/` → `/usr/share/containers/systemd/users/${UID}` →172 `/usr/share/containers/systemd/users/`.173174Criteria: **your own stuff goes in `/etc/containers/systemd/` (rootful) or175`~/.config/containers/systemd/` (rootless)**. `/run/...` only for testing. `/usr/share/...` is176packager territory — do not write there.177178**`podman generate systemd` is DEPRECATED.** Verbatim quote from the manual: *"**podman generate179systemd** is deprecated. We recommend using Quadlet files when running Podman containers or pods180under systemd. There are no plans to remove the command. It will receive urgent bug fixes but no181new features."* Correct reading: **it is not removed, it is frozen**. Therefore:182183- ❌ Forbidden in any new unit.184- Existing units generated that way **work**, but they are debt: they receive no functionality,185 and their model (generating a `.service` from an already-created container) is imperative —186 the real state lives in the container, not in the file, and that is exactly what breaks187 rebuildability from scratch.188- Migration: `podlet` for the draft, review by hand, and a **cold start test** (§4).189190### 3.2 Repository layout191192```193containers/194├─ quadlet/195│ ├─ system/ # → /etc/containers/systemd/196│ │ ├─ app.network197│ │ ├─ app-data.volume198│ │ └─ app.container199│ └─ users/<svc>/ # → ~<svc>/.config/containers/systemd/200├─ containers.conf.d/ # drop-in overrides, never the base file201├─ registries.conf.d/202├─ policy.json203└─ README.md # owner, purpose, and the justification for each rootful204```205206- **Everything under version control**, deployed by Ansible/`iac-standards`. A Quadlet file207 edited by hand on the host is a snowflake.208- systemd overrides on top of the generated unit: `/etc/systemd/system/<svc>.service.d/*.conf`209 when something Quadlet does not expose is needed. Document why.210211### 3.3 Rootless by default212213- **Requirements**: ranges in `/etc/subuid` and `/etc/subgid` for the user (one per user, without214 overlaps) and **`loginctl enable-linger <user>`**. Without linger, user units **do not215 start at boot and die when the session closes** — it is cause no. 1 of "it worked and after216 the reboot it is gone".217- **One service user per service**, without an interactive shell. Do not share a user between218 unrelated services: they share storage, networking and the user namespace.219- **What is lost and how it is solved**:220221| Rootless limitation | Resolution |222|---|---|223| Cannot listen on ports < 1024 | Publish on a high port and **put a reverse proxy in front**, or lower `net.ipv4.ip_unprivileged_port_start` via `sysctl.d` (documented). **Never** grant `CAP_NET_BIND_SERVICE` to the rootless process as a shortcut |224| Limited capabilities | If the service *really* needs them, it is a candidate for rootful — with justification |225| UID inside ≠ UID outside on volumes | `--userns=keep-id` (or `UserNS=keep-id` in Quadlet) so the user's UID maps 1:1. Without this, the bind mount's files show up as `nobody` |226| I/O performance on overlay without native `fuse-overlayfs` | Verify that the kernel supports overlay in a user namespace (normal today); if not, measure before assuming |227| Some tools expect the root socket | User socket at `$XDG_RUNTIME_DIR/podman/podman.sock` |228229- **Rootful only with written justification in the repo**, and even then: `NoNewPrivileges`, no230 `--privileged`, minimum capabilities. The detail of that hardening belongs to231 `container-runtime-security-standards`.232233### 3.4 Networking234235- **netavark** is the backend (CNI removed in Podman 6); **aardvark-dns** provides name236 resolution between containers.237- **DNS between containers only works on explicitly created user networks.** On the238 default `podman` network **there is no name resolution**. Therefore: **one `.network` per239 stack**, and the stack's containers talk to each other by service name. This is not optional,240 it is the difference between a maintainable configuration and a file full of IPs.241- **Rootless uses pasta**. A difference from the late `slirp4netns` that must be known: pasta242 **copies the host's network configuration** instead of creating a separate NAT network, so **from243 the container the host's main IP is not reachable by default** and `host.containers.internal`244 may not behave as you expect. If you need to talk to the host, `--map-gw` or another explicit245 route — and it gets documented.246- **The firewall trap**: publishing a port (`PublishPort=`) **inserts rules into the247 host's ruleset** and can open the service to the whole network even if your filtering policy says248 otherwise. Rule: **`PublishPort=` always with an explicit listen IP** (`127.0.0.1:8080:8080`249 or the correct interface's IP), **never the bare port**. The host policy belongs to250 `firewall-policy-standards`; the obligation not to bypass it belongs here.251- IPv6: if the host has it, the stack's network declares it explicitly. A half-done dual-stack252 network produces intermittent connection failures that diagnose terribly.253- Podman 6: *"Network isolation now defaults to enabled"* — new behaviour relative to 5.x254 that may break stacks that relied on containers on different networks seeing each other.255 **Verify when migrating.**256257### 3.5 Storage258259- **Named volume (`.volume`) by default.** Bind mount only when the data has to be260 visible and manipulable from the host (config, certificates, an existing data directory).261- **Every mount with SELinux labelling**: `:z` (shared between containers) or `:Z`262 (exclusive). The exact semantics and their risks —`:Z` on a system directory263 relabels recursively and can break the host— belong to `selinux-standards`. Here only:264 **a bind mount without `:z`/`:Z` on a host with SELinux in enforcing fails, and "disable SELinux"265 is not the solution.**266- Permissions: with rootless, `UserNS=keep-id` so the UIDs line up. With named volumes,267 Quadlet `.volume` accepts, since Podman 6, the keys **`UID=`, `GID=` and `Options=`**, which avoid268 the classic manual `chown` in an `ExecStartPre`.269- **`Mount=` without a source** creates anonymous volumes (supported since Podman 6). Avoid it in270 services: an anonymous volume is data with no name, no backup and no owner.271- **Backup**: the volume is backed up **by stopping the container or quiescing the application**; a272 hot `tar` of a database directory is a corrupt copy that looks fine. The273 mechanics, retention and tested restore belong to `backup-recovery-standards`. What this274 skill sets: **every `.volume` of a service has an entry in the backup plan, or an explicit275 line saying it is disposable.**276277### 3.6 Images, registries and updates278279- **`registries.conf`**: `unqualified-search-registries` is a **security hazard**, not a280 convenience. An unqualified `podman run nginx` resolves against the list and may pull the281 wrong image from the wrong registry. **Rule: all image references in the282 units are fully qualified** (`registry.example.com/ns/img:tag`), and the unqualified283 search list is left **empty** on servers.284- **Mirrors and `blocked`/`insecure`**: `insecure = true` is **forbidden** in production. An285 internal registry without TLS is a registry that anyone on the network can impersonate.286- **`policy.json`**: the default policy on many distros is `insecureAcceptAnything`.287 **It must be hardened** to require a signature on your own registries (`signedBy`/sigstore). The288 management of the signing keys belongs to `cryptography-pki-standards`; signing in the pipeline, to289 `cicd-standards`. Here: **the host verifies; a host that accepts anything nullifies the whole290 supply chain built upstream.**291- **Digest versus tag — the real tension**:292 - **Digest** (`img@sha256:…`) is the contract: reproducible, auditable. It is the right thing for a293 critical service and for everything IaC deploys.294 - **Tag** is what `podman auto-update --policy registry` needs, since it compares against the295 registry: **a reference pinned by digest does not update itself** (by definition: the296 digest is already exact).297 - **Criteria**: critical services → **digest + deliberate updates through the pipeline**.298 Low-risk services with a trusted vendor image → **stable tag +299 auto-update + rollback**. **Never `:latest`.**300- **`podman auto-update`** (verified in the manual):301 - Two policies: **`registry`** (queries the registry) and **`local`** (compares against the image302 already in local storage). They are declared with the `io.containers.autoupdate` label or the303 Quadlet `AutoUpdate=` key.304 - It is triggered by `podman-auto-update.service`, **activated daily at midnight by305 `podman-auto-update.timer`**. Change that schedule to the agreed window and **add306 `RandomizedDelaySec`** if there are several hosts: otherwise the whole fleet hits the registry at once.307 - **Rollback is enabled by default** ("Default is true"): if the unit fails to restart with308 the new image, it goes back to the previous one. **But real failure detection requires the309 container to send `READY` via SDNOTIFY, created with `--sdnotify=container`.** Without that, "the310 process started" counts as success and the rollback never fires: you have automatic updates311 **without a safety net**, which is worse than not having them.312 - **Governance requirement**: auto-update **only** over images from a registry whose signature313 `policy.json` verifies. Auto-update against a public registry without verification is remote314 code execution scheduled by `cron`.315- **Hygiene**: periodic `podman system prune` (and `podman image prune`) on its own timer. Careful:316 **in Podman 6 `podman volume prune` only prunes unused anonymous volumes** (a change to317 match Docker); the previous behaviour requires `--all`. An `--all` inherited from a script318 of the 5.x era **deletes named volumes**: review before migrating.319320### 3.7 Real integration with systemd321322What separates "a container that runs" from "a service":323324- **`Notify=true` / `--sdnotify=container`**: the unit is declared active when the application325 says so, not when the process exists. A requirement for reliable dependencies and for the rollback of326 auto-update. If the application does not speak sdnotify, `Notify=healthy` (which ties READY to the health327 check) is the alternative — **verify availability in the installed version**.328- **Health checks**: `HealthCmd=`, `HealthInterval=`, `HealthRetries=`, `HealthStartPeriod=` and329 **`HealthOnFailure=`** (`kill`/`restart`/`stop`/`none`). A health check that only paints330 "unhealthy" and does not act is decoration. Criteria: **`HealthOnFailure=kill` + `Restart=on-failure`331 in the unit**, so systemd closes the loop with its backoff, which is where it belongs.332- **Dependencies**: `After=`/`Requires=` on other units; Quadlet's `.network` and `.volume`333 generate implicit dependencies. For external network dependencies,334 `Wants=network-online.target`. **The semantics of systemd dependencies belong to335 `linux-administration-standards`** — do not reinvent them here.336- **Resources per cgroup**: `MemoryMax=`, `CPUQuota=`, `IOWeight=` in the `[Service]` section of the337 Quadlet unit. **Every service carries at least a memory limit**: without it, a container338 with a leak takes the host down with it. Podman 6 exposes `OOMKilled` in the `died` event — useful339 for telling "the kernel killed it" from "it crashed on its own".340- **Logs**: `journald` driver (default). The container's logs go to the host's journal and from there341 to the aggregator. Retention and limits, in `linux-administration-standards`. **Forbidden**: the342 `json-file` driver on a host with systemd: it duplicates storage and dodges journal rotation.343- **Secrets**: the order of preference is344 1. **`LoadCredential=`/`systemd-creds`** in the unit, mounting the credential on a tmpfs only345 for that service;346 2. **`podman secret`** (`Secret=` in Quadlet) mounted as a file;347 3. — and nothing else. **`Environment=` with the secret in clear text is forbidden**: it ends up in348 `systemctl show`, in `podman inspect` and in the journal. The origin of the secret and its rotation349 belong to `secrets-management-standards`.350- **Unit sandboxing**: the generated unit accepts systemd's hardening351 directives. Apply the criteria of `linux-administration-standards` and measure with352 `systemd-analyze security <unit>`.353354### 3.8 Docker versus Podman355356- **On a host with systemd, the default answer is Podman.** Docker adds a daemon with a357 socket that is equivalent to root, a startup model parallel to systemd's, and containers358 that are not units. Podman produces services that are operated with `systemctl`.359- **CLI compatibility**: high but **not total**. A `docker=podman` alias is convenient in360 interactive use and **a source of surprises in scripts**. In automation you write `podman`.361- **`podman.socket`** exposes a Docker-compatible API; with `DOCKER_HOST` pointing there,362 `docker compose` v2 works for many flows. Known limits:363 - `docker compose build` **sends the entire context to the socket** — you lose the daemonless model.364 Build in CI.365 - Partial API coverage: recent Compose features may not exist.366 - **A TCP socket (`podman system service tcp://…`) has no authentication**. Forbidden to367 expose it; if remote access is needed, SSH.368 - The rootful socket (`/run/podman/podman.sock`) is **equivalent to root on the host**: the369 permissions on that socket are a first-order security control.370- **`podman compose`** is a wrapper that delegates to an external provider: **`docker-compose`371 takes precedence** if installed, otherwise `podman-compose`. It is pinned explicitly with372 `compose_providers` in `containers.conf` or `PODMAN_COMPOSE_PROVIDER` — **pin it**, do not let it373 depend on which package happens to be installed on each host.374- **`podman-compose`** (v1.6.0, Jun 2026): a Python reimplementation, daemonless, oriented to375 rootless. Gaps versus Compose: external and environment-sourced secrets, `configs`, networking nuances.376 **Criteria: it is not the basis of a production stack.**377378### 3.9 Migrations379380**From `docker-compose` to Quadlet** — one service per `.container`, and:381382| In Compose | In Quadlet |383|---|---|384| `services:` (each one) | One `.container` file |385| `networks:` | A `.network` (mandatory for DNS by name) |386| `volumes:` | A `.volume` |387| `depends_on` | `After=`/`Requires=` — and **Compose's `depends_on` does not wait for the service to be ready**; `Notify=` does. The migration is an **improvement**, not a translation |388| `restart: always` | `Restart=always` in `[Service]` — but **`on-failure` is a better default** |389| `healthcheck` | `HealthCmd=` + **`HealthOnFailure=`** |390| `env_file` with secrets | `LoadCredential=`/`Secret=`. **It is not translated as is** |391| `ports: "8080:8080"` | `PublishPort=127.0.0.1:8080:8080` — with an explicit IP |392393Procedure: `podlet` generates the draft → manual review of networking, secrets, limits and394SELinux labelling → **cold start test** (§4). **An upstream Compose that395changes every release is not migrated**: there you consume the original Compose via the socket, or you accept the debt of396translating it on every version (and note down who does it).397398**From Quadlet to Kubernetes** — when and how:399- The trigger is from §2.1 (scheduling, scaling, surviving a host going down), **not**400 "we have grown".401- `podman kube generate` produces a starting YAML. **It is a starting point, not a production402 manifest**: it lacks `securityContext`, probes, resources, network policies and everything403 `kubernetes-standards` demands.404- The real work of the migration is not the manifests: it is **the state**. Local volumes405 turn into network storage or a managed service, and that is decided before406 writing the first YAML.407408## 4. Quality and gates409410In order of increasing cost. **The first three break the deployment.**4114121. **Unit lint**: `/usr/libexec/podman/quadlet -dryrun` (with `-user` if applicable) produces413 a unit and **emits no warnings**. An invalid Quadlet generates nothing and fails silently.4142. **Qualified references and signature**: no image without an explicit registry, none with415 `:latest`, `policy.json` is not `insecureAcceptAnything` for your own registries.4163. **No secrets in clear text**: `grep` the unit for `Environment=`/`PodmanArgs=` with417 sensitive material. A `gitleaks` over the unit repo (`secrets-management-standards`).4184. **MAIN GATE — cold start after a reboot.** Not `systemctl start`: **`reboot`**, and the419 service ends up active and healthy without intervention. It is the only test that counts, and the one that catches:420 - a missing `loginctl enable-linger` on user units;421 - badly declared network dependencies;422 - mounts that do not exist yet when the unit starts;423 - images that are no longer in local storage and an unreachable registry at boot;424 - volume permissions that only worked because someone did a manual `chown`.4255. **A health check that fails and recovers**: the failure is induced inside the container (stopping the426 process, saturating the dependency) and it is checked that the unit goes to `unhealthy`, that427 `HealthOnFailure=` acts and that systemd brings it back up. A health check never tested in failure428 **is not tested**.4296. **Automated updates that do not break**: in a test environment, an image deliberately430 broken is published with the tag auto-update watches and it is checked that **the rollback431 happens** and the service ends up serving with the previous version. If it does not happen, almost always432 `--sdnotify=container` is missing.4337. **Volume restore**: a copy is restored on a clean host and the service starts with434 it. The mechanics belong to `backup-recovery-standards`; **that it has been done at least once per435 service is a gate from here**.4368. **Non-privilege test**: the service runs rootless unless justified in writing; it is437 checked with `podman info` / `systemctl --user status` that this is indeed the case.438439## 5. Stack security440441- **Minimum version ≥ 5.8.4 / ≥ 6.0.0** because of CVE-2026-57231 (§2). Non-negotiable on hosts that442 run third-party images.443- **Supply chain**: qualified image + digest or tag verified by signature + `policy.json`444 requiring that signature. Without the last link, the first two are theatre.445- **`unqualified-search-registries` empty** on servers. It is the *typosquatting* vector for446 images.447- **Podman socket**: the rootful one is equivalent to root. It is not exposed over TCP, not mounted inside448 a container (that is `container-runtime-security-standards` territory, and there it is449 forbidden for the same reason).450- **Rootless as damage reduction, not as isolation**: an escape from rootless leaves you as451 the service user, not as root — that is a lot, but **it is not a complete security452 boundary**. The isolation model and its real limits are set by453 `container-runtime-security-standards`.454- **Podman Desktop**: if it is in the estate, **≥ 1.26.2** because of CVE-2026-34045 (unauthenticated455 HTTP server: DoS and information disclosure). It has no place on servers, but it tends to be on456 the team's laptops.457- **`podman kube play`** with YAML from an untrusted source: vetoed (GHSA-wp3j-xq48-xpjw).458- **Updates to Podman itself**: cadence with the rest of the OS (`rhel-fedora-standards` /459 `linux-administration-standards`), and CVE triage via `vulnerability-management-standards`.460- **Migration to Podman 6**: make a copy of the storage before the first start — the461 BoltDB → SQLite migration is automatic and **is not designed to go back**.462463## 6. Performance and operability464465- **Metrics**: `podman` exposes per-container statistics; on a host with systemd, the466 natural route is the `cadvisor`/exporter the fleet already uses plus the unit's cgroup metrics.467 The design of what gets alerted belongs to `observability-standards`. **What this skill requires468 be watched**: unit `active`, health check status, restarts in a window, OOM kills, age of469 the running image versus the registry tag, and **failures of `podman-auto-update.service`**470 (an auto-update that has been failing silently for weeks is the worst of both worlds).471- **Boot**: `systemd-analyze blame` includes your units. A `.container` that pulls a large image472 from the registry on every boot is a slow, fragile boot — use `.image` as a dependency or473 keep the image locally.474- **Clean shutdown**: `StopTimeout=`/`TimeoutStopSec=` consistent with how long the application takes475 to close. The default cuts brutally and corrupts data in stateful services.476- **Capacity**: cgroup limits per service **and** headroom on the host. Image storage477 grows uncontrolled without `prune` on a timer.478- **Failure mode accepted in writing**: the host is the SPOF. If the service cannot tolerate it,479 the conversation is the one in §2.1, not tuning the unit.480481## 7. Sustainability and prohibitions482483**Cadence**484- Podman follows the distribution's cycle; a new major line **does not enter production until485 the distro packages it** and it has been tested on a test host. Podman 6 is a migration, not486 an update: it breaks cgroups v1, CNI, iptables, slirp4netns and the database.487- Half-yearly review of: units still coming from `podman generate systemd`, images with a488 moving tag, rootful services, and volumes outside the backup plan.489- Every unit has an **owner** in the repo's README. Without an owner, it gets shut down.490491**FORBIDDEN**492- ❌ `podman generate systemd` in anything new (deprecated and frozen). Migrate what exists.493- ❌ `:latest` or any moving tag without a verified signature in a service.494- ❌ Non-empty `unqualified-search-registries` on a server; image references without a registry.495- ❌ `policy.json` at `insecureAcceptAnything` for your own registries; `insecure = true`.496- ❌ Secrets in `Environment=` or on the container's command line.497- ❌ `PublishPort=` without an explicit listen IP.498- ❌ A bind mount without `:z`/`:Z` on a host with SELinux; and **never** "disable SELinux so it499 starts".500- ❌ A rootful service without written justification; `--privileged` (and its detail, vetoed in501 `container-runtime-security-standards`).502- ❌ A user unit without `loginctl enable-linger`.503- ❌ Quadlet `.build` in production: the image is built in CI, not on the host that runs it.504- ❌ Auto-update without `--sdnotify=container` (blind rollback) or against a registry without a verified505 signature.506- ❌ The `json-file` log driver on a host with journald.507- ❌ Exposing the Podman socket over TCP, or mounting it inside a container.508- ❌ A `docker=podman` alias inside scripts and automation.509- ❌ `podman-compose` as the basis of a production stack.510- ❌ Editing by hand the `.service` unit generated by Quadlet, or versioning it.511- ❌ A container without a cgroup memory limit.512- ❌ Anonymous volumes (`Mount=` without a source) in a stateful service.513- ❌ **Reimplementing an orchestrator with scripts** (cross-host startup over SSH, homegrown "failover",514 a service registry in a file). If scheduling is needed, a scheduler is used.515- ❌ **Putting Pacemaker in charge of a host's containers to "give them HA"**: cluster complexity516 without any of its guarantees. See `ha-clustering-standards`: the answer is an517 orchestrator, a load balancer in front of two instances with state kept outside, or accepting the518 downtime in writing.519- ❌ Kubernetes for four services on one host "because it will scale some day".520521## 8. Mandatory web verification522523Before pinning anything in a real project:5245251. **Podman version** on the target distro (not upstream) and its relation to the **security526 minimum ≥ 5.8.4 / ≥ 6.0.0** (CVE-2026-57231, GHSA-4hq8-gpf5-8p68). Source:527 `api.github.com/repositories/109145553/releases` and the advisory, not a summary.5282. **Companion versions required** by the installed Podman line (Buildah, Skopeo,529 netavark, aardvark-dns, `container-libs/common`). They change with every major.5303. **Current list of Quadlet unit types** and of **search paths** in531 `podman-systemd.unit(5)` for **the installed version**: both have grown and keep growing.5324. **Status of `podman generate systemd`**: today deprecated and frozen, **not removed**.533 Check whether any new line drops it.5345. **Rootless network default** in the installed version and in the distro: pasta since Podman 5.0535 upstream and RHEL/OL 9.5; **slirp4netns removed in Podman 6**.5366. **Modes and behaviour of `podman auto-update`** (`registry`/`local`, rollback, timer) in the537 manual of the installed version.5387. **Open CVEs** for `podman`, `crun`, `netavark`, `aardvark-dns`, `passt/pasta`, `buildah` and539 `skopeo`, and for the base images you run.5408. **Supply-chain incidents** in the container ecosystem before pinning a new541 tool or a CI action (recent precedent in the catalogue: the compromise of542 `trivy-action` of March 2026 documented in `kubernetes-standards`).5439. **Move to CNCF**: repository at `podman-container-tools`, import path544 `go.podman.io/podman/v6`. Review URLs pinned in automations.545546**Declared gaps — DO NOT fill from memory, verify before using:**547- **Exact behaviour of `podman auto-update` with references pinned by digest** (it is548 assumed here that it does not update, by definition of the digest, but **the verbatim sentence549 from the manual has not been read**). Verify before designing a mixed digest/tag policy.550- **Availability and exact semantics of `Notify=healthy`** in the installed version: it is mentioned551 as an alternative to sdnotify but **it has not been verified on the web** in this revision.552- **Podman version packaged in RHEL 10 / CentOS Stream 10, Debian 13 and Ubuntu 26.04 LTS**:553 not verified. It determines whether the security minimum is reached with the distro package or554 a module/backport is needed.555- **Real maintenance status of `podman-compose`** beyond the date of its v1.6.0556 (2026-06-03): neither cadence nor team size has been assessed.557- **`Pesto` / `rootless_port_forwarder=pasta`**: it appears in the Podman 6.0.1 notes as a558 rootless port-forwarding tool with a fixed rule-cleanup bug. **What it is, whether it is the559 default and its implications have not been verified**. Do not depend on it without reading the560 manual of your version.561- **Support and status of the `.artifact` unit type**: listed in the manual, but **its maturity has562 not been verified**.563- **`crun`-specific CVEs in 2026**: the search returned none, but **the repository's advisory564 feed was not consulted directly**. Confirm before pinning the runtime565566…(truncated)