Серверное исполнение кода (раннер)

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_ENABLEDtrueПолностью выключить серверный запуск
RUNNER_LANGUAGESвсеСписок языков через запятую
RUNNER_TIMEOUT_MS10000Лимит времени (Java — 20000)
RUNNER_MEMORY256mЛимит памяти (Java — 512m)
RUNNER_CPUS1Лимит CPU
RUNNER_PIDS_LIMIT64Лимит процессов (Java — 256)
RUNNER_MAX_OUTPUT_BYTES65536Лимит размера вывода
RUNNER_MAX_CONCURRENT4Одновременных запусков
RUNNER_DOCKER_BINdockerПуть к docker CLI
RUNNER_IMAGE_PREFIXrunit-runnerПрефикс имени образов. В проде — полное имя в реестре, ghcr.io/hexlet-volunteers/runit-runner
RUNNER_IMAGE_TAG1Тег образов. В проде равен версии приложения

Замечания для деплоя

  • Серверу нужен доступ к Docker. Доступ к docker-сокету равносилен root на хосте — не монтируйте сокет в контейнер приложения, которое исполняет чужой код, без отдельной изоляции (выделенный runner-хост, rootless или удалённый демон по mTLS).
  • Там, где Docker недоступен (например, Heroku), раннер корректно деградирует: сайт работает, серверный запуск отвечает подсказкой.
  • Установка пакетов (pip install и т.п.) из кода невозможна — сети в контейнере нет. Нужные библиотеки добавляются в образы (runner-images/).