# Podman Systemd Containers Standards

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

- Skill: `serialexperimentslainnnn/podman-systemd-containers-standards` (Agent Skill)
- Install (CLI): `npx skillmds@latest add serialexperimentslainnnn/podman-systemd-containers-standards`
- Raw SKILL.md: https://api.skillmd.com/api/skills/serialexperimentslainnnn/podman-systemd-containers-standards/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: serialexperimentslainnnn (https://skillmd.com/u/serialexperimentslainnnn)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/serialexperimentslainnnn/podman-systemd-containers-standards

---


# 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
  1. **`LoadCredential=`/`systemd-creds`** in the unit, mounting the credential on a tmpfs only
     for that service;
  2. **`podman secret`** (`Secret=` in Quadlet) mounted as a file;
  3. — 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.**

1. **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.
2. **Qualified references and signature**: no image without an explicit registry, none with
   `:latest`, `policy.json` is not `insecureAcceptAnything` for your own registries.
3. **No secrets in clear text**: `grep` the unit for `Environment=`/`PodmanArgs=` with
   sensitive material. A `gitleaks` over the unit repo (`secrets-management-standards`).
4. **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`.
5. **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**.
6. **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.
7. **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**.
8. **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:

1. **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.
2. **Companion versions required** by the installed Podman line (Buildah, Skopeo,
   netavark, aardvark-dns, `container-libs/common`). They change with every major.
3. **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.
4. **Status of `podman generate systemd`**: today deprecated and frozen, **not removed**.
   Check whether any new line drops it.
5. **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**.
6. **Modes and behaviour of `podman auto-update`** (`registry`/`local`, rollback, timer) in the
   manual of the installed version.
7. **Open CVEs** for `podman`, `crun`, `netavark`, `aardvark-dns`, `passt/pasta`, `buildah` and
   `skopeo`, and for the base images you run.
8. **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`).
9. **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)
