Эксперименты с кодом на рабочей машине заканчиваются одинаково: библиотека конфликтует с системной, тестовый скрипт занимает всю память, неудачный пакет оставляет следы на часы вычищения. Песочница на домашнем сервере снимает все три проблемы. Код запускается в контейнере с жёсткими ограничениями, ломается только он, а восстановление занимает одну команду. Ниже порядок постройки такой среды для обучения и для проверки нестабильных сборок.
Чем плоха работа на основной системе и что даёт отдельная среда
У основной машины есть свойство, мешающее экспериментам: она нужна в рабочем состоянии. На ней лежат документы, крутятся службы, к ней подключены устройства в доме. Сбой стоит не потерянного часа, а остановленной инфраструктуры.
Вторая проблема в накоплении мусора. Установка пакета ради пятиминутной проверки меняет системные библиотеки. Виртуальное окружение решает вопрос частично, но не защищает от всего остального: от записи в системные каталоги, от потребления памяти, от сетевых обращений.
Третья проблема касается самого обучения. Когда среда хрупкая, человек боится пробовать, а половина учебного процесса это попытки сделать что-то заведомо нерабочее и посмотреть, что выйдет. В песочнице такие попытки бесплатны.
Изолированный контейнер даёт три свойства, которых нет у основной системы: ограниченность ресурсов, воспроизводимость из текстового описания и одноразовость, поскольку сломанное выбрасывают целиком, а не чинят.
Три уровня изоляции и как выбрать нужный под задачу
Не всякая песочница одинаково строга, и уровень стоит выбирать под задачу, а не по принципу максимального.
Уровни различаются строго:
- Обычное виртуальное окружение защищает от конфликта версий библиотек и ни от чего больше, процесс видит всю файловую систему и пишет в домашний каталог, годится для учебного скрипта;
- Контейнер с ограничениями даёт отдельную файловую систему, отдельную сеть и лимиты памяти с числом процессов, закрывает большинство учебных задач и запускается за секунду;
- Контейнер без сети и с файловой системой только для чтения подходит для кода неизвестного происхождения, он физически не способен ничего скачать и ничего записать.
Разница между вторым и третьим уровнем это пара флагов, поэтому разумная практика такая: по умолчанию держать второй и переходить на третий при работе с непроверенным кодом.
Базовый образ песочницы со стеком инструментов и точка входа
Песочницу удобно строить не вокруг голого образа языка, а вокруг заранее подготовленного, где уже стоят интерпретатор, отладчик, редактор и средства проверки. Тогда каждая новая сессия начинается с кода, а не с установки инструментов.
FROM python:3.12-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
git curl vim-tiny procps gcc \
&& rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir \
pytest ruff ipython requests
RUN useradd -m -u 1000 dev
USER dev
WORKDIR /home/dev
CMD ["/bin/bash"]
Сборка и запуск:
docker build -t sandbox:py .
docker run -it --rm --name sandbox sandbox:py
Две детали в этом файле важнее, чем кажутся. Отдельный пользователь означает, что код работает не от имени суперпользователя, и случайная команда с повышенными полномочиями не сработает. Флаг --rm удаляет контейнер после выхода, что и делает среду одноразовой.
Для других языков набор меняется, но схема та же: базовый образ, инструменты разработки, обычный пользователь, оболочка как точка входа.
Постоянная песочница с описанием в devcontainer и общим томом для кода
Одноразовый контейнер неудобен тем, что установленное в нём исчезает. Для длительного обучения нужна среда, которая живёт между сессиями, но при этом остаётся описанной текстом.
{
"name": "study-sandbox",
"image": "sandbox:py",
"remoteUser": "dev",
"workspaceFolder": "/workspace",
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind",
"runArgs": [
"--memory=1g",
"--cpus=1",
"--pids-limit=256"
],
"customizations": {
"vscode": {
"extensions": ["ms-python.python"]
}
}
}
Файл devcontainer.json кладётся в корень каталога с задачами, и редактор поднимает среду сам. Код лежит на хосте, переживает удаление контейнера и попадает в систему контроля версий.
Такой подход даёт сочетание, которое трудно получить иначе: окружение воспроизводится из описания, а результаты работы сохраняются. Сломалось окружение, удалили контейнер и подняли заново, код не пострадал.
Для нестабильных сборок схема та же, меняется только образ и способ подачи артефакта.
Ограничение ресурсов, изоляция сети и правила работы с секретами
Основной риск в песочнице это не взлом, а ресурсный голод. Учебный код с утечкой памяти или бесконечным порождением процессов способен положить всю машину, если лимитов нет.
Память задаётся напрямую, и превышение приводит к принудительному завершению процесса внутри контейнера:
docker run -it --rm --memory 1g --memory-swap 1g sandbox:py
Параметр --memory-swap, равный --memory, отключает использование файла подкачки. Без этого контейнер начинает медленно потреблять дисковое пространство вместо быстрой остановки.
Число процессов ограничивается отдельно и защищает от лавинообразного порождения задач:
docker run -it --rm --pids-limit 256 sandbox:py
Процессорное время задаётся долей ядра:
docker run -it --rm --cpus 1.5 sandbox:py
Полный запуск с ограничениями выглядит так:
docker run -it --rm --name test \
--memory 1g --memory-swap 1g \
--pids-limit 256 --cpus 1.5 \
--tmpfs /tmp:size=256m \
-v "$PWD/code:/workspace:ro" \
sandbox:py
Проверить, что лимиты действительно применены, можно изнутри контейнера:
cat /sys/fs/cgroup/memory.max
cat /sys/fs/cgroup/pids.max
Отдельно стоит упомянуть каталог /tmp на tmpfs с явным размером. Многие программы пишут временные файлы именно туда, и без ограничения они способны заполнить диск хоста.
Сеть в учебной песочнице нужна не всегда, а когда не нужна, её лучше выключить целиком:
docker run -it --rm --network none sandbox:py
В таком контейнере нет даже разрешения имён, и любой сетевой вызов завершается ошибкой. Это лучший режим для запуска чужого кода: даже если в нём есть обращение наружу, оно не пройдёт.
Когда сеть нужна для установки пакетов, разумный компромисс это отдельная сеть с запретом доступа к остальной инфраструктуре:
docker network create sandbox-net --internal=false
docker run -it --rm --network sandbox-net sandbox:py
Смысл в том, чтобы контейнер мог выйти в интернет, но не видел домашние устройства и другие службы сервера. Без такого разделения экспериментальный код получает доступ ко всей локальной сети, что для учебного окружения совершенно лишнее.
Секреты в песочницу не передают вовсе. Учебные примеры, обращающиеся к внешним службам, стоит писать с заведомо нерабочими ключами, а реальные токены держать в отдельном хранилище и монтировать только в рабочие контейнеры. Правило простое: если код может напечатать содержимое переменных окружения, в них не должно быть ничего ценного.
Проверка нестабильной сборки и что обычно ломается внутри песочницы
Отдельный сценарий, ради которого песочницу часто и заводят: проверить свежую сборку программы до того, как она попадёт на рабочую машину. Порядок действий здесь важнее самих команд.
Артефакт кладут в каталог на хосте и монтируют внутрь только для чтения, и исходный файл остаётся нетронутым.
mkdir -p ~/sandbox/artifacts
cp ~/Загрузки/app-2.4.0-rc1 ~/sandbox/artifacts/
sha256sum ~/sandbox/artifacts/app-2.4.0-rc1 | tee ~/sandbox/artifacts/app-2.4.0-rc1.sha256
docker run -it --rm \
--memory 1g --pids-limit 256 --cpus 1 \
--network none \
--read-only --tmpfs /tmp:size=256m \
-v ~/sandbox/artifacts:/artifacts:ro \
sandbox:py /bin/bash
Внутри проверяют три вещи. Контрольную сумму, чтобы убедиться в целостности файла. Поведение на заведомо неверных входных данных, поскольку именно на них ломаются сырые сборки. И потребление памяти, которое удобно отслеживать снаружи во время прогона:
docker stats --no-stream test
Сравнение версий даёт больше, чем проверка одной. Если рядом поднять два контейнера, с текущей стабильной версией и с кандидатом на замену, разница в поведении видна сразу и не требует догадок. Имена контейнеров в этом случае стоит задавать осмысленно, чтобы в выводе статистики было понятно, где какая версия.
Отдельный приём для учебных задач: запуск решения с искусственно испорченными входными данными. Пустой файл, файл с неверной кодировкой, каталог вместо файла, очень длинная строка. Половина ошибок в учебных проектах всплывает именно на таких проверках, и делать их безопаснее в одноразовом контейнере.
Первое, с чем сталкиваются почти сразу: не хватает памяти. Установка тяжёлого пакета со сборкой расширений из исходников потребляет гигабайты, а лимит выставлен на уровне одного. Симптом выглядит как внезапное завершение процесса без внятной ошибки.
Проверить причину изнутри помогает просмотр системного журнала ядра на хосте:
dmesg | tail -20 | grep -i 'killed process'
Строка о принудительно завершённом процессе с указанием ограничения памяти означает, что сработал именно лимит. Лечится либо увеличением лимита на время установки, либо готовым собранным пакетом вместо сборки из исходников.
Второе, исчерпание числа процессов. Сборочные системы любят запускать задачи параллельно, и при ограничении в 256 процессов сборка упирается в потолок. Помогает явное указание числа параллельных задач:
pip install --no-cache-dir --no-build-isolation numpy
make -j2
Третье, отсутствие сети в момент, когда она нужна для установки зависимостей. Здесь порядок действий такой: сначала контейнер с сетью ставит пакеты, затем результат фиксируется в образе, а дальше работа идёт уже без сети.
docker run --name build --network sandbox-net sandbox:py pip install --no-cache-dir pandas matplotlib
docker commit build sandbox:py-data
docker rm build
Команда commit превращает изменённый контейнер в новый образ. Для песочницы приемлемо, хотя правильнее описывать установку в файле сборки, чтобы состав образа оставался документированным.
Одна среда на все задачи быстро становится тесной. Удобнее держать несколько изолированных песочниц: под конкретный язык, под конкретный курс, под эксперименты с чужим кодом.
Стоимость невелика, поскольку образы разделяют общие слои. Десять песочниц на базе одного образа занимают на диске ненамного больше одной.
Именование спасает через месяц, когда список контейнеров перестаёт быть очевидным:
docker ps -a --format 'table {{.Names}} {{.Image}} {{.Status}}'
Разумная схема включает назначение, язык и номер попытки. Имя study-py-07 говорит больше, чем test12, и позволяет удалять старое по общей части имени.
При обучении нескольких людей каталоги с кодом стоит разнести. Общий том приводит к тому, что один случайно затирает файл другого. У каждого свой каталог на хосте, смонтированный в свой контейнер.
Жизненный цикл песочницы, именование окружений и уборка после экспериментов
Песочница живёт циклами, и каждый стоит оформлять осознанно.
Создание: сборка образа один раз и повторное использование. Пересобирать образ на каждую сессию расточительно, слои кэшируются, и сборка занимает секунды только при изменении файла описания.
Работа: запуск с нужными флагами и общим томом для кода. Имя контейнера стоит задавать явно, чтобы потом не искать его в списке.
Уборка: остановленные контейнеры, неиспользуемые образы и тома занимают место незаметно.
docker container prune -f
docker image prune -f
docker volume ls --filter dangling=true
docker system df
Последняя команда показывает, сколько места съедает каждый тип объектов.
Отдельная практика, которая окупается быстро: держать файл с описанием песочницы в системе контроля версий рядом с учебным кодом. Через полгода среда восстанавливается одной командой сборки, а не вспоминанием того, что именно в ней было установлено.
Для обучения программированию такая конструкция даёт главное: возможность ошибаться без последствий. Сломанный контейнер это не катастрофа, а учебный материал, и разница между двумя подходами к ошибкам обычно определяет, дойдёт человек до конца курса или бросит на третьей неделе.