Если вы привыкли разворачивать контейнеры через Docker Compose, но хотите более нативной интеграции с systemd и безопасного rootless‑режима, обратите внимание на связку Podman + Quadlet. Quadlet — это генератор unit‑ов: вы описываете контейнер в .container/.pod файле, а он создаёт полноценную systemd‑службу, которую удобно запускать как user‑юнит (systemd --user) на вашем VDS. Получается чисто, предсказуемо, без лишних демонов и с удобным журналированием через journald.
Что такое Quadlet и зачем он на VDS
Quadlet входит в Podman и позволяет хранить декларативные описания контейнеров в каталоге пользователя, автоматически генерируя systemd‑юниты. Это избавляет от ручного написания сложных ExecStart, ускоряет онбординг и делает инфраструктуру прозрачнее. На VDS это особенно удобно: вы получаете автозапуск при перезагрузке, единый формат логов, гибкие рестарты, resource‑лимиты через systemd и минимум привилегий благодаря rootless.
Идея проста: вместо «супервизора поверх контейнеров» — сам systemd становится супервизором. Quadlet аккуратно подготавливает unit‑файл, а вы управляете сервисом привычными командами systemctl — только в user‑сессии.
Предварительные требования и версии
Рекомендуемые версии для комфортной работы:
- Podman 4.4+ (на свежих Linux‑дистрибутивах обычно уже так).
- systemd 252+ (user‑юниты стабильны; включаем «linger» для автозапуска после ребута).
- cgroups v2 (на современных дистрибутивах включён по умолчанию).
- Пакеты:
podman,uidmap,slirp4netns,fuse-overlayfs, а также плагины Podman (часто в составе базового пакета) для проброса портов в rootless.
Пути хранения для rootless Podman:
- Образы и контейнеры:
~/.local/share/containers. - Конфиги Quadlet:
~/.config/containers/systemd.
Установка на популярных дистрибутивах
Debian 12 / Ubuntu 22.04+:
sudo apt update
sudo apt install -y podman uidmap slirp4netns fuse-overlayfs
Rocky Linux 9 / AlmaLinux 9 / RHEL 9+:
sudo dnf install -y podman uidmap slirp4netns fuse-overlayfs
Проверьте версии:
podman --version
systemctl --version

