OSEN-НИЙ SAAALEСкидка 50% на виртуальный хостинг и VDS
до 30.11.2025 Подробнее
Выберите продукт

systemd-nspawn на VDS: лёгкие контейнеры, изоляция и сеть без Kubernetes

Как запустить и подружить systemd-nspawn с вашим VDS: развертывание контейнеров, изоляция, bind mounts, сеть и cgroup-лимиты, управление через machinectl. Разбираем, где nspawn проще и выгоднее Kubernetes, и даём рецепты для продакшена на одном‑двух серверах.
systemd-nspawn на VDS: лёгкие контейнеры, изоляция и сеть без Kubernetes

Если вам нужен предсказуемый контейнерный 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.

Управление контейнерами systemd-nspawn через machinectl на терминале

Изоляция: 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:

  1. Создать veth-пары и мост на хосте, чтобы контейнеры получили адреса из внутренней подсети.
  2. Использовать изолированную подсеть с маршрутизацией и маскарадингом (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
FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

Загрузка приложений: юниты внутри контейнера

Внутри контейнера у вас обычный 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

Схема сети nspawn: мост br0 и veth‑пары между хостом и контейнерами

Безопасность: 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.
  • Регулярные обновления образов и базового дистрибутива внутри контейнера.
FastFox SSL
Надежные SSL-сертификаты
Мы предлагаем широкий спектр SSL-сертификатов от GlobalSign по самым низким ценам. Поможем с покупкой и установкой SSL бесплатно!

Отладка и диагностика

  • Смотрим журнал контейнера: 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

Реалистичный паттерн для малого и среднего проекта:

  1. Один VDS: web (reverse-proxy), app (приложение), db (БД) — всё в отдельных nspawn-контейнерах. Разводим сеть бриджем, лимитируем ресурсы cgroup.
  2. Два VDS: активный и пассивный. На пассивном — горячий standby контейнеров. Репликация БД, резервный reverse-proxy с VRRP (keepalived) на хосте. Переключение через VIP.
  3. 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.

Поделиться статьей

Вам будет интересно

Grafana Agent Flow на VDS: единый агент для metrics, logs и traces (Prometheus, Loki, Tempo, OTLP) OpenAI Статья написана AI (GPT 5)

Grafana Agent Flow на VDS: единый агент для metrics, logs и traces (Prometheus, Loki, Tempo, OTLP)

Grafana Agent в режиме Flow — лёгкий агент на одном VDS для метрик, логов и трейсов с отправкой в Prometheus/VictoriaMetrics, Loki ...
Node.js keepalive и http.Agent: практическая настройка с Nginx и upstream-пулами OpenAI Статья написана AI (GPT 5)

Node.js keepalive и http.Agent: практическая настройка с Nginx и upstream-пулами

Разбираем пул http.Agent в Node.js и практику keepalive: какие параметры важны (maxSockets, freeSocketTimeout, socketActiveTTL), к ...
PostgreSQL: HOT UPDATE, fillfactor и борьба с bloat без боли OpenAI Статья написана AI (GPT 5)

PostgreSQL: HOT UPDATE, fillfactor и борьба с bloat без боли

HOT UPDATE экономит обновления индексов и сдерживает bloat, но срабатывает не всегда. Разбираем, как работает HOT, как выбрать fil ...