Если вам нужен предсказуемый контейнерный runtime на одном или нескольких VDS без тяжёлой оркестрации, systemd-nspawn — мощный и недооценённый инструмент. Он даёт привычные для Linux админа примитивы: namespaces, cgroup, bind mounts, интеграцию с systemd и управлением через machinectl. В этой статье разберём быстрый старт, изоляцию, сеть, работу с файлами, лимиты ресурсов и практики продакшена, а также честно сравним подход с Kubernetes.
Зачем systemd-nspawn на VDS вместо Docker/Kubernetes
systemd-nspawn — это контейнерный инструмент из набора systemd. Он не стремится быть платформой как Docker или Kubernetes, а скорее даёт лёгкие, прозрачные и «родные» для Linux контейнеры с минимумом магии. На одиночном VDS это часто означает:
- меньше зависимостей и компонентов;
- простые отладка и логирование через
journalctl; - нативные лимиты и иерархии
cgroup; - контролируемая изоляция:
user namespace,mount,network; - управление привычными unit'ами systemd;
- быстрые апдейты с
machinectl import/exportи снапшотами файловых систем.
На одном-двух серверах выгода очевидна: меньше TCO и когнитивной нагрузки по сравнению с Kubernetes. Если же нужен автоскейлинг на десятках узлов, интеграция с сервис-мешем и сложным ingress — тогда Kubernetes выигрывает. Но для большинства веб- и API-сервисов на 1–3 VDS, nspawn — золотая середина.
Быстрый старт: первый контейнер на базе debootstrap
Готовим rootfs
Создадим минимальную Debian/Ubuntu-основу. На хосте установите необходимые пакеты:
apt-get update
apt-get install -y debootstrap systemd-container dbus-user-session
Соберём корневую файловую систему для контейнера web1 в каталоге /var/lib/machines/web1 — это стандартное место, которое понимают machinectl и генераторы unit'ов.
debootstrap stable /var/lib/machines/web1
chroot /var/lib/machines/web1 /bin/bash -lc "apt-get update && apt-get install -y systemd-sysv curl ca-certificates"
chroot /var/lib/machines/web1 /bin/bash -lc "passwd -d root"
Создадим базовую сетевую конфигурацию внутри контейнера и hostname:
echo web1 > /var/lib/machines/web1/etc/hostname
echo "127.0.1.1 web1" >> /var/lib/machines/web1/etc/hosts
Первый запуск контейнера
Минимальный запуск с интерактивной оболочкой:
systemd-nspawn -D /var/lib/machines/web1 -M web1
Для фонового запуска с PID 1 как systemd внутри контейнера используем режим boot:
systemd-nspawn -D /var/lib/machines/web1 -M web1 -b
Теперь контейнер виден как «машина»:
machinectl list
machinectl status web1
journalctl -M web1 -b
Чтобы контейнер стартовал как systemd-юнит, создадим файл /etc/systemd/nspawn/web1.nspawn и включим шаблонный сервис:
systemctl enable systemd-nspawn@web1.service
systemctl start systemd-nspawn@web1.service
systemctl status systemd-nspawn@web1.service
Управление через machinectl
machinectl — главный друг администратора для управления «машинами» systemd (контейнеры, VM). Частые команды:
machinectl shell web1— интерактивная оболочка в контейнере без SSH;machinectl login web1— getty-подобный вход;machinectl poweroff web1илиmachinectl reboot web1;machinectl terminate web1— жёсткая остановка;machinectl import-tar web1.tar web1— импорт образа;machinectl export-tar web1 web1.tar— экспорт для бэкапов или CI.