Выделяем пользователя для контейнеров
Создадим отдельный аккаунт, под которым будут крутиться rootless‑контейнеры (или используйте свой обычный, если так удобнее):
sudo useradd -m -s /bin/bash app
sudo passwd app
Проверьте, что у пользователя есть записи в /etc/subuid и /etc/subgid (обычно добавляются автоматически):
grep ^app: /etc/subuid /etc/subgid || echo "subuid/subgid для app не найдены"
Если диапазонов нет, добавьте вручную безопасный диапазон (пример):
echo "app:100000:65536" | sudo tee -a /etc/subuid
echo "app:100000:65536" | sudo tee -a /etc/subgid
Включаем автозапуск user‑юнитов после ребута через «linger»:
sudo loginctl enable-linger app
Проверьте, что user‑сессия поднимется и без интерактивного логина. Это ключевой момент для systemd --user на VDS.
Мини‑проверка rootless Podman
Заходим под пользователем и запускаем тест:
sudo -iu app
podman run --rm quay.io/podman/hello
Если что‑то не стартует, читайте сообщения: часто не хватает uidmap или возникают проблемы с slirp4netns.
Структура каталогов Quadlet
Подготовим каталог под unit‑описания:
mkdir -p ~/.config/containers/systemd
Любой *.container, *.pod или *.volume файл внутри этого каталога будет обработан Quadlet и превратится в systemd‑юнит после systemctl --user daemon-reload.
Простой контейнер через Quadlet
Создадим демонстрационный сервис на базе образа traefik/whoami. Этот контейнер отвечает на HTTP и показывает сведения о запросе — удобно для проверки сети и портов.
Подготовим директории и переменные окружения:
mkdir -p ~/apps/whoami
cat > ~/apps/whoami/whoami.env << 'EOF'
TZ=UTC
WHOAMI_GREETING=Hello from Quadlet
EOF
Создадим Quadlet‑файл ~/.config/containers/systemd/whoami.container:
[Unit]
Description=Whoami demo (Podman Quadlet rootless)
Wants=network-online.target
After=network-online.target
[Container]
# Образ
Image=traefik/whoami:v1.10
ContainerName=whoami
# Сетевой стек rootless (по умолчанию slirp4netns)
Network=slirp4netns
# Проброс портов: внешний 8080 -> контейнерный 80
# Можно использовать 80:80, если поддерживается rootlessport для низких портов
PublishPort=0.0.0.0:8080:80
# Логи в journald для удобства systemd --user
LogDriver=journald
# Healthcheck через curl внутри контейнера
HealthCmd=curl -fsS http://127.0.0.1:80/ || exit 1
HealthInterval=30s
HealthTimeout=3s
HealthRetries=3
# Переменные окружения
EnvironmentFile=%h/apps/whoami/whoami.env
Environment=WHOAMI_COLOR=green
# Том с данными (пример бинд‑монта)
# Обратите внимание на суффикс :U для корректного маппинга uid/gid при rootless
Volume=/home/app/apps/whoami:/data:U,Z
# Автообновление образа (см. раздел ниже про таймер)
AutoUpdate=registry
[Service]
# Рестарт‑политика на уровне systemd
Restart=always
RestartSec=2s
# Лимиты ресурсов через cgroups v2
MemoryMax=256M
CPUQuota=80%
# Более аккуратное завершение
TimeoutStopSec=30s
[Install]
WantedBy=default.target
Активируем и запускаем:
systemctl --user daemon-reload
systemctl --user enable --now whoami.service
systemctl --user status whoami.service
Проверяем, что сервис слушает порт:
ss -ltnp | grep 8080
curl -fsS http://127.0.0.1:8080/ | head -n 3
Просмотр логов:
journalctl --user -u whoami -f
О портах ниже 1024 в rootless
В rootless‑режиме обычный пользователь не может слушать привилегированные порты (<1024) без специальных возможностей. Podman решает это через вспомогательный бинарник rootlessport с capability cap_net_bind_service. Проверьте:
command -v rootlessport
getcap $(command -v rootlessport)
Если capability присутствует, можете указать PublishPort=80:80 в Quadlet. В противном случае используйте высокие порты (например, 8080) и проксируйте через веб‑сервер на хосте.
Секреты и окружение
Никогда не храните секреты в явном виде внутри .container. Подключайте их через EnvironmentFile=, выставляйте права 0600 и владельца нужного пользователя:
chmod 600 ~/apps/whoami/whoami.env
При обновлении файла окружения просто перезапустите сервис:
systemctl --user restart whoami
Тома и права в rootless
На bind‑монтах в rootless часто «ломаются» владельцы и права внутри контейнера. Для простых кейсов помогает опция :U в Volume=, которая временно ремапит владельцев для контейнера. Если нужна точная контроль над владельцами, используйте опции idmap или заранее подготавливайте права под subuid/subgid.
Если SELinux включён (RHEL‑семейство), добавляйте суффикс
:Zили:zк монтам, как в примере выше.
Автообновление образов
Мы указали AutoUpdate=registry в .container. Теперь включим user‑таймер Podman, чтобы периодически подтягивать новые теги и перезапускать сервис:
systemctl --user enable --now podman-auto-update.timer
systemctl --user list-timers | grep podman-auto-update
Можно проверить в «сухом режиме»:
podman auto-update --dry-run
Если вы переходите с cron на таймеры, посмотрите наше руководство по системным таймерам: миграция с cron на systemd timers.
Продакшен‑совет: закрепляйте минорные версии и тщательно тестируйте, прежде чем включать автообновление на критичных сервисах.
Мультиконтейнер через Pod (опционально)
Quadlet поддерживает .pod, что удобно для группировки нескольких контейнеров с общим сетевым пространством. Допустим, нам нужна связка «приложение + БД». Создадим pod и два контейнера, присоединённых к нему.
Файл pod: ~/.config/containers/systemd/app.pod
[Unit]
Description=Application pod (rootless)
Wants=network-online.target
After=network-online.target
[Pod]
PodName=app
# Пробросим порт на хост, остальное оставим внутрь pod
PublishPort=127.0.0.1:5432:5432
[Service]
Restart=always
RestartSec=2s
[Install]
WantedBy=default.target
База данных: ~/.config/containers/systemd/app-db.container
[Unit]
Description=PostgreSQL in pod
[Container]
Image=postgres:16
ContainerName=app-db
Pod=app
Environment=POSTGRES_PASSWORD=secret
Environment=POSTGRES_USER=app
Environment=POSTGRES_DB=app
Volume=/home/app/apps/pgdata:/var/lib/postgresql/data:U,Z
[Service]
Restart=always
[Install]
WantedBy=default.target
Приложение: ~/.config/containers/systemd/app-web.container
[Unit]
Description=Web app in pod
[Container]
Image=ghcr.io/example/web:1.2.3
ContainerName=app-web
Pod=app
Environment=DATABASE_URL=postgres://app:secret@127.0.0.1:5432/app
[Service]
Restart=always
[Install]
WantedBy=default.target
Активируем всё:
systemctl --user daemon-reload
systemctl --user enable --now app.pod app-db.service app-web.service
systemctl --user status app-web

