Запуск изолированных Linux-процессов без Docker

Запуск изолированных Linux-процессов без Docker


Контейнеры кажутся чем-то загадочным - пока не соберёшь один сам. В этом пошаговом руководстве мы соберём крошечный, полностью изолированный Linux-контейнер, используя только инструменты, которые уже встроены в ядро. Никакого Docker, Podman или других контейнерных рантаймов.

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

Термин control group (cgroup) впервые появился в ядре Linux ещё в 2007 году и изначально назывался process containers.
Preview image

Подготовка собственного корня файловой системы

1.Создаём структуру каталогов для эксперимента:

mkdir -p /tmp/mybox/{lower,upper,work,merged}

2.Скачиваем какой-нибудь небольшой root-файловый архив - подойдёт Alpine, BusyBox или что-то похожее - и распаковываем его в lower:

3.Наслаиваем всё через overlayFS, чтобы все изменения записывались в upper, а исходные файлы в lower оставались нетронутыми:

mount -t overlay overlay \
  -o lowerdir=/tmp/mybox/lower,upperdir=/tmp/mybox/upper,workdir=/tmp/mybox/work \
  /tmp/mybox/merged

Теперь /tmp/mybox/merged - это, по сути, корневая директория нашего будущего контейнера.

Так как overlayFS хранит только изменённые файлы, большинство контейнерных образов меньше по размеру, чем музыкальный альбом, который вы стримили сегодня утром.

Настройка ограничений ресурсов через cgroups

Создаём новый срез cgroup и включаем в нём управление памятью и CPU:

mkdir -p /sys/fs/cgroup/mybox.slice/one
echo "+memory +cpu" > /sys/fs/cgroup/mybox.slice/cgroup.subtree_control

Задаём лимиты: не больше 15 % одного ядра CPU и максимум 512 МиБ оперативной памяти (swap - отключён):

echo "15000 100000" > /sys/fs/cgroup/mybox.slice/one/cpu.max
echo "512M"         > /sys/fs/cgroup/mybox.slice/one/memory.max
echo "0"            > /sys/fs/cgroup/mybox.slice/one/memory.swap.max
Почему именно "15000 100000"? Первое число - это допустимое время выполнения в микросекундах, второе - весь период. То есть процесс может работать 15 000 мкс из каждых 100 000 мкс - ровно 15 %.

Вход в новые пространства имён

Переключаемся на root-пользователя, добавляем текущий процесс в нужную cgroup и запускаем shell в новых пространствах имён:

sudo -i
echo $$ > /sys/fs/cgroup/mybox.slice/one/cgroup.procs

unshare \
  --uts --pid --mount --mount-proc \
  --net --ipc --cgroup \
  --fork /bin/bash

Теперь у вас:

  • UTS namespace - можно задать любое имя хоста;
  • PID namespace - первый запущенный процесс получает PID 1;
  • Mount namespace - точки монтирования изолированы от хоста.

Попробуйте сменить hostname:

hostname mycontainer2025   # видно только внутри контейнера

Безопасная смена корня с помощью pivot_root

Переходим в директорию merged, переключаем корневую файловую систему на неё и отцепляем старый корень:

cd /tmp/mybox/merged
mount --make-rprivate /
mkdir old_root
pivot_root . old_root
umount -l /old_root
rmdir old_root
pivot_root защищает от нескольких классических способов выхода из chroot, так как делает старую корневую файловую систему недоступной.

Монтирование основных виртуальных файловых систем

Без /proc, /sys и /dev многие утилиты просто не работают. Добавим минимальный набор:

mknod -m 666 dev/null c 1 3
mknod -m 666 dev/zero c 1 5
mknod -m 666 dev/tty  c 5 0

Создадим нужные каталоги и смонтируем виртуальные ФС

mkdir -p dev/{pts,shm}
mount -t devpts devpts dev/pts
mount -t tmpfs  tmpfs  dev/shm
mount -t sysfs  sysfs  sys
mount -t tmpfs  tmpfs  run
mount -t proc   proc   proc

Замена shell на ваше приложение

Для демонстрации я запускаю минимальный shell, который уже есть в rootfs:

exec /bin/busybox sh

С этого момента вы находитесь внутри контейнера, который собрали сами.

Проверка ограничений

Тест CPU

Внутри контейнера выполните:

while :; do :; done

А на хосте - посмотрите потребление CPU:

top -Hp $(pgrep -f busybox)

Должно быть видно, что процесс использует около 15 % CPU.

Тест памяти

Всё ещё внутри контейнера запустите:

python - <<'PY'
b = bytearray(700 * 1024 * 1024)  # allocate 700 MiB
PY

Когда процесс превысит лимит в 512 МиБ, контроллер памяти ядра завершит его и выведет сообщение Killed.

Если бы swap был включён, ядро могло бы сначала сбросить страницы на диск. Но с отключённым swap процесс убивается быстрее, что удобно для тестов.

Очистка

Выходим из контейнера и отмонтируем overlay:

exit            # leave the inner shell
umount /tmp/mybox/merged

Теперь временные директории можно спокойно удалить.

Заключение

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

  • overlayFS - для слоя с возможностью записи,
  • cgroups - чтобы задать лимиты на ресурсы,
  • namespaces - для изоляции окружения.

Инструменты вроде Docker просто оборачивают эти базовые механизмы - но сами механизмы давно были в ядре. Не стесняйтесь продолжить эксперимент: можно закрепить контейнер за конкретными ядрами CPU через cpuset или ограничить опасные системные вызовы с помощью seccomp.

Report Page