– 0%

Podman. Альтернатива Docker

Введение

В экосистеме контейнеров долгие годы де-факто стандартом считался Docker, и большинство инструментов, туториалов и CI-конфигураций построены вокруг его архитектуры с центральным демоном. Podman (POD Manager) предлагает ту же функциональность при принципиально другой модели: запускает контейнеры без демона, напрямую, как обычные процессы init-системы. Изначально разработанный Red Hat, проект передан в CNCF (Podman Container Tools, принят на уровень Sandbox 21 января 2025), код живёт в организации podman-container-tools, лицензия — Apache-2.0 без ограничений по размеру организации.

Разница между «есть демон» и «нет демона» — это не маркетинговая деталь, а вопрос безопасности, интеграции с systemd и поведения в multi-user окружениях. Именно из неё вырастают rootless-контейнеры «из коробки», Quadlet, podman kube play и Docker-совместимый API по требованию, а не по умолчанию.

Ключевые отличия Podman от классического стека:

  • Нет привилегированного демона-владельца контейнеров
  • Rootless — режим по умолчанию, а не «дополнительная установка»
  • Контейнер — штатный юнит systemd (systemctl, journalctl, лимиты cgroup)
  • Поды (pods) и запуск Kubernetes YAML без kubelet
  • CLI на ~99 % совместим с docker, поэтому миграция обычно начинается с alias docker=podman

Актуальная версия на данный момент — 6.1.x (6.1.4 от 23.10.2026).

Обзор и стек технологий

Podman: daemonless-движок

Podman — это CLI-обёртка над библиотекой libpod (Podman Library), которая управляет жизненным циклом контейнеров и подов напрямую через ядро: namespaces, cgroups, OCI-рантайм.

Ключевые характеристики:

  • Каждый контейнер — обычная ветвь дерева процессов init-системы, а не ребёнок демона
  • CLI создаёт контейнер и завершается: контейнер переживает выход из терминала и перезапуск любого процесса Podman
  • Состояние хранится локально в SQLite, доступ к состоянию — у того пользователя, которому принадлежат файлы
  • Поддержка OCI-образа, Docker-формата и OCI-артефактов (произвольные данные в реестре)
  • Лицензия: Apache-2.0 (CNCF)

Стек:

  • Язык: Go
  • Рантайм: crun по умолчанию, опционально runc, Kata, gVisor
  • Сборка: Buildah (встроен в CLI как podman build)
  • Перенос и подписи образов: Skopeo + библиотека containers/image
  • Хранилище: containers/storage (overlay / fuse-overlayfs / vfs)
  • Сеть: Netavark + Aardvark-dns, в rootless — Pasta (заменил slirp4netns) и rootlessport
  • Init-интеграция: Quadlet (podman-system-generator)
  • Требования хоста: cgroups v2 и nftables (в 6.x версиях cgroups v1 и iptables не поддерживаются вообще)

Вес и ресурсы: Podman не содержит постоянно живущего демона, поэтому на простое не потребляет память «за архитектуру». Из минусов модели — каждая команда поднимает собственный процесс и читает базу состояния, а warm-пула демона нет: на очень частых повторах коротких команд это заметно.

Экосистема сопутствующих инструментов

Podman редко используют «в одиночку», но почти каждый компонент заменяем и полезен сам по себе:

Компонент Роль аналог в стеке Docker
podman CLI + Docker-совместимый REST API docker + dockerd
libpod библиотека управления жизненным циклом containerd + dockerd
conmon супервизор контейнера: TTY, логи, exit code containerd-shim
buildah сборка образов из Containerfile BuildKit
skopeo inspect/copy/sign без распаковки в хост docker pull + доп. утилиты
crun OCI-рантайм runc
netavark настройка сетей CNI-плагины + встроенный bridge
aardvark-dns DNS-резолвинг имён контейнеров встроенный resolver
quadlet systemd-юниты из декларативных файлов docker-unit-файлы вручную
podman machine гостевая Linux-ВМ (macOS/Windows/Linux) Docker Desktop VM

Модель процессов

Docker: docker CLI → dockerd (root) → containerd → runc → контейнер.

Podman: podman run → conmon → PID 1 контейнера, после чего CLI завершается:

systemd (PID 1)
└── user@1000.service                  ← rootless: пользовательский systemd
    └── conmon                         ← владеет TTY и логом, ждёт main process
        └── <PID 1 контейнера>         ← например: nginx: master process
            └── nginx worker

Что это даёт на практике:

  1. Меньше поверхность атаки — нет долгоживущего root-демона с API-сокетом, компрометация которого равна компрометации хоста
  2. Контейнер как сервис — systemctl status, journalctl -u, Restart=, MemoryMax= работают без «костылей»
  3. Изоляция пользователей — UID/GID-диапазоны, хранилище и сети каждого пользователя не пересекаются
  4. Каскадная остановка — systemctl stop даёт SIGTERM, затем SIGKILL по таймауту
  5. Нет единой сериализующей точки — параллельные запуски от разных пользователей не конкурируют за один API-воркер

conmon при этом остаётся важным процессом: он держит открытыми pty/pipes контейнера, пишет вывод в выбранный log-драйвер (k8s-file по умолчанию для run, journald — рекомендуемый в systemd-окружении), фиксирует код завершения и генерирует событие died, а также обрабатывает SIGWINCH. Начиная с 6.2 сигналы корректно проксируются и для podman exec: Ctrl-C убивает и процесс внутри контейнера.

Возможности и API

Ключевые группы команд

Podman построен вокруг нескольких плоских пространств команд, и почти всё, что вы умеете в Docker, здесь имеет прямой аналог:

  1. Жизненный цикл контейнеров — работа с отдельным контейнером:

podman run / create / start / stop / restart / kill / rm

podman exec (с 6.0 есть --no-session — без API-сессии и записи в БД, быстрее в CI)

