惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Cisco Talos Blog
Cisco Talos Blog
V
V2EX
C
Check Point Blog
GbyAI
GbyAI
D
Docker
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
B
Blog RSS Feed
H
Hackread – Cybersecurity News, Data Breaches, AI and More
N
Netflix TechBlog - Medium
T
Troy Hunt's Blog
博客园 - Franky
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Microsoft Security Blog
Microsoft Security Blog
P
Privacy & Cybersecurity Law Blog
WordPress大学
WordPress大学
The Cloudflare Blog
S
SegmentFault 最新的问题
Latest news
Latest news
Microsoft Azure Blog
Microsoft Azure Blog
P
Proofpoint News Feed
I
InfoQ
博客园 - 【当耐特】
NISL@THU
NISL@THU
A
About on SuperTechFans
T
Tailwind CSS Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
The Hacker News
The Hacker News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Scott Helme
Scott Helme
雷峰网
雷峰网
C
CXSECURITY Database RSS Feed - CXSecurity.com
Security Latest
Security Latest
V
Vulnerabilities – Threatpost
Security Archives - TechRepublic
Security Archives - TechRepublic
A
Arctic Wolf
Hacker News: Ask HN
Hacker News: Ask HN
N
News and Events Feed by Topic
IT之家
IT之家
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
aimingoo的专栏
aimingoo的专栏
T
Threat Research - Cisco Blogs
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
SecWiki News
SecWiki News
大猫的无限游戏
大猫的无限游戏
S
Security Affairs
The Register - Security
The Register - Security
www.infosecurity-magazine.com
www.infosecurity-magazine.com
L
LINUX DO - 热门话题
T
Tor Project blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром T-TOPS: Как распутать гордиев узел проекта после выхода в прод (меч не понадобится) Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Вместо 8 разных VPS: как я организовал практику студентам на одном сервере
Иван · 2026-04-16 · via Все публикации подряд на Хабре

Простой

17 мин

8.1K

Всем привет!

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

Я преподаю на 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-задачки и делимся полезным опытом.