Изоляция: namespaces, user namespace и capabilities
systemd-nspawn опирается на стандартные Linux namespaces: mount, pid, uts, ipc, net, user. Правильная настройка user namespace — важная часть жёсткой изоляции: UID 0 внутри контейнера не равен UID 0 снаружи.
Главная идея userns: даже если процесс «root» в контейнере, на хосте он отображается в обычного пользователя с непривилегированными правами.
Пример запуска с userns и автоматическим chown содержимого rootfs на поддиапазон:
systemd-nspawn -D /var/lib/machines/web1 -M web1 --private-users=pick --private-users-ownership=chown -b
Выборочно ограничим привилегии (capabilities). По умолчанию nspawn уже урезает набор, но можно сильнее:
systemd-nspawn -D /var/lib/machines/web1 -M web1 -b --drop-capability=all --capability=CAP_NET_BIND_SERVICE
В постоянной конфигурации используйте секцию [Nspawn] в /etc/systemd/nspawn/web1.nspawn:
[Nspawn]
PrivateUsers=pick
PrivateUsersOwnership=chown
DropCapability=all
Capability=CAP_NET_BIND_SERVICE
SystemCallFilter=@system-service
Обратите внимание на SystemCallFilter — наборы системных вызовов ограничивают поверхность атаки. Это не «волшебная пуля», но дополнительный слой защиты.
Файловые системы и bind mounts
Базовый rootfs должен быть как можно компактнее. Данные и логи удобнее хранить на хосте и монтировать в контейнер с помощью bind mounts. Это упрощает бэкапы, ротацию логов и откаты. Для публичных сервисов не забывайте про TLS и актуальные ключи: при необходимости оформите надёжные SSL-сертификаты.
Временные монтирования для ручного запуска:
systemd-nspawn -D /var/lib/machines/web1 -M web1 -b --bind-ro=/etc/ssl/certs:/etc/ssl/certs --bind=/srv/web1/var:/var/lib/myapp --tmpfs=/tmp
Постоянные монтирования через файл /etc/systemd/nspawn/web1.nspawn в секции [Files]:
[Files]
BindReadOnly=/etc/ssl/certs:/etc/ssl/certs
Bind=/srv/web1/var:/var/lib/myapp
TemporaryFileSystem=/tmp
Для «иммутабельного» контейнера можно включить режим только чтение и дать приложению writable-накат на отдельные пути:
[Nspawn]
ReadOnly=yes
[Files]
Bind=/srv/web1/state:/var/lib/myapp
TemporaryFileSystem=/run
TemporaryFileSystem=/var/tmp
Эфемерные контейнеры для тестов можно поднимать с --volatile=state или --volatile=yes — состояние не будет сохраняться между перезапусками.
Сеть: veth, bridge, адресация и проброс портов
Есть два распространённых подхода к сети для контейнеров nspawn:
- Создать veth-пары и мост на хосте, чтобы контейнеры получили адреса из внутренней подсети.
- Использовать изолированную подсеть с маршрутизацией и маскарадингом (NAT) на хосте.
Вариант 1: мост с адресами контейнеров в отдельной подсети
Настроим systemd-networkd на хосте. Создаём мост и включаем IP-проброс:
sysctl -w net.ipv4.ip_forward=1
sysctl -w net.ipv6.conf.all.forwarding=1
Файлы конфигурации сети (на хосте):
/etc/systemd/network/br0.netdev
[NetDev]
Name=br0
Kind=bridge
/etc/systemd/network/br0.network
[Match]
Name=br0
[Network]
Address=10.20.0.1/24
IPForward=yes
/etc/systemd/network/80-container-veth.network
[Match]
Name=vb-*
[Network]
Bridge=br0
IPv6AcceptRA=no
В web1.nspawn привяжем контейнер к мосту:
[Network]
Bridge=br0
MACVLAN=no
При старте контейнер получит сетевой интерфейс вида host: vb-web1 и guest: host0. Внутри контейнера можно настроить статический IP или DHCP от systemd-networkd на хосте (через DHCPServer=yes в br0.network, если требуется).
Далее настраиваем проброс портов наружу (nftables пример):
nft add table inet filter
nft add chain inet filter input { type filter hook input priority 0; policy drop; }
nft add rule inet filter input ct state established,related accept
nft add rule inet filter input iif lo accept
nft add rule inet filter input tcp dport { 22, 80, 443 } accept
nft add table inet nat
nft add chain inet nat prerouting { type nat hook prerouting priority -100; }
nft add chain inet nat postrouting { type nat hook postrouting priority 100; }
# DNAT 80->web1 (10.20.0.10) и 443->web1
nft add rule inet nat prerouting tcp dport 80 dnat to 10.20.0.10:80
nft add rule inet nat prerouting tcp dport 443 dnat to 10.20.0.10:443
# Маскарадинг из подсети контейнеров наружу
nft add rule inet nat postrouting ip saddr 10.20.0.0/24 oif "eth0" masquerade
Вариант 2: изолированная подсеть с маршрутизацией
Если мост не нужен, можно использовать --network-veth, хост получает ve-*, контейнер — host0, и назначить адресацию вручную. Маскарадинг и DNAT настраиваются аналогично предыдущему примеру.
Временный запуск:
systemd-nspawn -D /var/lib/machines/web1 -M web1 -b --network-veth --private-network
Постоянная конфигурация:
[Network]
VirtualEthernet=yes
Private=yes
Помните: nspawn не делает автоматического NAT/порт-маппинга, как Docker. Это осознанный дизайн: сеть под вашим полным контролем через systemd-networkd и брандмауэр хоста.
Ресурсы и cgroup: CPU, память, I/O
Каждый контейнер — это scope/служба в иерархии cgroup. Удобнее всего задавать лимиты через unit systemd-nspawn@name.service или по месту через systemctl set-property:
# Лимиты для конкретного контейнера
systemctl set-property systemd-nspawn@web1.service MemoryMax=1G
systemctl set-property systemd-nspawn@web1.service CPUQuota=200%
systemctl set-property systemd-nspawn@web1.service IOReadBandwidthMax=/dev/vda 20M
systemctl set-property systemd-nspawn@web1.service IOWriteBandwidthMax=/dev/vda 10M
Лимиты можно задать и в drop-in для unit'а:
/etc/systemd/system/systemd-nspawn@web1.service.d/limits.conf
[Service]
MemoryMax=1G
CPUWeight=200
IOReadBandwidthMax=/dev/vda 20M
IOWriteBandwidthMax=/dev/vda 10M
Проверить, как применились ограничения:
systemctl show systemd-nspawn@web1.service | grep -E "MemoryMax|CPU|IO"
systemd-cgls
systemd-cgtop
Загрузка приложений: юниты внутри контейнера
Внутри контейнера у вас обычный systemd. Разверните приложение, добавьте unit'ы и таймеры, настройте логирование в journald и, при необходимости, форвардинг логов на хост.
# внутри контейнера
cat > /etc/systemd/system/myapp.service <<'EOF'
[Unit]
Description=My App
After=network-online.target
Wants=network-online.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/lib/myapp
ExecStart=/usr/local/bin/myapp --listen 0.0.0.0:8080
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
EOF
systemctl enable --now myapp.service
systemctl status myapp.service
Логи удобно читать с хоста:
journalctl -M web1 -u myapp.service -f
Образы, апдейты и откаты
Есть несколько стратегий доставки обновлений в nspawn-контейнеры:
- Импорт/экспорт tar-образов при помощи
machinectl import-tarиexport-tar. - Сборка образов в CI и публикация как tar; на VDS — атомарный импорт и перезапуск контейнера.
- Использование файловой системы со снапшотами (btrfs, LVM thin): перед апдейтом — снапшот, при проблемах — откат за секунды.
Пример потока:
# на CI собираем web1.tar
# на VDS подменяем rootfs атомарно
systemctl stop systemd-nspawn@web1.service
machinectl import-tar web1.tar web1
systemctl start systemd-nspawn@web1.service
Для тестов и одноразовых запусков пригодится эфемерный режим:
systemd-nspawn -D /var/lib/machines/web1 -M web1 --volatile=state -b

