Введение
Jupyter-ноутбуки давно стали стандартным инструментом для анализа данных, экспериментов с машинным обучением, обучения и быстрого прототипирования. Запускать Jupyter на ноутбуке удобно, пока вы работаете в одиночку. Проблемы начинаются, когда то же окружение нужно команде или учебной группе: каждый ставит свой Python, версии библиотек расходятся, ноутбук, который работает у одного, падает у другого, а тяжёлые вычисления на часы занимают личный компьютер.
Эту задачу решает JupyterHub. Это многопользовательский сервер, который выдаёт каждому человеку собственное окружение Jupyter в браузере, а все эти окружения работают на одной общей машине с одинаковым набором программ и данных. Пользователи входят по логину и паролю, а администратор управляет учётными записями, ресурсами и установленными пакетами в одном месте.
В этой статье мы разберём, как устроен JupyterHub, когда его имеет смысл запускать на виртуальном сервере, и пошагово установим его на Ubuntu 24.04: от виртуального окружения Python и службы systemd до обратного прокси Nginx с бесплатным HTTPS-сертификатом и автоматической остановкой неактивных сессий.
Основные термины
- VPS (Virtual Private Server): виртуальная машина с выделенными процессором, памятью и диском и полным root-доступом, работающая в облачной инфраструктуре.
- JupyterHub: серверное приложение, которое аутентифицирует пользователей и запускает для каждого из них отдельный сервер Jupyter.
- Hub: основной процесс JupyterHub. Он хранит базу пользователей, обрабатывает вход и решает, когда запускать и останавливать пользовательские серверы.
- Прокси: компонент, который принимает весь входящий HTTP-трафик и направляет его либо в Hub, либо на нужный пользовательский сервер. По умолчанию это configurable-http-proxy, приложение на Node.js.
- Однопользовательский сервер (single-user server): обычный сервер Jupyter (JupyterLab или Notebook), работающий от имени одного пользователя. JupyterHub запускает его командой jupyterhub-singleuser.
- Аутентификатор (authenticator): подключаемый модуль проверки учётных данных. Стандартный PAM-аутентификатор использует системные учётные записи Linux на сервере.
- Спаунер (spawner): подключаемый модуль, который запускает пользовательские серверы. Стандартный LocalProcessSpawner запускает каждый сервер как локальный процесс под учётной записью пользователя в Linux.
- JupyterLab: современный веб-интерфейс для ноутбуков, терминалов, текстовых файлов и расширений.
- Обратный прокси (reverse proxy): веб-сервер (в нашем случае Nginx), который стоит перед приложением, обслуживает HTTPS и передаёт ему запросы.
Как работает JupyterHub
JupyterHub состоит из трёх взаимодействующих частей: Hub, прокси и однопользовательских серверов. Команда jupyterhub запускает Hub и по умолчанию поднимает прокси как дочерний процесс.
Типичная сессия выглядит так:
- Пользователь открывает адрес сервера в браузере. Запрос попадает на прокси, который по умолчанию слушает порт 8000.
- Прокси передаёт запрос в Hub, и тот показывает страницу входа.
- Аутентификатор проверяет логин и пароль. При PAM-аутентификации это данные учётной записи пользователя в Linux на сервере.
- После успешного входа спаунер запускает для пользователя отдельный сервер Jupyter под его учётной записью и в его домашнем каталоге.
- Hub сообщает прокси новый маршрут: все запросы к /user/<username>/ теперь идут напрямую на сервер этого пользователя.
- Пользователь работает в JupyterLab. Трафик ноутбуков, ядер и терминалов идёт через прокси к пользовательскому серверу, минуя Hub.
Внутри системы Hub слушает 127.0.0.1:8081 для своего REST API, а прокси открывает API управления на 127.0.0.1:8001. Оба порта остаются локальными, и открывать их в интернет нельзя. Пользователи видят только публичный порт прокси или порт Nginx, который стоит перед ним.
Поскольку каждый пользовательский сервер работает под отдельной учётной записью Linux, пользователи не могут читать чужие файлы, если вы явно этого не разрешите. При этом у них общая установка Python, общие пакеты и общие ресурсы сервера. Именно этим JupyterHub удобен для команд: библиотеку достаточно установить один раз, и она появится у всех.
Почему Hub работает от root
В стандартной конфигурации Hub нужны права root по двум причинам: PAM-аутентификатор читает системные данные о паролях, а спаунер должен запускать процессы от имени других пользователей. Сами пользовательские серверы работают без root. Если запуск Hub от root в вашей среде недопустим, JupyterHub можно настроить с sudospawner или со спаунерами на основе контейнеров, но эти варианты выходят за рамки статьи.
Сценарии использования и выбор конфигурации сервера
JupyterHub на виртуальном сервере хорошо подходит, когда небольшой или средней группе нужно общее окружение, доступное круглосуточно:
- Команды data science: общий доступ к датасетам на сервере, одинаковые версии библиотек и возможность оставить долгие вычисления на ночь.
- Курсы и воркшопы: студентам нужен только браузер, а преподаватель готовит окружение один раз вместо того, чтобы разбираться с десятками ноутбуков.
- Исследовательские группы: общее место для экспериментов, где результаты хранятся на сервере, а не разбросаны по личным компьютерам.
- Внутренняя аналитика: аналитики подключаются к корпоративным базам данных с сервера внутри контролируемой сети, а не со своих устройств.
Требования к ресурсам зависят от числа одновременно работающих пользователей и от нагрузки. Каждый активный пользовательский сервер с запущенным ядром Python в простое занимает примерно 100–300 МБ оперативной памяти, а после загрузки данных в память гораздо больше. Для начала можно ориентироваться на такую таблицу:
| Сценарий | Одновременных пользователей | vCPU | RAM | Диск |
|---|---|---|---|---|
| Небольшая команда, лёгкий анализ | 2–5 | 2 | 4 ГБ | 40 ГБ |
| Воркшоп или учебная группа | 10–20 | 4 | 8–16 ГБ | 80 ГБ |
| Машинное обучение, большие датасеты | 5–10 | 8+ | 32 ГБ+ | 160 ГБ+ |
Преимущество VPS в облаке Serverspace в том, что размер не нужно угадывать заранее: можно начать со скромной конфигурации и добавлять процессоры, память или диск по мере роста числа пользователей.
Для крупных инсталляций на десятки и сотни пользователей проект JupyterHub поддерживает отдельный дистрибутив для Kubernetes. Для одного сервера описанная ниже схема проще и легче в сопровождении.
Установка JupyterHub на VPS: пошаговая инструкция
Мы установим JupyterHub с JupyterLab на Ubuntu 24.04, разместим его в изолированном виртуальном окружении Python в каталоге /opt/jupyterhub, запустим как службу systemd и опубликуем через Nginx с HTTPS-сертификатом от Let's Encrypt.
Что понадобится
- VPS с Ubuntu 24.04, минимум 2 vCPU и 4 ГБ оперативной памяти.
- Доступ по SSH под root или под пользователем с правами sudo.
- Доменное имя, например hub.example.com, с A-записью, указывающей на IP-адрес сервера. Домен нужен для HTTPS-сертификата.
Шаг 1. Обновите систему и установите зависимости
Подключитесь к серверу по SSH и обновите списки пакетов и установленные пакеты:
sudo apt update && sudo apt upgrade -y
Установите инструменты Python, Node.js с npm (они нужны для прокси) и Nginx:
sudo apt install -y python3 python3-venv python3-pip nodejs npm nginx
Проверьте установленные версии:
python3 --version
node --version
На Ubuntu 24.04 вывод выглядит так:
Python 3.12.3
v18.19.1
Шаг 2. Установите configurable-http-proxy
Прокси распространяется как пакет npm. Установите его глобально, чтобы команда configurable-http-proxy была доступна во всей системе:
sudo npm install -g configurable-http-proxy
Убедитесь, что команда находится:
which configurable-http-proxy
Что делать, если команда не найдена
Глобальные пакеты npm обычно ставятся в /usr/local/bin. Если which ничего не вывела, посмотрите вывод npm prefix -g и добавьте соответствующий каталог bin в переменную PATH в unit-файле systemd, который мы создадим на шаге 6.
Шаг 3. Создайте виртуальное окружение и установите JupyterHub
Мы держим JupyterHub отдельно от системного Python, чтобы обновления ОС его не ломали, а он не мешал другим программам. Создайте виртуальное окружение:
sudo python3 -m venv /opt/jupyterhub
Установите в него JupyterHub, JupyterLab и классический интерфейс Notebook. Пакет jupyterhub-idle-culler понадобится позже для остановки неактивных сессий:
sudo /opt/jupyterhub/bin/python3 -m pip install --upgrade pip
sudo /opt/jupyterhub/bin/python3 -m pip install jupyterhub jupyterlab notebook jupyterhub-idle-culler
Проверьте версию:
/opt/jupyterhub/bin/jupyterhub --version
Команда выведет номер установленной версии, например 5.3.0. Точное значение зависит от актуального релиза.
Установите в то же окружение библиотеки, которые нужны пользователям, например:
sudo /opt/jupyterhub/bin/python3 -m pip install numpy pandas matplotlib scikit-learn
Установленные так пакеты доступны всем пользователям, потому что все пользовательские серверы запускаются из /opt/jupyterhub.
Шаг 4. Создайте учётные записи пользователей
PAM-аутентификатор пускает пользователей по их учётным записям Linux, поэтому каждому пользователю JupyterHub нужен системный пользователь. Для примера создадим две учётные записи: администратора alice и обычного пользователя bob:
sudo adduser alice
sudo adduser bob
Команда adduser создаёт домашний каталог и запрашивает пароль. Именно этот пароль пользователь будет вводить на странице входа JupyterHub. Задавайте надёжные пароли: страница входа будет доступна из интернета.
Шаг 5. Настройте JupyterHub
Создайте каталог для конфигурации и сгенерируйте файл настроек по умолчанию:
sudo mkdir -p /opt/jupyterhub/etc/jupyterhub
cd /opt/jupyterhub/etc/jupyterhub
sudo /opt/jupyterhub/bin/jupyterhub --generate-config
Команда создаст файл jupyterhub_config.py, в котором все доступные параметры закомментированы. Это обычный файл на Python. Откройте его в редакторе:
sudo nano /opt/jupyterhub/etc/jupyterhub/jupyterhub_config.py
Добавьте в конец файла следующие строки:
import sys
# Слушать только localhost: публичный трафик обслуживает Nginx
c.JupyterHub.bind_url = 'http://127.0.0.1:8000'
# После входа открывать JupyterLab вместо классического интерфейса
c.Spawner.default_url = '/lab'
# Пользователи, которым разрешён вход
c.Authenticator.allowed_users = {'alice', 'bob'}
# Пользователи с доступом к панели администратора
c.Authenticator.admin_users = {'alice'}
# Останавливать пользовательские серверы после часа бездействия
c.JupyterHub.services = [
{
'name': 'idle-culler',
'command': [sys.executable, '-m', 'jupyterhub_idle_culler', '--timeout=3600'],
}
]
c.JupyterHub.load_roles = [
{
'name': 'idle-culler',
'scopes': ['list:users', 'read:users:activity', 'read:servers', 'delete:servers'],
'services': ['idle-culler'],
}
]
Что делают эти настройки:
- bind_url заставляет прокси принимать подключения только с самого сервера. Пользователи будут заходить в JupyterHub через Nginx по HTTPS, а не напрямую на порт 8000.
- default_url задаёт страницу, которая открывается после входа. Удалите эту строку, если предпочитаете классический интерфейс Notebook.
- allowed_users перечисляет учётные записи, которым разрешён вход. Начиная с JupyterHub 5, если разрешённые пользователи не заданы, по умолчанию войти не может никто. Так другие системные учётные записи на сервере защищены от входа через JupyterHub.
- admin_users даёт доступ к панели администратора, где видны активные сессии, можно останавливать и запускать пользовательские серверы и добавлять пользователей.
- Блоки services и load_roles запускают idle culler как службу JupyterHub и выдают ей только нужные права. Она останавливает пользовательские серверы, которые бездействуют дольше 3600 секунд, и освобождает занятую ими память. Файлы пользователя при этом не затрагиваются.
Храните служебные файлы в отдельном каталоге
При первом запуске JupyterHub создаёт в рабочем каталоге файлы jupyterhub.sqlite (база пользователей) и jupyterhub_cookie_secret (ключ для подписи cookie). В нашей схеме это каталог /opt/jupyterhub/etc/jupyterhub. Не публикуйте эти файлы и включите их в резервные копии.
Шаг 6. Создайте службу systemd
Чтобы JupyterHub запускался автоматически при загрузке и перезапускался после сбоев, создайте unit-файл systemd:
sudo nano /etc/systemd/system/jupyterhub.service
Вставьте в него следующее содержимое:
[Unit]
Description=JupyterHub
After=network.target
[Service]
User=root
Environment="PATH=/opt/jupyterhub/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
WorkingDirectory=/opt/jupyterhub/etc/jupyterhub
ExecStart=/opt/jupyterhub/bin/jupyterhub -f /opt/jupyterhub/etc/jupyterhub/jupyterhub_config.py
Restart=on-failure
[Install]
WantedBy=multi-user.target
Переменная PATH ставит /opt/jupyterhub/bin на первое место. Это важно: спаунер ищет команду jupyterhub-singleuser в PATH, а пользовательские серверы будут использовать Python и пакеты из виртуального окружения.
Перечитайте конфигурацию systemd, включите службу и запустите её:
sudo systemctl daemon-reload
sudo systemctl enable --now jupyterhub
Проверьте статус:
sudo systemctl status jupyterhub
Служба должна быть в состоянии active (running). Чтобы следить за журналом в реальном времени, выполните:
sudo journalctl -u jupyterhub -f
Среди сообщений при запуске должна появиться строка примерно такого вида:
[I 2026-10-18 10:15:42.318 JupyterHub app:3770] JupyterHub is now running at http://127.0.0.1:8000/
Время и номер строки в исходном файле у вас будут другими. Чтобы выйти из просмотра журнала, нажмите Ctrl+C.
Шаг 7. Настройте Nginx как обратный прокси
JupyterLab активно использует WebSocket для связи с ядрами, поэтому конфигурация Nginx должна передавать заголовки Upgrade и Connection. Создайте конфигурацию сайта:
sudo nano /etc/nginx/sites-available/jupyterhub
Добавьте следующее, заменив hub.example.com на свой домен:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name hub.example.com;
client_max_body_size 100M;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 86400;
}
}
Несколько пояснений к конфигурации:
- Блок map выставляет заголовок Connection в значение upgrade для WebSocket-запросов и в close для обычных.
- client_max_body_size поднимает стандартный лимит загрузки в 1 МБ, чтобы пользователи могли загружать датасеты через интерфейс JupyterLab. Подберите значение под свои задачи.
- proxy_read_timeout не даёт Nginx закрывать долгоживущие соединения с ядрами после стандартных 60 секунд тишины.
Включите сайт, отключите сайт по умолчанию, проверьте синтаксис и перезагрузите Nginx:
sudo ln -s /etc/nginx/sites-available/jupyterhub /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx
Если конфигурация верна, nginx -t выведет:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Шаг 8. Включите HTTPS с Let's Encrypt
На странице входа пользователи вводят пароли, поэтому соединение должно быть зашифровано. Установите Certbot с плагином для Nginx:
sudo apt install -y certbot python3-certbot-nginx
Запросите сертификат для своего домена:
sudo certbot --nginx -d hub.example.com
Certbot спросит адрес электронной почты для уведомлений об истечении срока, получит сертификат, добавит настройки HTTPS в конфигурацию Nginx и настроит перенаправление с HTTP на HTTPS. Продление выполняется автоматически по таймеру systemd. Проверить его можно пробным запуском:
sudo certbot renew --dry-run
Шаг 9. Настройте файрвол
Разрешите только SSH и веб-трафик, а всё остальное, включая порт 8000, оставьте закрытым:
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
Разрешайте SSH до включения файрвола, иначе можно потерять доступ к серверу. Проверьте результат:
sudo ufw status
Status: active
To Action From
-- ------ ----
OpenSSH ALLOW Anywhere
Nginx Full ALLOW Anywhere
OpenSSH (v6) ALLOW Anywhere (v6)
Nginx Full (v6) ALLOW Anywhere (v6)
Шаг 10. Войдите и проверьте работу
Откройте в браузере https://hub.example.com. Появится страница входа JupyterHub. Войдите как alice с паролем, заданным на шаге 4. Через несколько секунд JupyterHub запустит личный сервер, и откроется JupyterLab с содержимым домашнего каталога пользователя.
Создайте новый ноутбук и выполните простую проверку:
import sys, getpass
print(getpass.getuser())
print(sys.executable)
Ожидаемый вывод подтверждает, что сервер работает под учётной записью пользователя и использует общее виртуальное окружение:
alice
/opt/jupyterhub/bin/python3
Поскольку alice администратор, в меню File → Hub Control Panel ей доступна страница Admin. Там видно, какие пользователи активны, можно запускать и останавливать их серверы и добавлять новых пользователей.
Как добавить пользователя позже
Чтобы добавить пользователя, создайте учётную запись Linux командой adduser, впишите имя в allowed_users в файле jupyterhub_config.py и перезапустите службу командой sudo systemctl restart jupyterhub. Учтите, что в этой схеме перезапуск останавливает и серверы пользователей, которые в этот момент работают: JupyterLab автоматически сохраняет ноутбуки раз в пару минут, но последние несохранённые правки и все переменные в запущенных ядрах теряются, поэтому планируйте перезапуски на спокойное время.
Ограничение ресурсов для пользователей
Со стандартным LocalProcessSpawner один пользователь может занять весь процессор и всю память сервера, например загрузив огромный датасет. Параметры mem_limit и cpu_limit в JupyterHub есть, но этот спаунер их игнорирует. Если нужны жёсткие лимиты, установите в виртуальное окружение пакет jupyterhub-systemdspawner и задайте c.JupyterHub.spawner_class = 'systemdspawner.SystemdSpawner'. Этот спаунер запускает каждый пользовательский сервер как отдельный unit systemd и применяет ограничения памяти и процессора через cgroups. Другой вариант — DockerSpawner, который изолирует каждого пользователя в контейнере.
Решение типичных проблем
- «Spawn failed» после входа. Посмотрите journalctl -u jupyterhub. Чаще всего причина в том, что не находится jupyterhub-singleuser: в PATH в unit-файле systemd нет /opt/jupyterhub/bin.
- Страница входа сообщает, что пользователю вход запрещён. Проверьте, что имя есть в allowed_users и что служба перезапущена после правки конфигурации.
- Ноутбук открывается, но ядро не подключается. Обычно это значит, что не передаются заголовки WebSocket. Сравните свою конфигурацию Nginx с конфигурацией из шага 7.
- Ошибка 502 Bad Gateway. JupyterHub не запущен или слушает другой адрес. Проверьте systemctl status jupyterhub и значение bind_url.
Заключение
Мы развернули JupyterHub на одном виртуальном сервере: установили его в изолированное окружение Python, настроили вход по системным учётным записям, автозапуск через systemd, публикацию через Nginx с HTTPS и автоматическую остановку неактивных сессий. В результате каждый участник команды получает личный JupyterLab в браузере, а библиотеки, данные и вычислительные мощности управляются из одного места.
Такая схема хорошо подходит для команд из нескольких человек, учебных групп и исследовательских коллективов. С ростом нагрузки логичные следующие шаги — лимиты ресурсов через SystemdSpawner, единый вход через OAuth-аутентификатор и регулярное резервное копирование домашних каталогов. VPS от Serverspace позволяет масштабировать сервер вместе с нагрузкой: можно начать с небольшой конфигурации и добавлять ресурсы, когда они понадобятся пользователям.