Серверное исполнение кода (раннер)
August 27, 2026 · View on GitHub
Часть документации Runit. Оглавление — в README.
Серверное исполнение кода (раннер)
JavaScript исполняется в браузере (Web Worker) — Docker не нужен, работает всегда.
Python, PHP, Ruby, Java исполняются на сервере в изолированном Docker-контейнере. Чтобы это работало локально, нужен запущенный Docker и собранные образы:
npm run runner:build-images
Без Docker приложение полностью работоспособно: при запуске такого сниппета в консоли появляется подсказка («Docker-демон не запущен…»), а не ошибка сервера.
В проде образы не собирают, а скачивают из реестра: их публикует CI на каждый тег версии (publish.yml, job «Раннер — <язык>»). Приложение при старте само проверяет наличие образов включённых языков и скачивает отсутствующие — в фоне, с отчётом в лог. Вручную то же самое:
RUNNER_IMAGE_PREFIX=ghcr.io/hexlet-volunteers/runit-runner \
RUNNER_IMAGE_TAG=1.2.3 \
DOCKER_HOST=tcp://runner-host:2376 \
npm run runner:pull-images
Скачивание при старте, а не по запросу, — сознательно: в обработчике запуска образы не тянутся никогда, иначе первый сниппет на «холодном» хосте ждал бы минуты вместо секунд, и запуск кода зависел бы от доступности реестра.
Юнит-тесты раннера (не требуют Docker):
npm run test:runner
Реальное исполнение всех языков проверяется смоуком — ему нужен запущенный Docker и собранные образы:
npm run runner:smoke
Тот же смоук прогоняется в CI на каждый PR (job «Раннер — исполнение 9 языков в Docker»): там проверяются вывод, stdin, таймаут, изоляция сети и что после таймаута не остаётся осиротевших контейнеров.
Изоляция песочницы
JavaScript (в браузере). Код сниппета исполняется в sandbox-iframe без allow-same-origin, то есть в непрозрачном origin, а внутри него — в Web Worker. Сеть закрыта политикой CSP (connect-src 'none'), как у серверных языков закрыта --network=none.
Обе границы обязательны, и вот почему. Раньше код шёл в Worker, созданный прямо на странице приложения: воркер делит с ней origin, поэтому запросы из него шли с cookie сессии. Проверка показала полную цепочку — сниппет читал auth.me (почта владельца), получал auth.csrfToken и выполнял мутации от его имени, например публиковал приватный сниппет. Срабатывало на самом обычном действии: открыть чужой сниппет по ссылке и нажать «Выполнить». Отдельный origin убирает cookie и доступ к нашему DOM, CSP убирает сеть, Worker оставляет отдельный поток — чтобы бесконечный цикл вешал только песочницу.
Серверные языки (в Docker). Код пользователя запускается с жёсткими ограничениями: без сети (--network=none), без capabilities (--cap-drop=ALL), от непривилегированного пользователя, с read-only файловой системой (writable только /tmp в памяти), с лимитами памяти, CPU, числа процессов, времени выполнения и размера вывода. Параллельные запуски ограничены семафором. Флаги собираются в одном месте — src/runner/dockerArgs.ts, и покрыты тестами, чтобы изоляцию нельзя было ослабить незаметно.
Настройки (переменные окружения)
| Переменная | По умолчанию | Назначение |
|---|---|---|
RUNNER_ENABLED | true | Полностью выключить серверный запуск |
RUNNER_LANGUAGES | все | Список языков через запятую |
RUNNER_TIMEOUT_MS | 10000 | Лимит времени (Java — 20000) |
RUNNER_MEMORY | 256m | Лимит памяти (Java — 512m) |
RUNNER_CPUS | 1 | Лимит CPU |
RUNNER_PIDS_LIMIT | 64 | Лимит процессов (Java — 256) |
RUNNER_MAX_OUTPUT_BYTES | 65536 | Лимит размера вывода |
RUNNER_MAX_CONCURRENT | 4 | Одновременных запусков |
RUNNER_DOCKER_BIN | docker | Путь к docker CLI |
RUNNER_IMAGE_PREFIX | runit-runner | Префикс имени образов. В проде — полное имя в реестре, ghcr.io/hexlet-volunteers/runit-runner |
RUNNER_IMAGE_TAG | 1 | Тег образов. В проде равен версии приложения |
Замечания для деплоя
- Серверу нужен доступ к Docker. Доступ к docker-сокету равносилен root на хосте — не монтируйте сокет в контейнер приложения, которое исполняет чужой код, без отдельной изоляции (выделенный runner-хост, rootless или удалённый демон по mTLS).
- Там, где Docker недоступен (например, Heroku), раннер корректно деградирует: сайт работает, серверный запуск отвечает подсказкой.
- Установка пакетов (
pip installи т.п.) из кода невозможна — сети в контейнере нет. Нужные библиотеки добавляются в образы (runner-images/).