Всем привет!

Часто ли вам приходится основательно заморачиваться ради задачи, результат которой будет использован буквально пару раз? Признаюсь честно: мне — постоянно. И вот как раз один из таких случаев.

Я преподаю на IT-курсе, и два последних занятия у нас были посвящены деплою и настройке CI/CD. Поскольку главная цель нашего курса — это суровая практика, мне совершенно не хотелось ограничиваться сухой теорией или, как делают многие, показывать весь процесс в «тепличных» условиях локальной виртуальной машины. Моей задачей было дать студентам живой боевой опыт работы с настоящим VPS и реальным процессом развертывания приложений. Чтобы всё было по-взрослому, я даже выделил каждому из них персональный поддомен!

Собственно, в этой статье я расскажу, как именно я организовал полностью изолированные рабочие пространства для четырёх студентов на одном сервере с помощью системы контейнеризации Incus. Мы разберем, с какими подводными камнями я столкнулся в процессе и как вы можете повторить весь этот сетап у себя, не наступая на мои грабли.

Если вам нравятся подобные практические материалы, подписывайтесь на мой Telegram-канал «Код на салфетке»!


Первый блин комом

Озадачивать студентов покупкой собственных VPS — такое себе решение. Просить серверы у руководства тоже не вариант: согласования заняли бы время, которого до занятия оставалось в обрез. К тому же, мне было дико интересно поднять подобную инфраструктуру самому, поэтому я пошёл искать готовые инструменты. Первое, на что пал взгляд — связка LXC/LXD, и я с энтузиазмом принялся за настройку.

В качестве железа я взял «бесплатную картошку» от Сбера (думаю, вы поняли, о каком облачном сервере речь ?). Это был мой единственный сервер на Ubuntu, который я обычно использовал как эдакий тестовый полигон для экспериментов. Зато на нём было аж 4 ГБ оперативки, что в нашем случае очень сыграло на руку.

Что вообще такое LXC, я подробно расскажу далее в тексте, когда дойдём до практической части. Если вкратце: LXC — это сама базовая технология системных контейнеров, а LXD — это мощная надстройка (менеджер) над ней, которая предоставляет удобный интерфейс управления. Разрабатывалась она компанией Canonical (создателями Ubuntu). Именно поэтому сервер на Ubuntu подошёл идеально: нужная инфраструктура поддерживается там буквально «из коробки».

В общем, вооружившись официальной докой, статьями и ИИшкой, я пошёл инициализировать пространства.

Первая проблема: доступ только по ключу

Итак, после успешной инициализации контейнеров для студентов и одного тестового, я радостно попытался подключиться к тестовому по SSH — и получил от ворот поворот.

Дело в том, что официальный образ Ubuntu 22.04, из которого собирается контейнер, из соображений безопасности по умолчанию разрешает подключение только по SSH-ключу. А мне нужно было ровно обратное: чтобы студенты сначала зашли в систему по паролю, освоились, и уже внутри сами сгенерировали и добавили свои ключи для безопасного безпарольного входа.

Чтобы решить проблему и пустить студентов, нужно зайти в каждый контейнер и принудительно разрешить вход по паролю. Делать это руками в консольном редакторе для каждого студента — сомнительное удовольствие и трата времени. Но прелесть системных контейнеров в том, что мы можем выполнить команду внутри них прямо с нашей хост-машины. Делается это вот так:

