Два разработчика клонируют один репозиторий. У первого тесты проходят, у второго падают на импорте библиотеки, которая «вчера ещё работала». Выясняется, что у одного стоит версия зависимости месячной давности, у другого свежая, а у третьего вообще системная Node, о которой никто не помнит, кто её ставил. Репозиторий один, а среды разные. Хочется сказать: «давайте вынесем всё на сервер», но сервер сам по себе ничего не выравнивает. Если на нём просто завести пару учётных записей и руками поставить инструменты, получится ещё одна лоскутная среда, только удалённая.
Dev Container устроен иначе, и чтобы понять, что именно он даёт команде, полезно разобрать его на части: что в нём общее и версионируется, что принадлежит конкретному запущенному контейнеру, а что остаётся личной работой человека. От этого зависит всё остальное: где лежат исходники, как делить процессор и память, что пропадёт при пересоздании и насколько безопасно открывать чужой код.
Что лежит в репозитории, а что живёт в контейнере
Общей частью является описание среды: файл конфигурации в каталоге .devcontainer, а рядом с ним, если нужно, Dockerfile или compose-файл. Это текст, он лежит в git, его можно ревьюить, откатывать и сравнивать между ветками. Если в нём сказано, какой базовый образ взять, какие пакеты поставить, какие расширения редактора включить и какую команду выполнить после создания контейнера, то у любого, кто запустит эту конфигурацию, получится одинаковая отправная точка.
Слово «отправная» здесь важно. Конфигурация описывает, как создать среду, а не гарантирует, что она останется такой же через полгода. Если в Dockerfile написано FROM node:lts или pip install requests без версии, два контейнера, собранные в разные дни, могут разойтись. Именно это и случилось с нашими двумя разработчиками, только в более аккуратной упаковке. Одинаковость появляется не от самого слова «контейнер», а от того, что в описании зафиксировано:
- базовый образ закреплён конкретным тегом, а для строгой воспроизводимости ещё и дайджестом;
- зависимости проекта ставятся из lock-файла (
package-lock.json,poetry.lock,go.sumи аналоги), который тоже лежит в репозитории; - системные пакеты имеют зафиксированные версии или хотя бы осознанно принимаются «как есть»;
- команды первичной настройки идемпотентны: повторный запуск не ломает результат.
Ко второй части относится состояние самого контейнера: что в нём установили руками, какие файлы появились вне проекта, какие процессы запущены, какая история команд накопилась в оболочке. Всё это живёт в слое записи контейнера и исчезает вместе с ним, если не вынесено в том. Третья часть, личная, это то, что разработчик делает поверх: свои ветки, незакоммиченные правки, настройки редактора, ключи, алиасы. Общей средой это быть не должно, и хорошая конфигурация сознательно оставляет для этого место, а не пытается всё унифицировать.
Простой тест для любой строчки в вашей настройке: «если контейнер удалят сегодня ночью, что я потеряю?» Если ответ «ничего, всё восстановится из репозитория», строчка на своём месте. Если ответ «полдня работы», значит, личное состояние спрятано там, где его никто не защищает.
Где на самом деле лежат исходники
Самое частое недоразумение при переходе на удалённый Docker звучит так: «у меня в конфигурации указан bind mount текущей папки, значит, мой код окажется в контейнере на сервере». Это неверно. Согласно документации VS Code по работе с удалённым Docker-хостом, bind mount локальной файловой системы не переносится автоматически в контейнер на удалённом Docker host (VS Code: Develop on a remote Docker host). Монтирование выполняет демон Docker, а демон работает на сервере и видит файловую систему сервера, а не вашего ноутбука. Путь /home/anna/project с ноутбука либо не существует на сервере, либо, что хуже, существует и указывает на что-то совсем другое.
Поэтому путь к исходникам нужно выбрать явно. По документации есть два основных варианта: удалённая папка на самом хосте либо Docker volume. Рассмотрим, чем они отличаются на практике.
| Признак | Папка на сервере | Docker volume |
|---|---|---|
| Где лежит код | В домашнем каталоге пользователя на VPS | В томе, которым управляет Docker |
| Как подключается редактор | Remote SSH открывает папку, затем Dev Containers пересобирает среду вокруг неё | Репозиторий клонируется прямо в том, а контейнер монтирует его |
| Видимость из оболочки сервера | Файлы доступны обычными командами и инструментами резервного копирования | Нужно обращаться через Docker или временный контейнер |
| Что происходит при удалении контейнера | Код остаётся на диске | Код остаётся, пока не удалён том |
| Права на файлы | Определяются владельцем на хосте и пользователем в контейнере, возможны расхождения UID | Обычно проще, так как том создан под контейнер |
Для команды, где у каждого разработчика своя учётная запись на сервере, вариант с папкой обычно нагляднее: у Анны код лежит в её домашнем каталоге, у Бориса в его, и это видно любому администратору. Remote SSH и Dev Containers, как сказано в той же документации, можно использовать вместе для папки на удалённом хосте: сначала редактор подключается по SSH, потом открывает папку и предлагает переоткрыть её в контейнере. Том удобнее, когда важна скорость файловых операций и не хочется возиться с владельцами файлов, но тогда у кода появляется «невидимое» место хранения, и о нём нужно помнить отдельно.
Есть и второй способ связать редактор с сервером: направить локальный клиент Docker на удалённый демон через SSH. Документация отмечает, что такое подключение использует SSH-аутентификацию. Это удобно тем, что отдельных паролей не появляется, но у способа есть цена, о которой поговорим ниже, когда дойдём до границ доверия: доступ к демону даёт гораздо больше, чем кажется.
Один сервер, несколько человек: как поделить процессор и память
Теперь представим, что на одном VPS работают четыре разработчика, и у каждого свой контейнер. Самый частый сценарий конфликта: один запускает полную сборку или тяжёлый набор тестов, и редактор у остальных начинает подтормаживать, а языковой сервер перезапускается по таймауту. Железо при этом исправно, просто ресурсы никто не разделил.
Контейнеры изолируются средствами ядра: по документации Docker, в изоляции участвуют namespaces и cgroups (Docker Engine security). Namespaces отвечают за то, что именно контейнер «видит» (процессы, сеть, точки монтирования), а cgroups позволяют ограничивать и учитывать ресурсы. Из этого следует практический вывод: по умолчанию контейнер может потреблять столько, сколько доступно хосту, и лимиты нужно задавать осознанно. В Dev Container это делается через параметры запуска, например в runArgs, либо через раздел ресурсов compose-файла.
{
"name": "project-dev",
"build": { "dockerfile": "Dockerfile" },
"runArgs": ["--cpus=2", "--memory=4g", "--memory-swap=4g"]
}Здесь две вещи заслуживают пояснения. Ограничение по процессору через --cpus задаёт долю процессорного времени: контейнер с двойкой может использовать эквивалент двух ядер, но не больше. Ограничение по памяти жёстче: при превышении процесс внутри контейнера будет завершён ядром. Это неприятно, но предсказуемо: падает один контейнер разработчика, а не весь сервер вместе с чужими сессиями. Установка --memory-swap равным --memory запрещает контейнеру уходить в своп сверх лимита, иначе вместо быстрого падения вы получите медленную агонию всей машины.
Пример расчёта на четверых
Возьмём условный пример, не привязанный к измерениям. Допустим, у нас сервер с 8 vCPU и 16 ГБ RAM, а работают четыре человека. Часть ресурсов нужна самой системе, демону Docker, SSH и фоновым службам, поэтому резервируем условно 1 vCPU и 2 ГБ. Остаётся 7 vCPU и 14 ГБ.
- Строгое деление. Каждому по 1,5 vCPU и 3 ГБ. Предсказуемо, никто никому не мешает, но пиковая сборка у любого идёт медленнее, чем могла бы, пока соседи простаивают.
- Переподписка по процессору. Каждому
--cpus=4при общих 7 доступных. Пока работают один-два человека, всем хорошо; если сборки совпали, процессор делится, и все замедляются, но никто не падает. Для процессора это обычно приемлемо. - Строгая память, мягкий процессор. Память делим жёстко (по 3,5 ГБ), процессор переподписываем. Именно такая комбинация чаще всего оказывается разумной: процессорное время можно «растянуть», а память нет.
Эти цифры иллюстрируют ход рассуждения, а не обещают, что именно такая схема подойдёт вашему проекту. Реальную потребность показывает замер: сколько памяти занимает ваша полная сборка, сколько тянет языковой сервер на вашем объёме кода, сколько нужно базе данных для тестов. Публичные тарифы MAATRIX для ориентира: Ba (1 vCPU, 2 ГБ RAM, 40 ГБ NVMe), Uab (2 vCPU, 4 ГБ, 80 ГБ), Heka (4 vCPU, 8 ГБ, 160 ГБ), Maat (8 vCPU, 16 ГБ, 240 ГБ), Djed (16 vCPU, 32 ГБ, 320 ГБ). Это параметры тарифов, а не обещание ёмкости: сколько человек реально уместится, определяется вашей нагрузкой, и проверять это нужно на своих сборках. Заметьте и про диск: образы, слои сборки и тома нескольких разработчиков растут быстро, поэтому объём NVMe в таком сценарии расходуется не только кодом, но и кэшем сборки.
Есть и вторая стратегия: не делить один сервер на всех, а выдать каждому небольшой отдельный VPS либо разделить команду на два сервера по проектам. Это дороже по числу машин, зато убирает соседство и упрощает вопрос доступа. Выбор зависит от того, как часто пики совпадают и насколько вам важна простота администрирования одного хоста.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.
Что исчезнет, когда контейнер пересоздадут
Пересоздание контейнера, то есть «Rebuild Container», происходит чаще, чем кажется: изменили Dockerfile, обновили базовый образ, поменяли devcontainer.json, перезагрузили сервер с очисткой. В этот момент разработчик вдруг обнаруживает, что было в контейнере «просто так». Разберём по слоям, что переживёт пересоздание, а что нет.
| Где лежит | Переживёт пересоздание? | Комментарий |
|---|---|---|
| Код в папке на хосте или в томе | Да | Включая незакоммиченные правки, пока папку или том не удалили |
Слой записи контейнера (пакеты, установленные вручную, файлы в /tmp, /usr/local) | Нет | Это и есть «состояние конкретного контейнера» |
| Домашний каталог пользователя в контейнере | Нет, если не вынесен в том | История оболочки, локальные конфиги инструментов, кэши |
| Расширения редактора на стороне сервера | Переустанавливаются из конфигурации | Если перечислены в devcontainer.json, вернутся сами |
| Данные тестовой базы внутри контейнера | Нет | Для долгоживущих данных нужен отдельный том |
Из таблицы следует главное правило: незакоммиченная работа должна лежать в месте, которое не зависит от жизни контейнера. Если исходники в папке на хосте или в томе, пересоздание им не угрожает. Опасны другие вещи, менее заметные. Разработчик однажды поставил руками утилиту через apt install, настроил под неё скрипт и забыл. После пересборки утилиты нет, скрипт падает, а коллеги не понимают почему: у них она не нужна или ставится иначе.
Здесь и проявляется ценность «общей» части. Каждый раз, когда кто-то говорит «у меня работает, потому что я доустановил то-то», это сигнал, что описание среды неполно. Правильное действие: добавить установку в Dockerfile или в команду после создания, закоммитить и тем самым превратить личный фокус в общее знание. Тогда пересоздание перестаёт быть страшным: оно становится проверкой, что описание действительно самодостаточно. Многие команды устраивают такую проверку намеренно: раз в какое-то время пересобирают контейнер с нуля на чистом кэше и смотрят, запускаются ли тесты.
Для личных вещей, которые стоит сохранять, есть аккуратные варианты. Историю оболочки и пользовательские конфиги инструментов можно вынести в именованный том, привязанный к домашнему каталогу. Персональные настройки редактора синхронизируются средствами самого редактора, а не через общий devcontainer.json. Дотфайлы (алиасы, конфигурация оболочки) разумнее держать в собственном репозитории и подтягивать при создании контейнера, если ваш редактор это поддерживает, чем вписывать в общую конфигурацию вкусы одного человека.
Отдельно о данных, которые контейнер порождает в ходе работы: дампы базы, загруженные фикстуры, артефакты сборки. Если их можно восстановить одной командой из репозитория, не храните. Если нельзя, это уже не среда разработки, а данные, и у них должен быть собственный способ хранения и копирования. Не рассчитывайте, что платформа сделает копии за вас: в публичных материалах MAATRIX наличие автоматических резервных копий во всех тарифах не подтверждено, поэтому всё, что нельзя потерять, копируйте сами.
Открыть папку и выполнить код: одно и то же
Теперь о неприятном. Когда редактор предлагает «Reopen in Container», кажется, что вы просто открываете папку. На самом деле конфигурация из репозитория определяет, какой образ собирать, какие команды выполнять при создании, какие тома и устройства подключать, с какими привилегиями запускать процесс. Любая из команд жизненного цикла, например выполняемая после создания контейнера, запускается автоматически. Если репозиторий чужой, вы позволяете его автору выполнить команды у себя на сервере.
Dev Container не является автоматической безопасной песочницей. Документация Docker говорит об этом прямо в терминах границ доверия: изоляцию обеспечивают namespaces и cgroups, но итоговая картина зависит от конфигурации контейнера и дополнительных возможностей процесса (Docker Engine security). Если в конфигурации включены привилегированный режим, дополнительные capabilities, проброс сокета Docker или монтирование каталогов хоста, граница между контейнером и сервером становится тоньше. А общий смысл такой: контейнер без этих послаблений изолирован лучше, чем процесс, запущенный прямо на хосте, но «лучше» не означает «безопасно для чего угодно».
Самое чувствительное место здесь демон Docker. Та же документация указывает, что доступ к rootful Docker daemon является существенной границей доверия: управление им может позволить смонтировать файловую систему хоста. Для команды отсюда вытекают конкретные последствия:
- Если разработчик подключается к удалённому демону по SSH и его пользователь состоит в группе
docker, он фактически получает права, сопоставимые с root на сервере, пусть и косвенно. Такой доступ нужно выдавать так же осторожно, как root. - Общий сервер с общим rootful-демоном означает, что один скомпрометированный контейнер или одна неосторожно открытая конфигурация потенциально затрагивает чужие каталоги и контейнеры остальных.
- Для неизвестных репозиториев разумнее отдельная машина без чужих данных и без общих ключей, которую не жалко пересоздать, чем ваш рабочий сервер команды.
Перед открытием незнакомого репозитория стоит просмотреть содержимое .devcontainer глазами, а не мышкой. На что смотреть: поля privileged, capAdd, securityOpt, mounts и runArgs; команды initializeCommand, onCreateCommand, postCreateCommand и им подобные; Dockerfile, где скрипты скачиваются и сразу исполняются; подключение томов с хоста, особенно домашних каталогов и сокета Docker. Если вы не готовы прочитать всё это, не запускайте на общем сервере. Хорошая командная привычка: изменения в .devcontainer проходят ревью с той же строгостью, что и изменения в скриптах развёртывания, потому что по сути это они и есть.
Доступ без общей учётной записи
Всё сказанное сходится на вопросе: как человек попадает на сервер. Самый короткий путь — завести один пользователь dev с одним паролем в чате. Он же и самый вредный: нельзя понять, кто что запустил, нельзя отозвать доступ одного человека, у всех одно рабочее дерево, и чужие незакоммиченные правки становятся вашей проблемой.
Правильнее выдать каждому собственную учётную запись и вход по SSH-ключу. VPS MAATRIX предоставляет root-доступ, поэтому создание пользователей и настройка SSH в ваших руках. Публичный ключ человека добавляется в его authorized_keys, закрытый остаётся у него. Раз подключение к удалённому демону использует SSH-аутентификацию, то ровно те же ключи работают и для редактора, и для Docker, и отдельных паролей заводить не нужно. Уход сотрудника сводится к удалению одной учётной записи или одного ключа.
Дальше нужно выбрать, как отделить рабочие деревья. Каждый хранит клон репозитория в собственном домашнем каталоге (или в собственном томе) и запускает собственный контейнер с собственным именем. Тогда ветки и незакоммиченные правки не пересекаются, а общим остаётся только то, что и должно быть общим: описание среды в git. Проблему доверенного демона это не снимает полностью, и если вам нужна более жёсткая граница, рассмотрите отдельные экземпляры Docker для пользователей или отдельные серверы для групп, но для доверенной команды, которая работает над одним проектом, схема «учётная запись на человека плюс ключи» уже сильно лучше общего входа.
Если ваша команда работает с зарубежными сервисами или тестовыми окружениями и вам важна локация сервера, у MAATRIX в публичном предложении перечислены UK, US, RU и FR; точное наличие образов и ОС в выбранной локации проверяется в форме заказа. О том, зачем может понадобиться сервер в конкретной стране для разработки и тестирования, есть отдельный материал: VPS в США для разработки и тестирования.
Новый сотрудник в понедельник утром
Соберём всё в один понятный сценарий. В команду приходит Дарья. У неё нет доступа на сервер, нет общего пароля и не должно быть чужого рабочего дерева. Что происходит.
- Дарья генерирует пару SSH-ключей на своём ноутбуке и отправляет администратору только публичный ключ.
- Администратор с root-доступом создаёт на VPS её учётную запись, добавляет ключ и включает её в нужные группы, осознавая, что членство в группе
dockerравнозначно очень высоким правам. Если такой уровень доверия для новичка неприемлем, ей выдают отдельный экземпляр или отдельный сервер. - Дарья подключается к серверу через Remote SSH, клонирует репозиторий в собственный каталог и открывает эту удалённую папку. Путь к исходникам однозначен: это папка на хосте в её домашнем каталоге, а не волшебным образом перенесённый локальный bind mount.
- Редактор находит
.devcontainerи предлагает переоткрыть папку в контейнере. Перед этим Дарья, как и любой, кто принимает новую конфигурацию, пробегает глазами изменения в этом каталоге: привилегии, монтирования, команды запуска. - Контейнер собирается из зафиксированного образа, зависимости ставятся из lock-файла, расширения подтягиваются из конфигурации, лимиты процессора и памяти применяются из
runArgs. Через несколько минут у Дарьи те же версии инструментов, что у остальных. - Она создаёт свою ветку, правит код, коммитит. Если контейнер завтра пересоздадут, правки останутся в её папке, а всё установленное вручную придётся добавить в общее описание, и это нормально: так среда становится лучше для следующего новичка.
Если при первом запуске у неё что-то не работает, у команды есть редкая роскошь: проблема воспроизводима. Не «у меня почему-то не ставится», а конкретный Dockerfile и конкретный шаг, на котором он падает. Исправление идёт коммитом в общий репозиторий и достаётся всем.
Для такой схемы подойдёт обычный VPS с достаточным запасом CPU, памяти и NVMe-диска под образы и кэши, причём подбирать размер стоит по замерам ваших сборок, а не по числу участников. Если вы собираетесь запускать такую среду на отдельной машине, аренда VPS MAATRIX с NVMe, AMD EPYC и root-доступом даёт нужную основу: оплата картой РФ и СБП, автоматическое создание сервера через пару минут после оплаты. Всё остальное, от пользователей до лимитов и правил ревью конфигурации, остаётся вашим описанием среды, лежащим в git рядом с кодом.