podman commit, podman export/import, podman container clone (5.x+)

  1. Образы и реестры — доставка артефактов:
  2. podman pull / push / build / save / load / tag / search
  3. podman image tree, podman history, podman manifest ...
  4. podman login / logout, podman image trust, podman image sign

  5. Группировка и декларативность:

  6. podman pod create|ps|stats|stop|rm
  7. podman kube play / podman kube generate (Kubernetes YAML)
  8. podman quadlet install / list
  9. podman generate kube (легаси-алиасы, предпочтительна новая раскладка kube ...)

  10. Ресурсы платформы:

  11. podman network, podman volume, podman secret, podman config
  12. podman system df|prune|migrate|service|events, podman auto-update

  13. Удалённое управление:

  14. podman system connection add|default|list|remove
  15. глобальный --url (ssh://, unix://, qemu://)

Базовый рабочий цикл

Пример запуска сервиса:

# detached, с портом, volume и restart-политикой
podman run -d --name web -p 8080:80 \
  --restart=unless-stopped \
  -v web-data:/usr/share/nginx/html:ro \
  docker.io/library/nginx:1.27-alpine

# отладка
podman ps -a --size --format '{{.Names}}\t{{.Status}}\t{{.Image}}'
podman logs -f --timestamps web
podman exec -it web sh
podman stats --no-stream web
podman diff web                        # что изменилось в rootfs
podman inspect web --format '{{.State.Status}} {{.State.ExitCode}}'

# остановка
podman stop -t 10 web && podman rm web

Параллельные запуски без демона — штатная ситуация:

seq 1 50 | xargs -P16 -I{} podman run --rm docker.io/library/alpine echo "job {}"

Сети

podman network create backend --subnet 10.89.0.0/24 --gateway 10.89.0.1 --dns=true
podman network inspect backend --format json

podman run -d --name db  --network backend docker.io/library/postgres:16-alpine
podman run -d --name api --network backend:alias=api --network-alias gateway ghcr.io/acme/api

podman exec api getent hosts db        # DNS обслуживает Aardvark-dns
podman network reload --all            # пересобрать правила после апгрейда стека

# блокировка доступа к подсети на уровне маршрутизации (netavark 2.0, 6.0+)
podman network create blocked --route 10.20.30.0/24,blackhole
Режим сети rootful rootless Комментарий
bridge (по умолчанию) Netavark + nftables Netavark + Pasta/rootlessport отдельный netns, NAT из user namespace
macvlan / ipvlan ✅ ❌ нужен CAP_NET_ADMIN
none ✅ ✅ максимальная изоляция
slirp4netns ❌ удалён в 6.0 замена — Pasta
pvpassthrough свежий Netavark ❌ проброс без NAT-петли, сохраняет source IP
host ✅ ⚠️ частично в 6.0 host.containers.internal = 127.0.0.1

Хранилище: volumes против bind mounts

podman volume create --label app=wiki pg-data
podman volume inspect pg-data
podman run -d -v pg-data:/var/lib/postgresql/data docker.io/library/postgres:16-alpine

# named volume с опциями монтирования: NFS без root внутри контейнера
podman volume create --opt type=nfs --opt device=10.0.0.5:/srv/nfs/wiki \
  --opt o=nolock,addr=10.0.0.5 wiki-nfs

# nocreate: упасть с ошибкой, если volume не существует, вместо тихого создания
podman run --mount type=volume,src=pg-data,dst=/var/lib/postgresql/data,nocreate \
  docker.io/library/postgres:16-alpine

# примонтировать volume на хосте (rootless — через podman unshare)
podman unshare podman volume mount pg-data

Bind-монты — главный источник ошибок у «пришедших из Docker»:
- -v /home/me/data:/data → снаружи файлы принадлежат вашему UID, а процесс внутри контейнера работает под другим UID: получаем EACCES
- решения: --userns=keep-id, суффикс :U (chown под UID контейнера), :z/:Z (SELinux relabel: shared/private), явный --user

podman run --rm -v ./out:/out:z --userns=keep-id docker.io/library/alpine touch /out/x

Image volumes (инструкция VOLUME в Dockerfile) с 5.0 обрабатываются как bind, а не tmpfs — это частый источник «откуда там старые данные». Управление: --image-volume=bind|tmpfs|ignore.

Поды

Pod — группа контейнеров, разделяющая network namespace (и, в зависимости от настроек, IPC/UTS) — та же единица, что в Kubernetes. Поэтому порты объявляются на поде:

podman pod create --name webstack -p 80:80 -p 443:443
podman run -d --pod webstack --name nginx docker.io/library/nginx:1.27-alpine
podman run -d --pod webstack --name app   ghcr.io/acme/app

podman pod ps
podman pod stats webstack
podman pod inspect webstack                    # конфигурация пода целиком
podman pod stop webstack

Внутри пода контейнеры обращаются друг к другу по localhost. Инфраструктурный контейнер (infra, аналог pause) держит namespace; --infra=false убирает его, и тогда namespace держит первый пользовательский контейнер.

Kubernetes YAML без kubelet

podman kube play ./deployment.yaml              # из манифеста → локальные поды
podman kube play --start=false --build ./app.yaml
podman kube generate webstack > webstack.yaml   # обратно в YAML

Поддерживается подмножество API: Deployment, Pod, Service, ConfigMap, Secret, PVC. Не поддерживаются: планировщик, autoscaling, RBAC, контроллеры кроме Deployment/Pod, большинство полей securityContext. С 6.2 seccomp читается из securityContext.seccompProfile (annotation-механизм объявлен deprecated и исчезнет в 7.0), добавлены --multiple-pods (несколько подов на реплику), --validate (строгость к неизвестным полям YAML) и --label.

Сборка образов и multi-arch

# платформо-осознанная сборка и push
podman build --platform linux/amd64,linux/arm64 -t myapp:1.4.2 --push .

# ручная сборка manifest list
podman build -t myapp-amd64 --platform linux/amd64 .
podman build -t myapp-arm64 --platform linux/arm64 .
podman manifest create myapp:1.4.2
podman manifest add myapp:1.4.2 containers-storage:myapp-amd64 --os linux --arch amd64
podman manifest add myapp:1.4.2 containers-storage:myapp-arm64 --os linux --arch arm64
podman manifest push myapp:1.4.2 docker.io/acme/myapp:1.4.2

# «ферма»: распределение сборки по машинам-агентам
podman farm create multiarch aarch64-runner x86-runner
podman farm build -t myapp:1.4.2 --push .

Отличия от BuildKit: движок — Buildah, синтаксис Dockerfile совместим, но кэш-механика своя и RUN --mount=type=cache ведёт себя иначе. Сборка идёт в собственном user namespace, поэтому Dockerfile-трюки с монтированием ФС требуют rootful или дополнительных capabilities.

OCI-артефакты и подписи

# произвольные данные в OCI-реестре: модели, отчёты, конфиги
podman artifact add --file-type application/gzip quay.io/acme/models:v1 ./model.bin.gz
podman artifact ls
podman artifact inspect quay.io/acme/models:v1
podman artifact pull quay.io/acme/models:v1 ./out

# подписи и политики доверия
podman image sign --sign-by platform@acme.example.com myapp:1.4.2
podman image trust show
podman image trust set --type accept internal-registry.example.com:5000

Политика в /etc/containers/policy.json (sigstore-signed, simple signing, insecureAcceptAnything) — то, что реально останавливает supply-chain-атаку на реестр: неподписанный или недоверенный образ просто не запустится. Не путать с проверкой digest'ов.

Удалённое управление

podman system connection add prod ssh://deploy@prod.example.com/run/podman/podman.sock
podman system connection default prod
podman system connection list
podman --url ssh://deploy@prod/run/user/1000/podman/podman.sock ps

Туннель — SSH: не нужен exposed TCP-сокет, что заметно безопаснее классического «Docker daemon на 2375 с TLS-сертификатами».

Rootless: как это устроено

Что происходит при первом запуске

  1. Проверка subordinate IDs — Podman читает /etc/subuid и /etc/subgid:
  2. стандартное значение — диапазон из 65536 идентификаторов (user:100000:65536)
  3. если диапазонов нет, команды запуска контейнера не будет (no subuid ranges found)

  4. Строится mapping в user namespace:

  5. вызываются newuidmap/newgidmap (пакет uidmap)
  6. внутри контейнерный процесс видит себя root (UID 0), снаружи его файлы принадлежат диапазону вроде 100000…165535

  7. Поднимается rootless-сеть — Pasta (usermode-стек) и rootlessport (проброс портов)

  8. Создаётся pause-процесс, удерживающий user namespace открытым:

  9. в 6.0 есть экспериментальный режим без pause-процесса на ядрах ≥ 6.18 — через nsfs-дескриптор, включается env-флагом drop-pause-process

Настройка окружения

# диапазоны subordinate IDs
sudo usermod --add-subuids 200000-265535 --add-subgids 200000-265535 $USER
grep -E "^${USER}:" /etc/subuid /etc/subgid

# user namespaces не должны быть запрещены политикой ядра
cat /proc/sys/user/max_user_namespaces
sudo sysctl -w user.max_user_namespaces=15000

# сессия пользователя должна «жить» после logout
loginctl enable-linger $USER
echo "$XDG_RUNTIME_DIR"          # /run/user/1000
systemctl --user status

# привилегированные порты
sudo sysctl -w net.ipv4.ip_unprivileged_port_start=80

Без linger и XDG_RUNTIME_DIR разваливаются пользовательский systemd, socket-активированный Docker-совместимый API, Quadlet-юниты и сеть (Pasta требует runtime-каталог).

Ограничения rootless

Это не баги, а следствия отсутствия привилегий:

Ограничение Причина Решение
Bind-монты файлов другого владельца host UID ≠ container UID --userns=keep-id, :U, relabel, общий GID
Порты < 1024 нет CAP_NET_BIND_SERVICE net.ipv4.ip_unprivileged_port_start, --sysctl, rootful
--cpus, --memory нужен контроль cgroups cgroups v2 + делегирование в user slice
Raw-сокеты, ping нет CAP_NET_RAW снаружи --cap-add NET_RAW (внутри namespace)
--network host host network требует привилегий rootful
macvlan / ipvlan нужен CAP_NET_ADMIN rootful-сетап сети + rootless-контейнеры
iptables/nftables хоста нет CAP_NET_ADMIN правила внутри namespace; хостовые — только rootful

Полезный принцип: rootless для рабочих нагрузок, rootful для инфраструктурного сетевого сетапа, либо гибрид (один rootful «шлюз» + rootless-приложения).

Делегирование cgroups v2

Лимиты ресурсов в rootless возможны только потому, что systemd делегирует subtree-контроллеры пользовательскому slice:

podman info --format '{{.Host.CgroupManager}} {{.Host.CgroupsVersion}}'   # systemd 2
cat /sys/fs/cgroup/user.slice/user-1000.slice/cgroup.controllers
stat -fc %T /sys/fs/cgroup/                                               # cgroup2fs

Если контроллеров нет — проверьте унифицированную иерархию (systemd.unified_cgroup_hierarchy=1) и cgroup_manager = "systemd" в containers.conf. В Podman 6 поддержка cgroups v1 удалена полностью.

FUSE и overlay

Классическая связка — драйвер overlay поверх fuse-overlayfs, если ядро не даёт непривилегированный overlay. На современных ядрах native overlay в user namespace работает и FUSE не нужен:

podman info --format '{{.Store.GraphDriverName}}'
podman info --format '{{.Store.GraphOptions}}'

При конфликтах metacopy помогает явный overlay.mount_opt = ["nodev", "metacopy=on"] в storage.conf.

Нюансы и подводные камни

Обновление до 6.x

  1. Только cgroups v2 и nftables — хосты на cgroups v1 или iptables-legacy не запустят Podman 6 вообще
  2. CNI удалён — все сети пересоздаются на Netavark; старые конфиги из /etc/cni не работают, помогает podman network reload --all
  3. slirp4netns удалён — rootless-сеть теперь только Pasta; вместе с ним удалён глобальный --network-cmd-path
  4. BoltDB удалён — при первом запуске Podman 6 пытается мигрировать базу в SQLite автоматически
  5. Жёсткая версионная привязка — Podman 6.0 требует Buildah 1.44.0, Skopeo 1.23, Netavark/Aardvark 2.0.0 и конфиги из container-libs common/v0.68.0; смесь версий даёт нерабочую сеть
  6. Переписан парсинг конфигурации — приоритет источников в containers.conf/storage.conf/registries.conf изменился, после апгрейда стоит пересобрать конфиги и проверить поведение

Сеть и публикация портов

  1. Потеря source IP — rootlessport проксирует соединение, в логах приложения виден IP хоста; частичное решение — экспериментальный rootless_port_forwarder = "pasta" (6.0) или pvpassthrough в rootful
  2. Публикация на 0.0.0.0 по умолчанию — используйте --publish-address localhost|none|<ip> и новый параметр default_host_ips в containers.conf
  3. --internal отключает и NAT, и DNS-выход — не рассчитывайте на DNS-резолвинг в такой сети
  4. host.containers.internal у контейнеров с --net=host в 6.0 = 127.0.0.1, а не публичный адрес машины
  5. Сетевая изоляция (network isolation) теперь включена по умолчанию — раньше требовала явного включения; если у вас были образы, «незаметно» наследовавшие хостовые настройки сети, поведение после апгрейда нужно проверить

Хранилище и права

  1. Image volumes = bind с 5.0 (раньше tmpfs) — неожиданные данные в VOLUME-путях
  2. podman volume prune с 6.0 чистит только anonymous volumes (как Docker); прежнего поведения добивайтесь флагом --all, безопасно — через --dry-run
  3. podman commit теперь ставит контейнер на паузу — для «горячих» ФС с параллельной записью это лучше, но вернуть старое поведение можно --pause=false
  4. SELinux «съедает» bind-монты без :z/:Z; в rootless без container-selinux и верного типа файла будет EACCES без внятной ошибки в логе контейнера (смотреть ausearch/journalctl -k)

Команда и API

  1. --format='{{json .Labels}}' с 6.0 печатает k=v,k=v, а не JSON-карту — ломает jq-выражки в CI
  2. Фильтры podman volume list и label!= комбинируются через AND (раньше OR)
  3. MemorySwappiness в inspect теперь nil, если не задан явно
  4. podman compose — только делегат внешнего провайдера (docker-compose приоритетом, иначе podman-compose); своей реализации Compose Spec в Podman нет
  5. podman generate systemd объявлен deprecated — официальная рекомендация: Quadlet

Безопасность: CVE, о которых стоит знать

  • CVE-2026-57231 (исправлено в 6.0.0 / 5.8.4): образ с некорректно сформированными записями в Env мог привести к утечке переменных окружения хоста внутрь контейнера, в том числе с glob-*. Практический вывод: не передавайте окружение хоста (--env-host) в контейнеры из недоверенных реестров
  • CVE-2026-19730 (6.0.0): podman quadlet install --replace не усекал заменяемый файл — «хвост» старого содержимого оставался
  • CVE-2026-94603 (6.1.3 / 5.8.8): запуск checkpoint-образа (image с аннотацией io.podman.annotations.checkpoint.runtime.name) мог отключить всю песочницу, включая явно заданную пользователем. Поддержка checkpoint-образов в podman run удалена; CRIU-чекпойнты по-прежнему доступны как podman container checkpoint/restore

Производительность и нагрузка

Где возникает разница

Универсальных цифр «Podman быстрее Docker» не существует: steady-state у обоих стеков один и тот же — namespaces, cgroups v2, OCI-рантайм, overlay. Отличается модель владения процессом, и именно она даёт и плюсы, и минусы:

Аспект Podman Docker Engine
Память на простое нет демона — почти нулевая dockerd + containerd постоянно в RAM
Старт одной команды новый процесс + чтение SQLite быстрый путь через сокет живого демона
Очень много коротких контейнеров дороже per-invocation оверхед демон «прогрет»
Параллельные пользователи/CI-джобы без единой сериализующей точки общая очередь запросов к dockerd
Утечки и деградация со временем состояние только в БД и на диске долгий аптайм демона — известный источник «утечек»
Rootless по умолчанию отдельный режим установки

Что реально измерять на своём хосте

# стоимость запуска (без pull'а образа)
hyperfine -N --runs 30 'podman run --rm alpine true'
hyperfine -N --runs 30 'docker run --rm alpine true'

# последовательные запуски: эффект «прогретого» демона
time for i in $(seq 1 50); do podman run --rm alpine true; done

# распараллеливание — где Podman обычно выигрывает
time seq 1 100 | xargs -P20 -I{} podman run --rm alpine true
time seq 1 100 | xargs -P20 -I{} docker run --rm alpine true

# память «системы в целом» при 0 запущенных контейнерах
systemd-cgtop -m -b -1 | head -20

Измеряйте на том же ядре, с тем же хранилищем и с уже скачанными образами: pull и сетевые правила дают разброс больше, чем сама архитектура.

Оптимизация

  • Подогревайте store заранее — podman image exists + отдельный .image-юнит Quadlet вместо pull в момент старта сервиса
  • Сокращайте лишние CLI-вызовы в CI: один podman ps --format вместо цикла по контейнерам; podman exec --no-session для частых входов
  • Не оставляйте «мусор» в store — слои и промежуточные image-контейнеры раздувают граф; podman system df, затем точечный prune
  • Для rootless-пути выбирайте Pasta, а не rootlessport, если нужен source IP и меньше проксирующих процессов
  • Логи — в journald, а не в k8s-file: меньше файлов на хосте, ротация централизованная
  • Помните про podman-auto-update.service: этот юнит, запускаемый таймером, после обновления образа сам выполняет podman image prune -f

Эволюция стека: 4.x → 5.x → 6.x

Версия Дата Значимое
4.4 2023 Quadlet (.container и др.)
4.5–4.9 2023–2024 podman farm build, Netavark 1.x, OCI-артефакты, healthcheck-улучшения
5.0 03.2024 новая раскладка CLI (podman kube play/generate), image volumes = bind, Quadlet-расширения, --cpu-compat
5.1–5.5 2024–2025 Quadlet .pod/.build/.image, шаблоны юнитов, podman quadlet install/list
5.6–5.8 2025–2026 стабилизация; ветка 5.8 поддерживается и сейчас (5.8.9 — 12.10.2026)
6.0 24.06.2026 зачистка стека, AMD GPU, новые возможности podman machine, переписан парсинг конфигов
6.1.x с 08.2026 6.1.3 (29.09.2026): CVE-2026-94603 + удаление checkpoint-образов; 6.1.4 — 23.10.2026
6.2.0-RC1 26.10.2026 podman push --platform, kube play --multiple-pods/--validate/--label, Quadlet Umask= и SetMountsDependency=

Что нового в 6.x полезного в работе

  • --gpus работает не только с NVIDIA, но и с AMD
  • несколько статических IP на контейнер: --net mynet:ip=10.0.0.2,ip=10.0.0.3
  • типы маршрутов blackhole, unreachable, prohibit при создании сети
  • podman exec --no-session — быстрее в CI
  • Quadlet: UID=/GID=/Options= в .volume, anonymous volumes через Mount=, дополнительные пути поиска в /usr/share/containers/systemd/users
  • событие died несёт атрибут OOMKilled; фильтры --filter annotation=; события подов и volumes содержат метки
  • --tls-details — тонкая настройка TLS реестра; default_host_ips — адрес публикации по умолчанию
  • CDI-устройства видны в podman info; для артефактов появились lifecycle-события
  • podman machine os update, podman system hyperv-prep, --import-native-ca, все machine-команды работают с ВМ любого провайдера; на macOS провайдер по умолчанию — libkrun

Где всё ещё «незрело»

  • rootless_port_forwarder = "pasta" — экспериментальная опция, дефолт остался rootlessport
  • режим без pause-процесса в rootless (ядро ≥ 6.18) — за env-флагом
  • seccomp через аннотации в Kubernetes YAML — объявлен deprecated, удалится в 7.0

Сравнение с аналогами

Общая классификация

Инструмент Модель Rootless Pods / K8s YAML Типовой сценарий
Podman daemonless CLI + libpod ✅ из коробки ✅ / ✅ RHEL/Fedora по умолчанию, CI-раннеры, edge, multi-tenant
Docker Engine демон + containerd ⚠️ отдельный режим ❌ / ❌ экосистема Docker, Swarm, Desktop-UX
nerdctl клиент containerd ⚠️ требует настройки ❌ / ❌ K8s-нода, где containerd уже есть
Buildah + Skopeo без runtime-оркестрации ✅ ❌ / ❌ только сборка/перенос образов
LXD system-контейнеры ⚠️ ❌ / ❌ «мини-виртуализация» вместо приложений

Podman против Docker: предметно

Параметр Podman Docker
Модель fork/exec, демона нет центральный dockerd
Rootless режим по умолчанию отдельная установка
Init-система systemd/Quadlet нативно свои restart-механизмы
Группировка поды только compose
Kubernetes YAML podman kube play нет (kind/minikube)
Сборка Buildah BuildKit
compose внешний провайдер встроенный docker compose
API Docker Engine API, в 6.x — уровень v1.44 оригинал
Лицензия Apache-2.0, бесплатно в организациях любого размера Docker Desktop — платный для крупных компаний

Podman против nerdctl

Если на узле уже есть containerd (Kubernetes, K3s), nerdctl даёт docker-подобный CLI поверх него и обычно «быстрее» за счёт живого демона. Podman в этом сценарии выигрывает там, где нужны rootless, поды без kubelet, Quadlet и единый CLI для сборки/переноса; проигрывает — когда нужна максимальная совместимость с Docker-плагинами и нативный docker compose.

Когда что выбирать

Выбираем Podman, если:

✅ Вам нужен rootless без компромиссов — общий сервер, CI-раннер, multi-user хост

✅ Инфраструктура на RHEL/CentOS Stream/Fedora, где Podman идёт по умолчанию и закрыт SELinux-интеграцией

✅ Вы хотите управлять контейнерами через systemd (Quadlet) и не держать оркестратор ради трёх сервисов

✅ Нужны поды и запуск Kubernetes YAML локально: podman kube play/generate

✅ Важен отсутствие постоянно живущего root-демона как политика безопасности и аудита

✅ Вы собираете multi-arch образы и используете OCI-артефакты для моделей/отчётов

❌ Не выбирайте, если у вас хосты на cgroups v1 или iptables-legacy, а также если вся команда завязана на Docker-специфичные плагины, buildx и docker compose без готовности к обходам.

Выбираем Docker, если:

✅ Вам критична экосистема «из коробки»: Docker Compose v2, расширения, плагины, готовый Desktop-UX для неподготовленных пользователей

✅ Нужен Swarm как оркестратор

✅ Ваша CI-система массово использует DinD и buildx-фичи, и миграция дороже выигрыша

✅ Рабочие станции на Windows 10 или Intel Mac (в 6.x поддержка удалена)

❌ Не выбирайте, если требуется rootless-мультиарендность на одном сервере: отдельный демон на пользователя усложняет схему.

Комбинированное использование

Оба движка могут жить на одной машине, но не в одном окружении: типичная связка — Docker на рабочих станциях разработчиков (из-за Desktop-экосистемы) и Podman на серверах и в CI (из-за rootless и Quadlet). Общий реестр, общие образы, один Containerfile. Отдельно стоит помнить: смешивать docker- и podman-демоны на одном хосте технически возможно, но вы получаете два независимых хранилища образов и две сетевые модели — обычно это лишний шум.

Практические примеры использования

Веб-сервис как штатный systemd-сервис

# /etc/containers/systemd/nginx.container
[Unit]
Description=Nginx reverse proxy

[Container]
Image=docker.io/library/nginx:1.27-alpine
ContainerName=nginx              # иначе по умолчанию systemd-<имя файла>
PublishPort=8080:80
Volume=nginx-html:/usr/share/nginx/html:ro
Network=frontend
AutoUpdate=registry
HealthCmd=curl -fsS http://127.0.0.1/
HealthInterval=30s
HealthOnFailure=kill
PodmanArgs=--log-driver=journald

[Service]
Restart=always
TimeoutStartSec=900              # pull + start легко превышают дефолтные 90 с
MemoryHigh=512M
MemoryMax=1G

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now nginx.container
systemctl status nginx.service
journalctl -u nginx.service -f

Стек из пода + volume + сети

# pg-net.network
[Network]
Driver=bridge
Subnet=10.91.0.0/24
Gateway=10.91.0.1

# pg-data.volume
[Volume]
Driver=local
UID=999
GID=999
Copy=true

# db.pod
[Pod]
PodName=pgstack
PublishPort=5432:5432
Network=pg-net

# postgres.container
[Unit]
RequiresMountsFor=/srv/nvme

[Container]
Pod=pgstack
Image=docker.io/library/postgres:16-alpine
Environment=POSTGRES_PASSWORD_FILE=/run/secrets/pgpass
Secret=pgpass,target=/run/secrets/pgpass
Volume=pg-data:/var/lib/postgresql/data
UserNS=keep-id:uid=999,gid=999
ShmSize=256m
Exec=postgres -c shared_buffers=512MB

Приложение из Kubernetes YAML

# deploy.yaml — подмножество, которое понимает podman kube play
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 2
  selector:
    matchLabels: {app: api}
  template:
    metadata:
      labels: {app: api}
    spec:
      containers:
        - name: api
          image: ghcr.io/acme/api:1.4.2
          ports: [{containerPort: 8080}]
          envFrom:
            - configMapRef: {name: api-env}
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: api-env
data:
  LOG_LEVEL: info
podman kube play ./deploy.yaml
podman pod ps

Hardened-запуск одной командой

podman run -d --name app \
  --user 10001:10001 \
  --pull=always \
  --read-only \
  --tmpfs /tmp:rw,noexec,nosuid,size=64m \
  --cap-drop ALL --cap-add NET_BIND_SERVICE \
  --security-opt no-new-privileges \
  --security-opt seccomp=./seccomp.json \
  --security-opt label=level:s0:c100,c200 \
  --annotation com.example.team=platform \
  --pids-limit 256 --memory 512m --cpus 1.5 \
  --log-driver=journald --log-opt tag=app-prod \
  registry.internal/acme/app:1.4.2

Разбор по слоям защиты:

  1. Идентичность — --user без права смены владельца, --userns=keep-id/свой --uidmap в multi-tenant случае
  2. Привилегии — --cap-drop ALL плюс точечный --cap-add; no-new-privileges блокирует setuid-эскалацию
  3. ФС — read-only rootfs + tmpfs для записи; маскирование путей через --security-opt mask=/proc/sys/...
  4. MAC/системные вызовы — SELinux-уровень/тип, собственный seccomp-профиль
  5. Ресурсы — --memory, --cpus, --pids-limit против fork-bomb и oom-сценариев
  6. Supply chain — --pull=always + подпись и policy.json

Развёртывание «пакетом» через Quadlet-CLI

podman quadlet install ./myapp.container      # кладёт файл и связанные с ним ресурсы в подкаталог (6.0+)
podman quadlet list                           # есть алиас ls, поле Pod, фильтр status=
sudo systemctl daemon-reload
systemctl --user start myapp.service

Интеграция с systemd и Kubernetes

Типы Quadlet-юнитов

Quadlet — это systemd-generator: он читает файлы специальных расширений и генерирует обычные .service-юниты при загрузке и на каждом daemon-reload. В отличие от deprecated podman generate systemd, перерегенерация вручную не нужна.

Тип файла Что делает Type по умолчанию
.container запускает контейнер notify
.kube разворачивает Kubernetes YAML notify
.pod создаёт под forking
.volume гарантирует наличие volume oneshot
.network создаёт сеть oneshot
.image тянет и кэширует образ oneshot
.build собирает образ из Containerfile oneshot
.artifact работает с OCI-артефактами oneshot

Пути поиска (rootful, по убыванию приоритета): /run/containers/systemd/, /etc/containers/systemd/, /usr/share/containers/systemd/.

Пути поиска (rootless): $XDG_RUNTIME_DIR/containers/systemd/, ~/.config/containers/systemd/, /etc/containers/systemd/users/ и .../users/${UID}, /usr/share/containers/systemd/users/ и .../users/${UID}. Симлинки разрешены, подкаталоги сканируются рекурсивно.

Важно: Quadlet требует cgroups v2. Проверка на хосте: podman info --format {{.Host.CgroupsVersion}}.

Ключи [Container] — основные соответствия

Единственный обязательный ключ — Image. Остальные либо транслируются в флаги podman run, либо остаются в podmanArgs:

Ключ Quadlet Эквивалент podman run
Image= image specification
ContainerName= --name
Exec=, Entrypoint= команда после образа, --entrypoint
PublishPort=, ExposeHostPort= --publish, --expose
Volume=, Mount=, Tmpfs= --volume, --mount, --tmpfs
Network=, Pod=, StartWithPod= --network, --pod
Environment=, EnvironmentFile=, UnsetEnvironment= --env, --env-file
AddCapability=, DropCapability= --cap-add, --cap-drop
SecurityLabelDisable=, SecurityLabelType=, Mask=, Unmask= --security-opt label=..., mask=/unmask=
NoNewPrivileges=, SeccompProfile= --security-opt no-new-privileges, seccomp=
User=, Group=, UserNS=, UIDMap=, GIDMap=, GroupAdd= --user, --userns, --uidmap, --group-add
Memory=, PidsLimit=, ShmSize=, Ulimit= соответствующие resource-флаги
HealthCmd=, HealthInterval=, HealthOnFailure=, HealthStartup*= --health-*
AutoUpdate=registry --label io.containers.autoupdate=registry
Notify=true --sdnotify container
ReloadCmd=, ReloadSignal= ExecReload в юните
Retry=, RetryDelay= --retry, --retry-delay
PodmanArgs= любые остальные флаги
GlobalArgs= глобальные опции CLI (--log-level и т. п.)

Порядок запуска и зависимости

Зависимости задаются обычными секциями systemd, а не флагами Podman:

# api.container
[Unit]
Requires=api-build.service pg-data.service
After=api-build.service pg-data.service

[Container]
Image=localhost/acme/app:latest

Механика Quadlet: генератор добавляет RequiresMountsFor= для известных путей из Volume=; в 6.2 это стало управляемым через SetMountsDependency= (Requires/Wants/ничего). Для «nologin»-пользователей юниты смотрятся так: sudo systemctl --machine alice@ --user list-unit-files.

Healthcheck и auto-update вместо self-healing-контейнеров

  • HealthOnFailure=kill убивает контейнер после провала проверки, Restart=always поднимает его заново — это встроенная замена docker-autoheal
  • с 6.2 учитывается StartInterval из конфигурации healthcheck образа (startup-периоды)
  • systemd-юниты, которые Podman создаёт для собственных healthcheck-циклов, явно именованы — libpod-healthcheck-$CONTAINERID-$randsuffix.service
  • AutoUpdate=registry + таймер podman-auto-update.timer дают «image pull → перезапуск через systemd → откат при неудаче» без оркестратора; для локально собранных образов есть scope image (rebuild по Containerfile)
systemctl --user enable --now podman-auto-update.timer
podman auto-update --dry-run
podman auto-update --filter label=tier=frontend        # 6.2; работает и на remote-клиенте

Шаблоны, drop-in'ы и rootless-нюансы

# worker@.container
[Container]
Image=docker.io/library/busybox
Exec=sh -c "echo job %i"
ContainerName=worker-%i
systemctl --user start worker@backup.service worker@report.service
systemctl --user stop 'worker@*.service'

Drop-ins работают как в systemd: для foo.container просматриваются foo.container.d/*.conf, усечённые уровни foo-.container.d/, foo-bar-.container.d/ и общий container.d/; одноимённые файлы на более низком уровне переопределяют верхние.

Важно: Quadlet-юниты нельзя запускать «под другим пользователем» через [Service] User=/DynamicUser= — для rootless нужно создать пользователя и положить файл в один из user-путей поиска.

Интеграция с Kubernetes: границы применимости

podman kube play — это не kubelet. Он подходит для:
- одного хоста с несколькими «микросервисами» и desired-состоянием, описанным YAML'ом
- офлайн-проверки манифестов перед применением в настоящем кластере
- edge-устройств, где кластерный контроллер неоправдан

Он не подходит для: балансировки между нодами, self-healing на уровне реплик, RBAC/NetworkPolicy, операторов и CRD.

Мониторинг и наблюдаемость

Логи

podman run -d --log-driver=journald --log-opt tag="api-prod" \
  --log-opt label=env=prod ghcr.io/acme/app      # 6.0: дополнительные метки в journal

journalctl -t api-prod -o json | jq -r '.MESSAGE'
podman logs --tail=200 --since=10m --timestamps api
journalctl -u api.service                          # логи контейнера + сообщения systemd в одном потоке

journald — правильный драйвер в systemd-мире: не нужна ручная ротация json-file, вывод привязан к cgroup/юниту. На высоконагруженных хостах не забудьте про лимиты журнала (SystemMaxUse=).

События

podman events --since 24h --filter type=container --filter event=died --format json
podman events --filter network=backend             # 6.2: фильтр по сети
podman events --filter label=tier=frontend

Событие died содержит OOMKilled (6.0), события подов и volumes несут метки — этого достаточно, чтобы собрать простой алертинг через podman events | jq → Vector.

Метрики и диагностика

Задача Средство
Мгновенная нагрузка контейнера podman stats --no-stream
Расход места по типам объектов podman system df, podman system df --format json
Состояние движка и хоста podman info --debug
Что изменилось в rootfs podman diff <ctr>
События и аудит podman events
Prometheus-эндпоинт встроенного нет → cAdvisor (умеет libpod) или свой exporter на podman ps/stats
Трейсинг strace/eBPF поверх conmon + crun

Что мониторить обязательно

  1. /var/lib/containers (или ~/.local/share/containers) — разрастание слоёв и anonymous volumes
  2. Ошибки юнитов — systemctl --user --failed, особенно после daemon-reload
  3. podman events --filter event=died — рост числа аварийных завершений
  4. Журнал ядра на предмет OOM и cgroup-делегирования
  5. Успешность podman auto-update — при неудачном pull юнит уйдёт в откат, и это надо видеть

Развёртывание и эксплуатация

Установка на Linux

# Fedora / CentOS Stream / RHEL (Podman там предустановлен)
sudo dnf -y install podman
sudo dnf -y install podman-machine       # на Fedora — для команд podman machine

# Debian / Ubuntu
sudo apt-get update && sudo apt-get -y install podman

# Arch
sudo pacman -S podman

# openSUSE
sudo zypper install podman

# Alpine
sudo apk add podman

Проверка окружения:

podman info --format '{{.Version.Version}} cgroups={{.Host.CgroupsVersion}} runtime={{.Host.OCIRuntime.Name}} rootless={{.Host.Security.Rootless}}'
stat -fc %T /sys/fs/cgroup/              # cgroup2fs — иначе Podman 6 не запустится
cat /proc/sys/user/max_user_namespaces
podman run --rm docker.io/library/alpine uname -a

Если cgroupVersion = 1, нужен переход на унифицированную иерархию: загрузочный параметр systemd.unified_cgroup_hierarchy=1 и перезагрузка.

Вне Linux: podman machine

podman machine init --now --cpus 4 --memory 8192 --disksize 100GB
podman machine start
podman machine list
podman ssh -m podman-machine-default
podman machine inspect --format '{{.ConnectionInfo.PodmanSocket.Path}}'

В 6.0: все machine-команды работают с ВМ любого провайдера (опция --all-providers удалена), добавлены podman machine os update (кроме wsl), --provider, --import-native-ca и podman system hyperv-prep; на macOS провайдер по умолчанию — libkrun; Linux-ВМ монтируют пользовательский конфиг (~/.config/containers) в /etc/containers гостя, но старые Linux-ВМ с volume-маунтами приходится пересоздавать.

Конфигурация реестров и аутентификации

# /etc/containers/registries.conf
unqualified-search-registries = ["docker.io", "quay.io"]

[[registry]]
prefix   = "docker.io"
location = "registry-1.docker.io"

[[registry.mirror]]                       # pull-through кэш внутри сети
location = "internal-registry.example.com:5000/net"

[[registry]]
prefix   = "internal-registry.example.com:5000"
location = "internal-registry.example.com:5000"

Дроп-файлы в /etc/containers/registries.conf.d/*.conf позволяют дистрибутиву и конфигурационным менеджерам не править основной файл. Имя без реестра (nginx) разрешается через unqualified-search-registries: в интерактивном режиме будет вопрос, в CI — ошибка.

podman login -u deploy --authfile ~/.config/containers/auth.json internal-registry.example.com:5000
export REGISTRY_AUTH_FILE=~/.config/containers/auth.json    # общий для podman/buildah/skopeo

Хранилище: storage.conf

# /etc/containers/storage.conf (root) или ~/.config/containers/storage.conf (rootless)
[storage]
driver    = "overlay"
runroot   = "/run/containers/storage"
graphroot = "/var/lib/containers/storage"

[storage.options]
mountopt = "nodev,metacopy=on"

[storage.options.overlay]
mount_program = "/usr/bin/fuse-overlayfs"    # убрать, если native overlay работает в rootless

Таймеры обслуживания и очистка

systemctl --user enable --now podman-auto-update.timer

podman system df
podman container prune
podman image prune -a
podman volume prune --dry-run          # 6.0: по умолчанию чистятся только anonymous volumes
podman volume prune --all
podman system prune -a --volumes       # радикально: всё неиспользуемое

Резервное копирование volumes

# через временный контейнер (универсальный способ)
podman run --rm -v pg-data:/d:ro docker.io/library/busybox tar cf - /d > pg-data.tar

# на хосте: rootless — через podman unshare
podman unshare podman volume mount pg-data
rsync -aX "$(podman volume inspect -f '{{.Mountpoint}}' pg-data)/" /backup/pg-data/
podman unshare podman volume unmount pg-data

CRIU-чекпойнты (podman container checkpoint/restore) работают, но чувствительны к рантайму и содержимому контейнера (GPU, сокеты) и недоступны как «checkpoint-образы» после CVE-2026-94603.

Миграция с Docker на Podman

Docker остаётся стандартом «по умолчанию» для рабочих станций, но на серверах мотивация перейти на Podman обычно три: rootless, systemd-интеграция и отсутствие root-демона. Ниже — практический разбор, а не обещание «поставьте алиас и забудьте».

Сравнение синтаксиса

Docker:

docker run -d --name web -p 8080:80 \
  --restart=unless-stopped \
  -v /srv/web:/usr/share/nginx/html:ro \
  nginx:1.27-alpine
docker exec -it web sh
docker compose up -d

Podman:

podman run -d --name web -p 8080:80 \
  --restart=unless-stopped \
  -v /srv/web:/usr/share/nginx/html:z \
  docker.io/library/nginx:1.27-alpine
podman exec -it web sh
podman compose up -d            # требует внешнего docker-compose или podman-compose
# либо декларативно, без compose вовсе:
sudo systemctl enable --now web.container

Ключевые отличия при миграции

Аспект Docker Podman
Демон обязателен не нужен
Rootless отдельный режим по умолчанию
Порты < 1024 в rootless — требует sysctl или rootful
restart без systemd --restart=... --restart работает, но «каноничный» путь — Quadlet
compose встроен внешний провайдер
buildx / Bake ✅ ⚠️ (podman build, podman farm, манифесты вручную)
DinD штатный паттерн ⚠️ нестабилен, лучше Kaniko/Buildah/remote
Swarm ✅ ❌
Kubernetes YAML ❌ ✅ podman kube play
SELinux неактуально метки :z/:Z, типы container_t
Плагин-экосистема (network/auth/logging) широкая уже, часть через стандартный API

Когда миграция оправдана

  • Сервер мультиарендный: несколько команд/пользователей на одном хосте
  • Требуется rootless по политикам безопасности или по требованиям аудита
  • Инфраструктура на RHEL-линейке, где Podman уже поставляется по умолчанию
  • Контейнеры должны быть «обычными» systemd-сервисами с journalctl и лимитами cgroup
  • Нужен локальный запуск Kubernetes YAML без кластера

Что следует учитывать: активное использование docker buildx, docker compose v2 и Swarm означает, что часть конвейера придётся перестраивать (Buildah/манифесты, внешний compose-провайдер, Quadlet/kube play вместо Swarm). Оценивайте стоимость как проект на спринт, а не как замену бинарника.

Чек-лист миграции

  1. Инвентаризация — docker ps -a, compose-файлы, --net=host, монтирования docker.sock внутрь контейнеров
  2. Хост — cgroups v2, nftables, наличие container-selinux там, где SELinux включён
  3. Реестры — registries.conf, зеркала, policy.json и подписи вместо --tls-verify=false
  4. Сеть — адресная публикация портов, потеря source IP в rootless, macvlan только rootful
  5. Хранилище — :z/:Z, --userns=keep-id, ожидания по image volumes
  6. systemd — замена самописных unit-файлов на Quadlet (не на podman generate systemd)
  7. Канареечный прогон — часть сервисов на Podman, сравнение логов, latency, памяти
  8. Откат — Docker-сокет и старый юнит остаются включёнными до конца проверки

Итог

Podman: идеальный выбор, когда...

Критерий Оценка
Сложность настройки ★★★☆☆ (просто на RHEL-линейке, чуть больше на Debian/Ubuntu)
Rootless ✅ (режим по умолчанию)
Интеграция с systemd ✅ (Quadlet, healthcheck, auto-update)
Kubernetes YAML без kubelet ✅ (podman kube play)
Docker-экосистема «из коробки» ⚠️ (через API-совместимость и внешние провайдеры)
Свой оркестратор ❌ (Swarm-аналога нет)
Требования к хосту ⚠️ cgroups v2 + nftables (6.x)
Стоимость и лицензия ✅ (Apache-2.0, CNCF)

Рекомендуется для: серверов и CI-раннеров с несколькими пользователями, systemd-ориентированных развёртываний, edge-устройств, сборки multi-arch образов и локального запуска Kubernetes YAML.

Docker: идеальный выбор, когда...

Критерий Оценка
Зрелость экосистемы ★★★★★
compose / buildx / Bake ✅ (нативно)
Desktop-UX для неподготовленных ✅
Rootless ⚠️ (отдельный режим)
Оркестрация без K8s ✅ (Swarm)
Поверхность атаки ⚠️ (привилегированный демон)

Рекомендуется для: рабочих станций с Docker Desktop, пилотов с активным использованием buildx/compose-плагинов, окружений, где Swarm уже работает и оправдан.

Комбинированное использование

Рекомендуется для: организаций, где разработчики живут на Docker Desktop, а серверы и CI переходят на Podman. Общие образы, один Containerfile, один внутренний реестр — движки тогда не конфликтуют, потому что не пересекаются по окружениям.

Заключение

Podman и Docker решают одну задачу, но с разными моделями владения процессом, и выбор корректен только в контексте конкретной инфраструктуры:

Сценарий Выбор
«Нужно запускать сервисы на сервере как systemd-юниты без root» Podman + Quadlet
«Нужен rootless CI-раннер на общем хосте» Podman
«Хочу запускать Kubernetes YAML локально без кластера» Podman (kube play)
«Вся команда на Docker Desktop, много buildx-плагинов» Docker
«Нужен Swarm и готовая плагин-экосистема» Docker
«Нужна единая сборка multi-arch + OCI-артефакты» Podman (или Buildah + Skopeo напрямую)

Podman к 2026 году заметно «повзрослел»: в 6.0 убрали cgroups v1, iptables, CNI, slirp4netns и BoltDB, API-совместимость поднята до уровня Docker v1.44, Quadlet покрыл почти все сценарии systemd-деплоя, а проект переехал в CNCF — это гарантирует независимость от одного вендора. Платой за это стала жёсткость требований к хосту и необходимость относиться к апгрейду 5.x → 6.x как к миграции.

Выбор между ними — не вопрос «что лучше», а вопрос «какая модель владения процессом подходит вашей архитектуре». Если вы управляете серверами, любите systemctl и не хотите root-демона — Podman сегодня самый логичный вариант. Если вы живёте в экосистеме Docker Desktop, buildx и Swarm — Podman имеет смысл хотя бы на серверной половине инфраструктуры.

Шпаргалка по API

Podman. Шпаргалка

Ключевые команды

podman version --format '{{.Server.Version}}'
podman info --debug > info.json
podman ps -a --size --format '{{.Names}}\t{{.Status}}'
podman pod ps
podman network ls --format '{{.Name}} {{.Driver}}'
podman volume ls
podman system df
podman events --since 1h --format json

Флаги podman run, которые реально нужны

podman run -d --name app \
  --user 10001:10001 \                  # идентичность процесса
  --userns=keep-id \                    # ваш UID в контейнере остаётся вашим
  --group-add keep-groups \             # supplementary-группы хоста
  --pull=always \                       # всегда сверять digest
  --read-only --tmpfs /tmp \            # неизменяемый rootfs
  --cap-drop ALL --cap-add NET_BIND_SERVICE \
  --security-opt no-new-privileges \
  --security-opt seccomp=profile.json \
  --security-opt label=type:container_t \
  --memory 512m --cpus 1.5 --pids-limit 256 \
  --shm-size 256m \
  --health-cmd "curl -fsS http://127.0.0.1/health" \
  --health-interval 30s --health-retries 3 \
  --health-on-failure kill \            # + --restart=always = self-healing
  --log-driver=journald --log-opt tag=app-prod \
  --publish-address localhost --publish 8443:443 \
  --restart=unless-stopped \
  registry.internal/acme/app:1.4.2

Сеть: быстрые рецепты

podman network create backend --subnet 10.89.0.0/24 --gateway 10.89.0.1 --dns=true
podman network create guarded --internal                    # без NAT и DNS-выхода
podman network create blocked --route 10.20.30.0/24,blackhole
podman run --network backend:alias=api --network-alias gateway ghcr.io/acme/app
podman run --net mynet:ip=10.0.0.2,ip=10.0.0.3 app        # несколько статических IP (6.0+)
podman network reload --all

Поды, kube и Quadlet

podman pod create --name webstack -p 80:80
podman run -d --pod webstack --name nginx docker.io/library/nginx:1.27-alpine
podman kube play ./deploy.yaml
podman kube generate webstack > webstack.yaml
systemctl daemon-reload && systemctl --user start webstack.service
podman quadlet list

Сборка, манифесты, артефакты

podman build --layers -t myapp:1.4.2 .
podman build --platform linux/amd64,linux/arm64 -t myapp:1.4.2 --push .
podman manifest create myapp:1.4.2
podman manifest add myapp:1.4.2 containers-storage:myapp-amd64 --os linux --arch amd64
podman manifest push myapp:1.4.2 docker.io/acme/myapp:1.4.2
podman farm create multiarch host-a host-b
podman artifact add --file-type application/gzip quay.io/acme/models:v1 ./model.bin.gz
podman image sign --sign-by platform@acme.example.com myapp:1.4.2
podman image trust show

Docker-совместимый сокет

# rootful
sudo systemctl enable --now podman.socket          # /run/podman/podman.sock

# rootless (для конкретного пользователя)
systemctl --user enable --now podman.socket
export DOCKER_HOST="unix://$XDG_RUNTIME_DIR/podman/podman.sock"

# вручную, с idle-таймаутом
podman system service -t 0 unix:///run/podman/podman.sock

API: уровень Docker Engine API в 6.x — v1.44; поведение части полей (Labels-формат, MemorySwappiness, Aliases vs DNSNames в inspect) приведено к Docker-скому.

Полезные --format-шаблоны

podman ps --format '{{.Names}}|{{.Status}}|{{.Image}}'
podman inspect web --format '{{.State.Status}} {{.State.ExitCode}} {{.Config.Image}}'
podman image ls --format '{{.Repository}}:{{.Tag}} {{.ID}}'
podman volume ls --format '{{.Name}} {{.Mountpoint}}'
podman info --format '{{.Host.CgroupsVersion}} {{.Host.OCIRuntime.Name}} {{.Store.GraphDriverName}}'

Конфигурационные файлы

Файл Отвечает
/etc/containers/containers.conf (+ ~/.config/containers/containers.conf) движок: cgroup_manager, runtime, events_logger, rootless_port_forwarder, default_host_ips
/etc/containers/storage.conf граф-драйвер, graphroot/runroot, опции overlay
/etc/containers/registries.conf + .d/*.conf search-реестры, зеркала, TLS/insecure
/etc/containers/policy.json политика доверия подписям
/etc/subuid, /etc/subgid диапазоны subordinate IDs для rootless
/etc/containers/systemd/** (и user-варианты) Quadlet-юниты

Troubleshooting

Симптом Вероятная причина Что делать
no subuid ranges found for user пустые /etc/subuid//etc/subgid usermod --add-subuids, проверить getent passwd
podman run зависает на pull, systemd убивает юнит дефолтный TimeoutStartSec=90 TimeoutStartSec=900 или предварительный pull через .image-юнит
Ошибка/prompt при podman pull nginx не настроены unqualified-search-registries полное имя docker.io/library/nginx или registries.conf
EACCES на bind mount несовпадение UID / SELinux label :z/:Z, --userns=keep-id, podman unshare chown
502/connection refused на опубликованный порт адресная публикация, прокси rootlessport --publish-address, default_host_ips, ss -lntup
Quadlet-юнит не появляется не было daemon-reload; неверный путь; cgroups v1 systemctl daemon-reload, проверить путь и .Host.CgroupsVersion
Контейнеры не пережили logout нет linger loginctl enable-linger $USER
После апгрейда не работают сети CNI→Netavark, slirp4netns→Pasta пересоздать сети, podman network reload --all, проверить версии пакетов
volume prune ничего не удаляет 6.0: только anonymous volumes --dry-run, затем --all
Медленный podman exec в CI API-сессия и запись в БД --no-session
Старые данные в VOLUME-пути образа image-volume = bind (5.0+) --image-volume=tmpfs\|ignore
podman run --checkpoint недоступен checkpoint-образы удалены (6.1.3) podman container checkpoint/restore

Важные нюансы при работе с API

Podman

  1. Требования хоста в 6.x жёсткие — cgroups v2, nftables, Netavark, Pasta, SQLite; на cgroups v1 Podman 6 не запускается вообще
  2. --restart — не замена systemd — --restart=... работает через внутренний механизм libpod, но «каноничный» production-путь — Quadlet-юнит с Restart=
  3. podman compose — делегат — требуется внешний docker-compose или podman-compose; Compose Spec в самом Podman не реализован
  4. Rootless публикует порты через прокси — source IP теряется, пока не включён rootless_port_forwarder = "pasta" (экспериментально) или pvpassthrough
  5. Image volumes обрабатываются как bind (с 5.0), а не tmpfs: проверяйте образы с VOLUME
  6. podman volume prune чистит только anonymous volumes (с 6.0), раньше — все неиспользуемые; для прежнего поведения нужен --all
  7. Checkpoint-образы удалены из podman run (6.1.3, CVE-2026-94603) — CRIU-чекпойнты остаются только как podman container checkpoint/restore
  8. Не полагайтесь на имена без реестра в скриптах — задавайте docker.io/library/... явно или настройте unqualified-search-registries

Quadlet

  1. Единственный обязательный ключ — Image в [Container]; имя контейнера по умолчанию systemd-<имя файла>, переопределяется ContainerName=
  2. Type задавать не нужно — генератор ставит notify для .container/.kube, forking для .pod, oneshot для .volume/.network/.build/.image/.artifact
  3. После правки файла обязателен systemctl daemon-reload — без него новые и изменённые юниты не появятся
  4. TimeoutStartSec нужен при первом pull — дефолтных 90 с не хватает на загрузку больших образов (для Type=oneshot таймаут по умолчанию отключён)
  5. [User]/DynamicUser в Quadlet не работают — rootless достигается размещением файла в user-путях поиска
  6. Зависимости — через [Unit] (Requires=/After=/RequiresMountsFor=), а не через флаги Podman; SetMountsDependency= (6.2) управляет автоматическим RequiresMountsFor
  7. Шаблоны (name@.container) и drop-in'ы поддерживаются на всех уровнях иерархии имён
Download PDF