lxc exec test -- bash -c "sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/g' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/*.conf 2>/dev/null; systemctl restart ssh"

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

  • lxc exec test — команда менеджеру контейнеров: «выполни действие внутри контейнера с именем test». Естественно, вместо test нужно подставить имя нужного вам контейнера.

  • -- — двойное тире говорит системе: «параметры для утилиты lxc закончились, дальше идёт сама команда, которую нужно пробросить внутрь».

  • bash -c "..." — запускаем внутри контейнера командную оболочку Bash и передаем ей строку в кавычках для выполнения.

  • sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/g' — используем потоковый текстовый редактор sed. Флаг -i означает, что мы редактируем файл «на месте» (сразу сохраняем изменения). Дальше идет правило: мы ищем строку, запрещающую вход по паролю (no), и меняем её на разрешающую (yes).

  • /etc/ssh/sshd_config /etc/ssh/sshd_config.d/*.conf — это пути к конфигурационным файлам SSH-сервера, в которых sed будет искать и менять текст.

  • 2>/dev/null — небольшой админский трюк. Если дополнительных файлов конфигурации (по маске *.conf) нет, терминал может выдать некритичную ошибку. Эта конструкция просто отправляет весь подобный «мусор» в небытие, чтобы не засорять нам экран.

  • ; systemctl restart ssh — точка с запятой позволяет выполнить следующую команду сразу после завершения предыдущей. Здесь мы перезапускаем службу SSH, чтобы наши новые настройки вступили в силу.

Одной этой строкой мы меняем конфиг и перезапускаем службу. Бинго!

Вторая проблема: коварный IPv6

Ура, мы внутри контейнера! Казалось бы, можно начинать полноценную работу. Первое, что делает любой уважающий себя линуксоид в свежей системе (и я не исключение), — запускает apt update для обновления списков пакетов. Но и тут меня ждал сюрприз: процесс просто завис.

Оказалось, что менеджер пакетов apt со всей уверенностью пытался достучаться до серверов Ubuntu по протоколу IPv6. Проблема заключалась в том, что внутри сети контейнеров адреса IPv6 выдавались, а вот на самом хостовом сервере (моей «картошке») внешнего маршрута для IPv6 не было. В итоге контейнер пытался отправить пакеты в интернет по современному протоколу, отправлял их в пустоту и уходил в тайм-аут.

Но, как выяснилось, это была лишь половина беды. Когда я начал копать глубже, всплыл ещё один нюанс, который регулярно пьёт кровь новичкам: на моём хостовом сервере уже был установлен Docker. А Докер из соображений безопасности жёстко переписывает правила системного файрвола (iptables) и по умолчанию отбрасывает чужой транзитный трафик (цепочка FORWARD). Из-за этого изолированные пространства лишаются доступа в интернет даже по привычному IPv4.

Решением стало полное отключение раздачи IPv6 для внутренней сети контейнеров и добавление разрешающих правил в iptables, чтобы файрвол хоста начал беспрепятственно пропускать трафик нашего сетевого моста lxdbr0.

Применяем настройки:

# Отключаем IPv6 для сетевого моста
lxc network set lxdbr0 ipv6.address none

# Разрешаем прохождение трафика от моста (исходящий) и к мосту (входящий) через файрвол хоста
sudo iptables -I FORWARD -i lxdbr0 -j ACCEPT
sudo iptables -I FORWARD -o lxdbr0 -j ACCEPT

# Перезагружаем контейнер, чтобы он переподключился к сети с новыми параметрами
lxc restart test

После этих манипуляций сеть внутри контейнера окончательно ожила, файрвол перестал резать пакеты, и обновления успешно скачались!

Третья проблема: файловая система, Docker и грабли с ZFS

На этом этапе меня отбросило к ошибке, допущенной ещё в самом начале — при инициализации нашего сервера контейнеров.

Дело в том, что при первичной настройке (команда init) система задает ряд вопросов, один из которых касается выбора базовой файловой системы. По умолчанию обычно предлагается ZFS. Спору нет, выбор отличный, надёжный и современный... но ровно до тех пор, пока вы не решите запустить внутри такого пространства Docker.

Почему так происходит? Docker по умолчанию использует для управления слоями образов драйвер overlay2. Проблема в том, что этот драйвер «из коробки» категорически отказывается работать поверх ZFS, особенно когда мы находимся внутри изолированного системного контейнера. Docker просто не сможет примонтировать свои слои и будет сыпать ошибками.

Правильное решение (на будущее): Идеальным вариантом было бы предвидеть это заранее и ещё на этапе инициализации Incus/LXD выбрать более дружелюбную к overlay2 файловую систему. Например, Btrfs или классический dir (который будет использовать базовую ФС хоста, вроде ext4).

Рабочее решение (когда переделывать поздно): Но что делать, если пространства уже нарезаны, студенты рвутся в бой, а пересобирать всё с нуля нет времени? Переходим к плану «Б». Мы можем заставить сам Docker внутри контейнера использовать другой драйвер — fuse-overlayfs. Он работает в пространстве пользователя (FUSE) и элегантно обходит ограничения ядра и файловой системы.

Для этого заходим в контейнер студента и выполняем следующие команды:

# Устанавливаем драйвер fuse-overlayfs
sudo apt install -y fuse-overlayfs

# Говорим Докеру использовать этот драйвер по умолчанию.
# Для этого записываем настройку в конфигурационный файл daemon.json
echo '{"storage-driver": "fuse-overlayfs"}' | sudo tee /etc/docker/daemon.json

# Перезагружаем службу Docker, чтобы изменения вступили в силу
sudo systemctl restart docker

Четвёртая проблема: размер диска и седина преподавателя

Последняя проблема вылезла прямо во время занятия, отчего я чуть не поседел во второй раз!

Дело в том, что при создании пространств я не настроил размер выделяемого диска для контейнеров, а на этапе тестов просто не обратил на это внимания. В итоге по умолчанию каждому студенту досталось крошечное корневое хранилище объемом всего около 1.6 ГБ.

Для понимания: чистая система, установленный сверху Docker и парочка скачанных базовых образов для деплоя съедают такой объем моментально. Место закончилось у всех практически на первых же минутах практики, и консоли студентов начали радостно сыпать ошибками в духе «No space left on device».

Готового заклинания для починки «на лету» в этой части не будет. Буду честен: в состоянии легкой паники мы перепробовали такое количество команд, ресайзов (изменений размера) пула и перезагрузок, что я до сих пор не уверен, что именно сработало, или же всё это сработало в связке.

Зато я вынес из этого главный урок, которым поделюсь с вами: настраивать дисковые квоты нужно строго до того, как вы пустили пользователей в контейнер. Поэтому дальше в практической части статьи мы не будем заниматься тушением пожаров, а сразу сделаем всё правильно и установим нужный размер диска при создании.


Что такое LXC, LXD и почему Incus

На самом деле, запутаться во всех этих аббревиатурах проще простого (что я изначально с успехом и сделал, хе-хе), но не всё так страшно.

Давайте сразу проясним главное: это не три разрозненных инструмента, а эволюционирующие части одной большой экосистемы.

LXC (Linux Containers): Фундамент и «Двигатель»

LXC — это базовая, довольно низкоуровневая технология, работающая непосредственно на уровне операционной системы.

Что это: По сути, это набор утилит, библиотек и шаблонов. Он берёт встроенные механизмы ядра Linux — namespaces (которые изолируют процессы друг от друга) и cgroups (которые ограничивают потребление ресурсов, вроде памяти и CPU) — и заставляет их работать вместе так, чтобы вы могли запустить полностью изолированную ОС.

Главная фишка: LXC создает именно системные контейнеры. В отличие от привычного всем Docker (который упаковывает и запускает одно конкретное приложение), LXC запускает полноценную операционную систему. В ней есть свой собственный процесс инициализации (например, systemd), свои системные службы, своя сеть и свои пользователи. Внутри всё это ощущается и ведет себя как самая классическая виртуальная машина, но при этом работает напрямую на ядре хоста — без «тяжелых» накладных расходов, присущих гипервизорам.

LXD: Комфортная надстройка (Менеджер)

По мере того как системные контейнеры становились всё популярнее, стало очевидно: управлять «голым» LXC руками слишком неудобно. Тогда компания Canonical (создатели Ubuntu) разработала LXD.

Что это: Это фоновый процесс (демон) и мощный REST API, который работает поверх LXC. Важный момент: сам по себе LXD не создает контейнеры. Он лишь отдаёт команды LXC сделать это, но зато полностью берет на себя всю сложную логику управления, распределения ресурсов и автоматизации.     

Что именно он добавил:

  • Удобный интерфейс командной строки (lxc ...). > Забавный факт: да, консольная команда называется lxc, хотя управляет она сервером LXD. Это историческая путаница, с которой просто нужно смириться!

  • Кластеризацию и живую миграцию: Возможность объединять серверы в кластеры и переносить работающие контейнеры между физическими машинами на лету, без остановки сервисов.

  • Продвинутую работу с инфраструктурой: Поддержку современных файловых систем (ZFS, Btrfs, Ceph) и сложных программно-определяемых сетей (OVS, OVN).

  • Поддержку виртуальных машин: Позже LXD научился управлять не только легковесными контейнерами LXC, но и полноценными виртуальными машинами (через QEMU), став эдаким универсальным комбайном.

Incus: Свободный форк и взгляд в будущее

Долгое время LXD развивался при активном участии Canonical, но оставался под эгидой независимого сообщества Linux Containers (которое, собственно, и создало базовый LXC). Однако летом 2023 года Canonical решила полностью забрать проект LXD под свой корпоративный контроль и убрала его с площадок открытого сообщества.

Как он появился: Часть разработчиков и само сообщество не согласились с такой политикой. В ответ они создали независимый форк — Incus.

Суть: Технически Incus — это полная копия LXD на момент их разделения, которая теперь идет совершенно самостоятельным путем.

В чём разница сейчас:

  • Нет привязки к Canonical: Incus полностью очищен от специфических экосистемных технологий компании. В частности, он больше не требует обязательного использования менеджера пакетов snapd (который многие не любят за тяжеловесность). Это делает Incus идеальным, легким решением для чистых установок на Debian, Arch, Alpine и других дистрибутивах.

  • Свобода развития: Разработчики Incus активно вычищают старый код, делают инструмент легче и быстрее, а новые функции добавляют, прислушиваясь к реальным потребностям сообщества, а не к планам продаж корпорации.

Почему именно Incus?

Все мои основные серверы работают исключительно на Debian (не считая той самой «картошки», которая используется только для тестов и экспериментов). К тому же, буду откровенен: я крайне негативно отношусь к Ubuntu и политике Canonical.

Они раз за разом стреляют себе в ногу, нарушая доверие пользователей, и я отказываюсь с этим мириться. Поэтому мой осознанный выбор — это открытый форк Incus. Он идеально встает на Debian-сервер, не засоряя его лишним «убунтовским мусором».

Наверняка найдутся те, кто со мной не согласится. Понимаю, звучит как вкусовщина и чистая необъективность, но статью-то пишу я, а значит, и мысли выражаю свои!


Установка и инициализация Incus

С лирикой покончено, пора переходить к тому, что наверняка вам интереснее всего — к суровой практике.

Поскольку в следующем потоке курса у меня будет уже не 4, а 8 студентов, тестовая «картошка» с такими нагрузками явно не справится. Поэтому для новых контейнеров я выделил другой, более серьезный сервер со следующими характеристиками:

  • ОС: Debian 13

  • ЦПУ: 4 ядра

  • ОЗУ: 8 ГБ

  • Диск: 80 ГБ

Этого ресурсов должно с головой хватить для комфортной работы восьми изолированных пространств.

Установка

На момент написания статьи самых свежих версий Incus в стандартных репозиториях Debian может не быть. Поэтому мы воспользуемся репозиторием Zabbly.

Небольшая справка: Zabbly — это репозиторий, который поддерживает лично Стефан Грабер (Stéphane Graber), создатель оригинального LXD и нынешний лидер проекта Incus. Так что источник максимально официальный и надежный.

Чтобы установить Incus, достаточно пошагово выполнить в терминале следующую последовательность команд:

# 1. Обновляем списки пакетов и ставим утилиты для скачивания (curl) и работы с ключами (gpg)
sudo apt update && sudo apt install -y curl gpg

# 2. Скачиваем официальный GPG-ключ репозитория Zabbly, чтобы система доверяла пакетам
curl -fsSL https://pkgs.zabbly.com/key.asc | sudo tee /etc/apt/keyrings/zabbly.asc

# 3. Добавляем сам репозиторий Zabbly в нашу систему (используется современный формат DEB822)
sh -c 'cat <<EOF > /etc/apt/sources.list.d/zabbly-incus-stable.sources
Enabled: yes
Types: deb
URIs: https://pkgs.zabbly.com/incus/stable
Suites: $(. /etc/os-release && echo ${VERSION_CODENAME})
Components: main
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/zabbly.asc
EOF'

# 4. Снова обновляем списки пакетов (теперь система увидит Incus) и запускаем установку
sudo apt update
sudo apt install -y incus

Учтите важный момент! Incus — инструмент мощный, поэтому при установке он потянет за собой довольно много системных зависимостей и займет примерно 1.3 ГБ дискового пространства на хосте. Убедитесь, что свободное место на сервере учитывает такие изменения.

Начнется процесс скачивания и распаковки. Дожидаемся окончания установки и, если терминал не выдал никаких красных ошибок, смело переходим к инициализации нашего сервера контейнеров.

Инициализация Incus

Для запуска базовой настройки выполняем следующую команду:

sudo incus admin init

Команда запустит интерактивный мастер, который будет поочередно задавать вопросы о будущей конфигурации сервера. Большинство из них в нашем случае не критичны, и можно просто нажимать Enter, соглашаясь со значениями по умолчанию. Но есть два важнейших пункта, которые ни в коем случае нельзя пропустить!

Вот как выглядит весь процесс (я оставил ответы пустыми там, где просто нажимал Enter, и вписал значения там, где это необходимо):

Would you like to use clustering? (yes/no) [default=no]:
Do you want to configure a new storage pool? (yes/no) [default=yes]:
Name of the new storage pool [default=default]:                        
Name of the storage backend to use (btrfs, dir, lvm, zfs) [default=zfs]: dir   
Where should this storage pool store its data? [default=/var/lib/incus/storage-pools/default]:                        
Would you like to create a new local network bridge? (yes/no) [default=yes]:                                                  
What should the new bridge be called? [default=incusbr0]:              
What IPv4 address should be used? (CIDR subnet notation, “auto” or “none”) [default=auto]:
What IPv6 address should be used? (CIDR subnet notation, “auto” or “none”) [default=auto]: none
Would you like the server to be available over the network? (yes/no) [default=no]:                                                  
Would you like stale cached images to be updated automatically? (yes/no) [default=yes]:                                                
Would you like a YAML "init" preseed to be printed? (yes/no) [default=no]:

Давайте подробнее разберем те самые два вопроса, где мы меняем настройки. Именно здесь мы страхуем себя от граблей, о которых я рассказывал в начале статьи:

  • Name of the storage backend to use — здесь система спрашивает, какую файловую систему использовать. Скорее всего, по умолчанию она предложит zfs. Обязательно впишите руками dir (или btrfs, если доступен). Этим мы сразу подготавливаем совместимую среду для Docker, чтобы потом не плясать с бубном вокруг драйверов fuse-overlayfs.

  • What IPv6 address should be used? — здесь по умолчанию стоит auto, но мы жестко прописываем none. Этим мы превентивно отключаем раздачу IPv6 внутри локальной сети контейнеров, чтобы наш apt update летал, а не зависал в бесконечном ожидании ответа от серверов репозиториев.

Как только вы ответите на последний вопрос, первоначальную настройку Incus можно считать успешно завершенной! Следующим шагом мы создадим отдельный профиль с правильными лимитами и, наконец, запустим наши контейнеры.


Создание профиля и запуск контейнеров

В Incus есть стандартный профиль (default), который дает базовые возможности — например, доступ к сети. Но мы пойдем дальше и создадим свой собственный профиль, специально настроенный под нужды студента.

Профиль в Incus — это своего рода «трафарет» или набор правил. Вместо того чтобы настраивать каждый контейнер вручную, мы один раз описываем конфигурацию в профиле, а затем просто «прикладываем» её к нужному количеству инстансов.

В нашем студенческом профиле мы:

  • Разрешим «вложенность» (nesting), чтобы внутри контейнера можно было запускать Docker.

  • Ограничим диск 10 ГБ, чтобы никто случайно (или специально) не забил всё место на сервере.

  • Выделим 1 ядро процессора и 1 ГБ оперативной памяти. Для учебных задач и деплоя небольших приложений этого хватит с головой. А кому-то такой конфигурации и для личного ТГ-бота на Python будет достаточно (думайте, подписывайтесь!).

Создание профиля

Сначала создаем пустую «оболочку» нашего профиля:

incus profile create student

Если всё прошло успешно, система ответит: Profile student created.

Теперь наполним её смыслом. Первым делом включаем ту самую вложенность. Без этого параметра Docker внутри контейнера просто не заведется, так как ядро Linux по умолчанию запрещает контейнеру создавать другие контейнеры (из соображений безопасности).

incus profile set student security.nesting=true

Теперь последовательно задаем аппаратные лимиты:

# 1. Выделяем 10 ГБ места в основном пуле (default) и монтируем в корень (/)
incus profile device add student root disk path=/ pool=default size=10GiB

# 2. Ограничиваем количество доступных ядер процессора
incus profile set student limits.cpu=1

# 3. Устанавливаем лимит оперативной памяти
incus profile set student limits.memory=1GiB

Отлично! «Трафарет» готов, теперь можно «штамповать» контейнеры.

Запуск контейнера

Пришло время запустить наш первый студенческий инстанс. Для этого используем команду launch:

incus launch images:debian/13 student1 --profile default --profile student

Давайте разберем её по частям, чтобы понимать, что происходит:

  • incus launch — команда «скачай образ и сразу запусти».

  • images:debian/13 — указываем, откуда брать образ и какой именно. В данном случае мы берем чистый Debian 13 из официального репозитория образов.

  • student1 — уникальное имя нашего контейнера.

  • --profile default --profile student — крайне важный момент! Мы применяем сразу два профиля. Первый (default) дает контейнеру сеть, а второй (student) — наши лимиты и права на вложенность.

Процесс запуска похож на Docker: Incus сначала скачивает образ (если его нет локально), а затем разворачивает его. Но помните: здесь запускается не просто одна программа, а целая операционная система.

Если вы увидели сообщение Launching student1, значит, всё получилось! Проверим состояние наших владений и узнаем IP-адрес контейнера:

incus list

На выходе получим симпатичную таблицу:

Обратите внимание: колонка IPV6 пуста, как мы и настраивали на этапе инициализации. А адрес 10.48.193.254 — это внутренний адрес в нашей сети incusbr0.


SSH и подключение к серверу

Итак, контейнер запущен, профили применены, всё вроде настроено. Но не хватает самого главного — мы пока не можем зайти внутрь по-человечески, через SSH!

Использовать стандартный 22-й порт мы, очевидно, не можем: постучавшись по нему на IP-адрес нашего сервера, мы попадем в саму хост-машину, а не в контейнер студента. Но решение есть. Мы можем использовать кастомные порты, например, 2201 для первого студента, 2202 для второго, 220n для n-го... Тем самым мы выдадим каждому изолированному пространству свой уникальный внешний порт для подключения.

Делается это невероятно легко — через команду создания так называемого proxy-устройства (проброса портов):

incus config device add student1 ssh2201 proxy listen=tcp:0.0.0.0:2201 connect=tcp:127.0.0.1:22

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

  • device add student1 — говорим Incus добавить новое виртуальное устройство к контейнеру student1.

  • ssh2201 — это просто произвольное имя, которое мы даем этому правилу проброса (чтобы потом не запутаться).

  • proxy — указываем тип устройства. Мы создаем прокси, который будет перехватывать трафик.

  • listen=tcp:0.0.0.0:2201 — заставляем наш хост-сервер слушать входящие TCP-соединения на порту 2201 со всех доступных внешних IP-адресов (0.0.0.0).

  • connect=tcp:127.0.0.1:22 — указываем, куда этот трафик перенаправить. В данном случае — на локальный адрес (127.0.0.1) внутри контейнера, на стандартный 22-й порт SSH.

Выполнив команду, мы должны увидеть сообщение: Device ssh2201 added to student1. Значит, правило создано успешно!

Кажется, победа? Пробуем подключиться к серверу в терминале (вместо <ip> нужно подставить внешний публичный IP-адрес вашего хост-сервера):

ssh -p 2201 root@<ip>

И... получаем обидную ошибку: Connection closed by <ip> port 2201.

Почему так? Тут всплывают сразу две проблемы. Первую я банально не учёл в момент написания черновика: официальный образ Debian 13 — минимальный. В целях экономии места в нём просто не установлен SSH-демон (служба, которая принимает подключения). Нам нужно установить его вручную. Но как только мы попытаемся это сделать через apt update, мы немедленно споткнемся о вторую проблему. Помните нашу историю с IPv6 и Docker? Мы ведь переехали на новый 80-гигабайтный сервер, и на нём тоже установлен Docker! А значит, его файрвол сейчас точно так же блокирует выход в интернет для нашего нового сетевого моста incusbr0.

Решим все эти проблемы одной элегантной последовательностью команд прямо на хост-машине:

# 1. Сначала чиним интернет: разрешаем транзит трафика для нового моста incusbr0
sudo iptables -I FORWARD -i incusbr0 -j ACCEPT
sudo iptables -I FORWARD -o incusbr0 -j ACCEPT

# 2. Обновляем списки пакетов внутри контейнера (используя команду exec)
incus exec student1 -- apt update

# 3. Устанавливаем SSH-сервер внутри контейнера
incus exec student1 -- apt install -y openssh-server

# 4. Создаем обычного пользователя для студента
incus exec student1 -- adduser student

Важное примечание: Зачем мы создали пользователя student? Дело в том, что по умолчанию SSH-сервер в целях безопасности запрещает вход по паролю для суперпользователя root. Гораздо правильнее и безопаснее создать обычного юзера, задать ему пароль в процессе выполнения команды adduser и подключаться уже под ним.

Теперь, когда всё готово, пробуем подключиться по-правильному — под созданным пользователем student:

ssh -p 2201 student@<ip>

Ура! Консоль запросит пароль, и после его ввода мы окажемся внутри полностью изолированного, рабочего пространства студента!


Пара слов про реверс-прокси и привязку доменов

По аналогии со входом по SSH, мы сталкиваемся с ещё одной проблемой: стандартные веб-порты (80 для HTTP и 443 для HTTPS) у сервера одни. Мы не можем пробросить их напрямую каждому студенту. Как тогда направить посетителя, который вводит в браузере student1.napkincode.ru, в нужный изолированный контейнер?

Здесь на сцену выходит реверс-прокси (обратный прокси-сервер). Грубо говоря, это умный регулировщик трафика. Он принимает абсолютно все веб-запросы из интернета и, читая имя запрашиваемого домена, маршрутизирует трафик в нужную внутреннюю подсеть.

Для этой задачи мне нравится использовать Caddy. Его главная киллер-фича — он сам, автоматически и бесплатно, получает и продлевает SSL-сертификаты (те самые зелёные замочки в браузере). Я ставлю Caddy на обеих сторонах: и на хост-машине, и внутри студенческих контейнеров.

Настройка на хост-машине (снаружи)

На основном сервере мы настраиваем Caddy так, чтобы он принимал внешний трафик по домену студента, вешал на него SSL-сертификат, а затем передавал этот трафик уже в чистом виде (по HTTP) внутрь конкретного контейнера.

Конфиг-файл (Caddyfile) на хосте выглядит так:

student1.napkincode.ru {
    reverse_proxy 10.48.193.254:80
}

Внутренний IP-адрес (10.48.193.254) мы берём из той самой таблички, которую нам выдала команда incus list. Такую запись нужно добавить для каждого выделенного поддомена и контейнера.

Настройка в контейнере (внутри)

А что происходит внутри пространства студента? Там тоже крутится свой маленький локальный Caddy. Он принимает этот транзитный трафик на 80-м порту и направляет его уже до конкретного проекта — например, до контейнера с Django-приложением, который работает на порту 8000.

Конфиг-файл внутри студенческого контейнера выглядит следующим образом:

http://student1.napkincode.ru {
    reverse_proxy django:8000
}

Обратите внимание на важную деталь: мы явно указали http:// перед доменом. Поскольку наш внешний (хостовый) Caddy уже расшифровал HTTPS-трафик, внутри контейнерной сети мы общаемся по простому HTTP, чтобы не нагружать систему двойной работой по шифрованию и не ловить ошибки сертификатов.

И всё! Такая элегантная двухэтапная схема работает как часы: внешние пользователи получают безопасное подключение, а студент внутри контейнера спокойно деплоит свои приложения, вообще не забивая голову выпуском сертификатов.


Заключение

На самом деле, я искренне рад, что не стал выпрашивать серверы у руководства или, что ещё хуже, просить студентов покупать собственные. Пойти по пути самостоятельного исследования и экспериментов было лучшим решением!

Да, прямо во время занятия у нас возникли непредвиденные проблемы. Но студенты не восприняли это как преподавательский «косяк». Наоборот, получился отличный совместный траблшутинг в реальном времени: им было дико интересно поучаствовать в поиске решения и искренне обрадоваться вместе со мной, когда злополучный размер диска наконец-то расширился.

Сама технология системных контейнеров — это вообще отдельная магия. Она даёт огромный простор для экспериментов, создания тестовых полигонов и обучения, избавляя от необходимости под каждую новую задачу покупать или арендовать отдельный сервер. Ты просто разворачиваешь новое пространство за пару секунд и делаешь там всё, что душе угодно.

Надеюсь, эта статья была для вас полезной и убережет от пары-тройки граблей на старте. Если формат вам понравился, обязательно подписывайтесь на мой Telegram-канал «Код на салфетке» — там мы регулярно разбираем такие вот жизненные IT-задачки и делимся полезным опытом.

Комментарии (2)


  1. arthurpro
    16.04.2026 12:57

    всё хорошо, но почему ни слова не сказано про утилиту ssh2incus?


    1. proDream Автор
      16.04.2026 12:57

      Потому, что я про неё не в курсе. Изучу)