--- type: reference title: Docker and Podman description: Current best practices for Docker and Podman - BuildKit, multi-stage builds, rootless operation, and pods. tags: - docker - podman - containers - infrastructure - engineering timestamp: 2026-08-16 verified: 2026-08-28 --- # Docker and Podman This page covers current (2025/2026) best practices for containers. ## Docker Use BuildKit for builds. It enables incremental builds, cache mounts, and secret mounts. Use multi-stage builds for production images. Build in a full stage with the toolchain. Copy only the artifacts to a minimal final stage. Name each stage with `AS`. Harden images with these practices: * Use distroless or alpine base images. * Set `USER` to a non-root user. * Use `ENTRYPOINT` in exec form. * Add a `HEALTHCHECK`. * Pin base image digests. * Use a `.dockerignore` file. Use Docker Compose v2 for multi-container applications. Run `docker compose` without a hyphen. ## Podman Podman runs without a daemon and rootless by default. Run it as a non-root user. An alias `alias docker=podman` works for most commands. Use Podman pods to group containers that share network and IPC namespaces. Pods suit single-host service groups. Use Quadlet for container lifecycle management under systemd. `podman generate systemd` is deprecated as of Podman 5.7; the deprecation notice points to Quadlet. Podman is a good choice for Linux servers, homelabs, and CI runners. ### Quadlet (systemd-managed containers) Quadlet files are systemd unit files with a podman section (`[Container]`, `[Network]`, etc.). Podman ships a systemd generator that turns them into `container` services. Rootless search path: `~/.config/containers/systemd/`. System-wide: `/etc/containers/systemd/`. | Fact | Value | |------|-------| | Unit naming (podman 5.x) | `foo.container` → `foo.service` (no `container-` prefix) | | Container naming | `ContainerName=foo` or default `systemd-foo` | | Boot start | `[Install] WantedBy=default.target` in the file; the generator applies it | | Rootless boot start | enable user-level linger: `loginctl enable-linger ` | | Start behavior | `podman run --name ... --replace --rm`; stop removes the container | | Cross-container deps | `[Unit] Wants=` / `After=` referencing other quadlet unit names | | Network | `Network=foo.network` uses a podman network created from `foo.network` | | Long pulls | add `[Service] TimeoutStartSec=900` | Reference: `man podman-systemd.unit` (podman-systemd.unit(5)). #### Gotchas found in the field (2026-08-16, Gitea migration on dankclaw) - The automatic rootless default network (`podman`) has `dns_enabled: false`. Container names do not resolve on it. Create a quadlet `.network` file (DNS is enabled by default for quadlet networks) and point the containers at it. - `${VAR}` expansion in the generated `ExecStart` needs the variable in the *systemd unit environment*. Put `EnvironmentFile=/path/to/env` in the `[Service]` section of the quadlet file. An `EnvironmentFile=` in `[Container]` only lands in the ignored `[X-Container]` section and does not feed expansion. - A podman restart (or `systemctl --user daemon-reload`) re-runs the generator after quadlet file edits. Verify the parsed unit with `systemctl --user cat `. - Bind-mounted files created by the container are owned by the mapped host UID (container uid 1000 → first subuid + 1000). Edit them with `podman exec --user 1000 ...`. #### Gotchas found in the field (2026-08-19, SearXNG trio migration on dankclaw) - The quadlet generator rejects several compose-familiar keys in `[Container]` and **silently skips the whole unit** (the error is only in `journalctl --user`, and the unit simply never appears in `systemctl list-unit-files`). Use the podman-flag names: `AddCapability=` (not `CapAdd=`), `AddDevice=HOST:CONTAINER` (not `Device=` — that is a `[Volume]` key). When a unit mysteriously does not generate, read the journal for `quadlet-generator: converting "...": unsupported key`. - Quadlet units are **generated** units (they live in `/run/user/UID/systemd/generator/`). `systemctl enable ` fails on them with "transient or generated". You do not enable them: the `[Install] WantedBy=default.target` in the `.container` file *is* the enablement — `systemctl is-enabled` reports `generated`. With user linger on, they start at boot. Just `daemon-reload` then `start`. - A `.network` unit is also generated and cannot be enabled. It is pulled in automatically by any container that sets `Wants=`/`After=` on it (or `Network=.network`). - To make one container reuse another's network namespace (the compose `network_mode: service:X` equivalent), set `Network=.container` on the consumer. The generator adds the dependency on `.service`. This is how SearXNG exits the Gluetun WireGuard tunnel: `Network=gluetun.container`. - Pin a quadlet network CIDR with `Subnet=` in the `.network` file (maps to `podman network create --subnet`). Rootless podman otherwise reassigns the CIDR, which breaks any `trusted_proxies` entry that references it. - A working pattern for a boot-persistent trio: a `.network` (DNS on, `Subnet=` pinned) that the VPN client and the cache join, plus the consumer container on `Network=.container`. Secrets live in a mode-0600 env file referenced by `EnvironmentFile=` in each unit's `[Service]`. ## Docker vs Podman | Factor | Docker | Podman | |--------|--------|--------| | Platform | Mac and Windows | Linux focus | | Model | Daemon-based | Rootless and daemonless | | Default user | Root | Non-root | | Grouping | Compose services | Pods | Use Docker for cross-platform development. Use Podman rootless for Linux servers and security-conscious deployments. ## Citations * [Docker documentation](https://docs.docker.com/) * [Podman documentation](https://podman.io/docs) * [Container Specification](https://containers.dev/) ## See Also * [Kubernetes](kubernetes.md) * [Go](../languages/go.md)