Подход с pod упрощает межконтейнерные связи и локальную сеть, но помните: rootless‑сеть — пользовательская, и некоторые возможности (например, мультикаст/низкоуровневые сетевые твики) могут быть ограничены.
Лимиты и приоритизация через systemd
Quadlet даёт право указывать системные опции в секции [Service] прямо в .container или .pod. Это открывает аккуратную приоритизацию на VDS: ограничивайте «шумные» сервисы и защищайте критичные.
MemoryMax=— жёсткий лимит памяти.CPUQuota=— доля CPU (например, 50% = пол‑ядра на однопоточном CPU).IOWeight=— относительный вес I/O при конкуренции.TasksMax=— ограничение количества потоков/процессов.
Все эти опции работают и в user‑юнитах, если включены cgroups v2 и поддержка в ядре.
Логи и мониторинг
С LogDriver=journald журналы контейнера попадают в journalctl. Это удобно для оперативного анализа и алертов. Типовой набор команд:
journalctl --user -u whoami -e
journalctl --user -u whoami -f
systemctl --user show whoami | sed -n '1,100p'
Если вы предпочитаете файловые драйверы логирования, переключайтесь на k8s-file и читайте логи из каталога контейнера. Но на практике journald плюс фильтрация по unit — очень удобная пара. Для централизованного сбора можно настроить journal-remote: включаем удалённый журнал systemd.
Типичные проблемы и отладка
- Сервис не стартует после ребута: проверьте
loginctl enable-linger app. Без linger user‑юниты не поднимаются автоматически. - «cannot find newuidmap/newgidmap»: установите пакет
uidmap. - Сеть не работает / порты не слушаются: проверьте
slirp4netns, наличиеrootlessportи его capability. Убедитесь, что вы используете высокие порты, либо включите возможность слушать <1024. - Права на томах: добавьте
:Uк монтам вVolume=или скорректируйте владельцев подsubuid/subgid. - «XDG_RUNTIME_DIR is not set»: используйте вход через
sudo -iu appили полноценный логин, а также убедитесь, что включён linger. Не запускайтеsystemctl --userиз «обезличенных» контекстов без окружения. - Healthcheck падает: пересмотрите
HealthCmd, интервалы и таймауты. Учтите, что команда выполняется внутри контейнера.
Стратегии обновления
Есть два основных подхода:
- Ручное: подтянуть новый образ и перезапустить службу.
podman pull traefik/whoami:v1.10
systemctl --user restart whoami
- Автоматическое:
AutoUpdate=registryв.containerи активныйpodman-auto-update.timer. Плюс стратегия тэгов (например,1.10вместоlatest).
Для нулевого простоя используйте два контейнера и внешний балансировщик или прокси с плавным переключением трафика. В рамках одной user‑службы systemd обеспечит быстрый рестарт, но не «бесшовный».
Безопасность rootless
- Избегайте
--privilegedи избыточных capability. - Открывайте наружу только необходимые порты. Для общих портов используйте обратный прокси на хосте.
- Храните секреты в
EnvironmentFileс правами 0600, не коммитьте их в репозитории. - Ограничивайте ресурсы через systemd, чтобы «шумный» контейнер не мешал соседям на VDS.
Сравнение с Docker Compose на одиночном сервере
- Плюсы Quadlet: плотная интеграция с systemd, ровные логи, нативные рестарты/лимиты, отсутствие внешнего демона, rootless «из коробки».
- Минусы: нет привычного YAML‑сценария с зависимостями и переменными (хотя
.podчастично это закрывает). Для сложной оркестрации лучше Kubernetes/nomad.
Для одиночного VDS или набора простых сервисов Quadlet часто оказывается проще и надёжнее.
Чек‑лист для продакшена
- Подтверждена поддержка cgroups v2 и актуальных версий Podman/systemd.
- Создан отдельный пользователь, включён
loginctl enable-linger. - Образ закреплён по версии, проверены
AutoUpdateи таймеры. - Логи сходятся в journald, при необходимости — отправляются в централизованный сборщик (journal-remote).
- Ресурсы ограничены (
MemoryMax,CPUQuota), порты открыты минимально. - Бэкапы конфигов:
~/.config/containers/systemdи каталогов с данными приложений.
Удаление и очистка
Корректно останавливаем и вычищаем юнит и данные:
systemctl --user disable --now whoami
rm ~/.config/containers/systemd/whoami.container
systemctl --user daemon-reload
podman ps --all
podman images
# При необходимости удалить образ/контейнер/том
# podman rm -f whoami
# podman rmi traefik/whoami:v1.10
Итоги
Связка Podman + Quadlet даёт удобный путь к управляемым, воспроизводимым и безопасным развертываниям на VDS. Rootless‑режим уменьшает поверхность атаки, а systemd --user с loginctl enable-linger обеспечивает надёжный автозапуск без root. Добавьте к этому healthcheck, journald, автообновления и лимиты ресурсов — и вы получаете аккуратный «мини‑оркестратор» из стандартных компонентов Linux, без лишней магии и постоянных демонов.


