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
Что это даёт на практике:
- Меньше поверхность атаки — нет долгоживущего root-демона с API-сокетом, компрометация которого равна компрометации хоста
- Контейнер как сервис —
systemctl status,journalctl -u,Restart=,MemoryMax=работают без «костылей» - Изоляция пользователей — UID/GID-диапазоны, хранилище и сети каждого пользователя не пересекаются
- Каскадная остановка —
systemctl stopдаётSIGTERM, затемSIGKILLпо таймауту - Нет единой сериализующей точки — параллельные запуски от разных пользователей не конкурируют за один API-воркер
conmon при этом остаётся важным процессом: он держит открытыми pty/pipes контейнера, пишет вывод в выбранный log-драйвер (k8s-file по умолчанию для run, journald — рекомендуемый в systemd-окружении), фиксирует код завершения и генерирует событие died, а также обрабатывает SIGWINCH. Начиная с 6.2 сигналы корректно проксируются и для podman exec: Ctrl-C убивает и процесс внутри контейнера.
Возможности и API
Ключевые группы команд
Podman построен вокруг нескольких плоских пространств команд, и почти всё, что вы умеете в Docker, здесь имеет прямой аналог:
- Жизненный цикл контейнеров — работа с отдельным контейнером:
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+)
- Образы и реестры — доставка артефактов:
podman pull/push/build/save/load/tag/searchpodman image tree,podman history,podman manifest ...-
podman login/logout,podman image trust,podman image sign -
Группировка и декларативность:
podman pod create|ps|stats|stop|rmpodman kube play/podman kube generate(Kubernetes YAML)podman quadlet install/list-
podman generate kube(легаси-алиасы, предпочтительна новая раскладкаkube ...) -
Ресурсы платформы:
podman network,podman volume,podman secret,podman config-
podman system df|prune|migrate|service|events,podman auto-update -
Удалённое управление:
podman system connection add|default|list|remove- глобальный
--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: как это устроено
Что происходит при первом запуске
- Проверка subordinate IDs — Podman читает
/etc/subuidи/etc/subgid: - стандартное значение — диапазон из 65536 идентификаторов (
user:100000:65536) -
если диапазонов нет, команды запуска контейнера не будет (
no subuid ranges found) -
Строится mapping в user namespace:
- вызываются
newuidmap/newgidmap(пакетuidmap) -
внутри контейнерный процесс видит себя root (UID 0), снаружи его файлы принадлежат диапазону вроде
100000…165535 -
Поднимается rootless-сеть — Pasta (usermode-стек) и rootlessport (проброс портов)
-
Создаётся pause-процесс, удерживающий user namespace открытым:
- в 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
- Только cgroups v2 и nftables — хосты на cgroups v1 или iptables-legacy не запустят Podman 6 вообще
- CNI удалён — все сети пересоздаются на Netavark; старые конфиги из
/etc/cniне работают, помогаетpodman network reload --all - slirp4netns удалён — rootless-сеть теперь только Pasta; вместе с ним удалён глобальный
--network-cmd-path - BoltDB удалён — при первом запуске Podman 6 пытается мигрировать базу в SQLite автоматически
- Жёсткая версионная привязка — Podman 6.0 требует Buildah 1.44.0, Skopeo 1.23, Netavark/Aardvark 2.0.0 и конфиги из
container-libscommon/v0.68.0; смесь версий даёт нерабочую сеть - Переписан парсинг конфигурации — приоритет источников в
containers.conf/storage.conf/registries.confизменился, после апгрейда стоит пересобрать конфиги и проверить поведение
Сеть и публикация портов
- Потеря source IP — rootlessport проксирует соединение, в логах приложения виден IP хоста; частичное решение — экспериментальный
rootless_port_forwarder = "pasta"(6.0) илиpvpassthroughв rootful - Публикация на 0.0.0.0 по умолчанию — используйте
--publish-address localhost|none|<ip>и новый параметрdefault_host_ipsвcontainers.conf --internalотключает и NAT, и DNS-выход — не рассчитывайте на DNS-резолвинг в такой сетиhost.containers.internalу контейнеров с--net=hostв 6.0 =127.0.0.1, а не публичный адрес машины- Сетевая изоляция (network isolation) теперь включена по умолчанию — раньше требовала явного включения; если у вас были образы, «незаметно» наследовавшие хостовые настройки сети, поведение после апгрейда нужно проверить
Хранилище и права
- Image volumes =
bindс 5.0 (раньшеtmpfs) — неожиданные данные вVOLUME-путях podman volume pruneс 6.0 чистит только anonymous volumes (как Docker); прежнего поведения добивайтесь флагом--all, безопасно — через--dry-runpodman commitтеперь ставит контейнер на паузу — для «горячих» ФС с параллельной записью это лучше, но вернуть старое поведение можно--pause=false- SELinux «съедает» bind-монты без
:z/:Z; в rootless безcontainer-selinuxи верного типа файла будетEACCESбез внятной ошибки в логе контейнера (смотретьausearch/journalctl -k)
Команда и API
--format='{{json .Labels}}'с 6.0 печатаетk=v,k=v, а не JSON-карту — ломает jq-выражки в CI- Фильтры
podman volume listиlabel!=комбинируются через AND (раньше OR) MemorySwappinessв inspect теперьnil, если не задан явноpodman compose— только делегат внешнего провайдера (docker-composeприоритетом, иначеpodman-compose); своей реализации Compose Spec в Podman нет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
Разбор по слоям защиты:
- Идентичность —
--userбез права смены владельца,--userns=keep-id/свой--uidmapв multi-tenant случае - Привилегии —
--cap-drop ALLплюс точечный--cap-add;no-new-privilegesблокирует setuid-эскалацию - ФС — read-only rootfs + tmpfs для записи; маскирование путей через
--security-opt mask=/proc/sys/... - MAC/системные вызовы — SELinux-уровень/тип, собственный seccomp-профиль
- Ресурсы —
--memory,--cpus,--pids-limitпротив fork-bomb и oom-сценариев - 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 → откат при неудаче» без оркестратора; для локально собранных образов есть scopeimage(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 |
Что мониторить обязательно
/var/lib/containers(или~/.local/share/containers) — разрастание слоёв и anonymous volumes- Ошибки юнитов —
systemctl --user --failed, особенно послеdaemon-reload podman events --filter event=died— рост числа аварийных завершений- Журнал ядра на предмет OOM и
cgroup-делегирования - Успешность
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). Оценивайте стоимость как проект на спринт, а не как замену бинарника.
Чек-лист миграции
- Инвентаризация —
docker ps -a, compose-файлы,--net=host, монтированияdocker.sockвнутрь контейнеров - Хост — cgroups v2, nftables, наличие
container-selinuxтам, где SELinux включён - Реестры —
registries.conf, зеркала,policy.jsonи подписи вместо--tls-verify=false - Сеть — адресная публикация портов, потеря source IP в rootless, macvlan только rootful
- Хранилище —
:z/:Z,--userns=keep-id, ожидания по image volumes - systemd — замена самописных unit-файлов на Quadlet (не на
podman generate systemd) - Канареечный прогон — часть сервисов на Podman, сравнение логов, latency, памяти
- Откат — 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
- Требования хоста в 6.x жёсткие — cgroups v2, nftables, Netavark, Pasta, SQLite; на cgroups v1 Podman 6 не запускается вообще
--restart— не замена systemd —--restart=...работает через внутренний механизм libpod, но «каноничный» production-путь — Quadlet-юнит сRestart=podman compose— делегат — требуется внешнийdocker-composeилиpodman-compose; Compose Spec в самом Podman не реализован- Rootless публикует порты через прокси — source IP теряется, пока не включён
rootless_port_forwarder = "pasta"(экспериментально) илиpvpassthrough - Image volumes обрабатываются как
bind(с 5.0), а неtmpfs: проверяйте образы сVOLUME podman volume pruneчистит только anonymous volumes (с 6.0), раньше — все неиспользуемые; для прежнего поведения нужен--all- Checkpoint-образы удалены из
podman run(6.1.3, CVE-2026-94603) — CRIU-чекпойнты остаются только какpodman container checkpoint/restore - Не полагайтесь на имена без реестра в скриптах — задавайте
docker.io/library/...явно или настройтеunqualified-search-registries
Quadlet
- Единственный обязательный ключ —
Imageв[Container]; имя контейнера по умолчаниюsystemd-<имя файла>, переопределяетсяContainerName= Typeзадавать не нужно — генератор ставитnotifyдля.container/.kube,forkingдля.pod,oneshotдля.volume/.network/.build/.image/.artifact- После правки файла обязателен
systemctl daemon-reload— без него новые и изменённые юниты не появятся TimeoutStartSecнужен при первом pull — дефолтных 90 с не хватает на загрузку больших образов (дляType=oneshotтаймаут по умолчанию отключён)[User]/DynamicUserв Quadlet не работают — rootless достигается размещением файла в user-путях поиска- Зависимости — через
[Unit](Requires=/After=/RequiresMountsFor=), а не через флаги Podman;SetMountsDependency=(6.2) управляет автоматическимRequiresMountsFor - Шаблоны (
name@.container) и drop-in'ы поддерживаются на всех уровнях иерархии имён