Безопасность: check-list для изоляции
PrivateUsers=pickиPrivateUsersOwnership=chownв.nspawn.DropCapability=allи выборочныйCapability=только то, что нужно.SystemCallFilter=наборы@system-serviceили точечная политика.ReadOnly=yesи явныеBindдля состояния.- Собственный
tmpfsдля/tmpи/run. - Разделение данных: один контейнер — один сервис/роль.
- Чёткие
nftablesполитики; вход — только нужные порты. MemoryMax,CPUQuota, I/O лимиты; общий бюджет вmachine.slice.- Регулярные обновления образов и базового дистрибутива внутри контейнера.
Отладка и диагностика
- Смотрим журнал контейнера:
journalctl -M web1 -b, сервис:-у myapp. - Сеть внутри:
machinectl shell web1 /bin/ip a; снаружи:ip a,nft list ruleset. - Проверяем cgroup:
systemd-cgtop,systemd-cgls. - Нагрузочные прогоны:
ab,wrkснаружи, наблюдаем лимиты. - Видимость процессов:
psвнутри и снаружи, сверяем namespaces.
Продакшен на одном-двух VDS без Kubernetes
Реалистичный паттерн для малого и среднего проекта:
- Один VDS:
web(reverse-proxy),app(приложение),db(БД) — всё в отдельныхnspawn-контейнерах. Разводим сеть бриджем, лимитируем ресурсы cgroup. - Два VDS: активный и пассивный. На пассивном — горячий standby контейнеров. Репликация БД, резервный reverse-proxy с VRRP (keepalived) на хосте. Переключение через VIP.
- CI/CD: сборка tar-образов,
machinectl import-tarи пошаговый перезапуск контейнеров с health-check'ами.
Плюсы такого стека: простота, минимум «оркестрации», прозрачная сеть и логи. Минусы: нет автоскейлинга, ручное управление размещением и зависимостями. Но на 1–3 серверах это часто не требуется.
Сравнение с Kubernetes: где nspawn выигрывает
- Простота: один runtime, нативный systemd, нет абстракций pod/service/deployment.
- Логи и отладка:
journalctlи привычные инструменты Linux. - Контроль сети: явные правила, предсказуемый трафик, меньше уровней.
- Цена владения: меньше компонентов = ниже расходы на поддержку.
Где Kubernetes сильнее:
- Автоскейлинг, self-healing на уровне кластера.
- Сетевые политики, сервис-меш, динамическая маршрутизация.
- Сторонние контроллеры, CRD, богатая экосистема.
Правильный критерий выбора — сложность домена и реальная нагрузка. Если у вас один VDS или пара машин и сервисы укладываются в один узел — systemd-nspawn даёт максимум пользы с минимальными издержками.
Готовый шаблон .nspawn
/etc/systemd/nspawn/web1.nspawn
[Exec]
Boot=yes
[Files]
BindReadOnly=/etc/ssl/certs:/etc/ssl/certs
Bind=/srv/web1/var:/var/lib/myapp
TemporaryFileSystem=/tmp
TemporaryFileSystem=/run
[Network]
Bridge=br0
Private=yes
[Nspawn]
PrivateUsers=pick
PrivateUsersOwnership=chown
ReadOnly=yes
DropCapability=all
Capability=CAP_NET_BIND_SERVICE
SystemCallFilter=@system-service
И напоминание о ресурсах на уровне unit'а:
/etc/systemd/system/systemd-nspawn@web1.service.d/limits.conf
[Service]
MemoryMax=1G
CPUQuota=200%
IOReadBandwidthMax=/dev/vda 20M
IOWriteBandwidthMax=/dev/vda 10M
Практические мелочи, которые экономят часы
- DNS: если на хосте работает
systemd-resolved, убедитесь, что контейнер получает валидный/etc/resolv.conf. При необходимости пробросьтеBindReadOnly=/run/systemd/resolve/stub-resolv.conf:/etc/resolv.conf. - Часы и таймзона: храните UTC на хосте, внутри контейнера синхронизируйте
/etc/localtimeчерез bind-ro. - SSH доступ: для интерактива используйте
machinectl shell. Открывать SSH внутрь контейнера необязательно. - Логи приложения: пишите в stdout/stderr и читайте через
journalctl -M. Это проще ротации файлов. - Снапшоты: используйте btrfs/LVMthin для
/var/lib/machines— бэкапы и откаты станут мгновенными. - Соглашения по именованию: контейнер = роль (web, app, db), а не проект-версия. Версии — в тегах образов tar.
Заключение
systemd-nspawn — отличный выбор для админов, которым нужны предсказуемые и лёгкие контейнеры на VDS без развёртывания полной оркестрации. Он даёт все ключевые примитивы: изоляцию через namespaces и user namespace, контроль ресурсов через cgroup, гибкую сеть с systemd-networkd, простые bind mounts и мощное управление через machinectl. Для малого и среднего масштаба это быстрая, прозрачная и безопасная база для продакшена.
Начните с одного контейнера, зафиксируйте шаблон .nspawn и unit-лимиты, добавьте CI для сборки tar-образов. Через пару итераций вы получите воспроизводимый и понятный стек, которым удобно управлять и который не «перегружает» ваш VDS.


