Generic selectors
Exact matches only
Search in title
Search in content
Post Type Selectors

DeepSeek Harness на сервере: установка, OpenRouter, systemd

Linux и DevOps

DeepSeek Harness (dsh) — открытый каркас для агентов, построенный на Cordis по принципу «всё есть плагин»: модели, инструменты, навыки, сессии, песочницы, файловые системы и сам интерфейс собираются как подключаемые слои. Лицензия MIT, состояние — предварительный выпуск для разработчиков с заявленными ломающими изменениями.

Локальный запуск сводится к одной команде npx @deepseek-ai/dsh web. Задача этой статьи другая: постоянно работающий узел на удалённой машине, доступный по домену с сертификатом и паролем, с моделями через OpenRouter и службой systemd.

Каркас собирается из исходников. При предварительном выпуске это единственный способ получить состояние ветки main раньше публикации в реестре npm, закрепиться на конкретной ревизии и при необходимости внести собственные правки. Готовый пакет из npm отстаёт и правок не допускает.

Наружу узел публикуется через Nginx Proxy Manager: сертификат, принудительный HTTPS, поддержка веб-сокетов и список доступа с паролем настраиваются в веб-интерфейсе, а не в конфигурационных файлах.

Конфигурация проверена на Ubuntu 26.04 LTS. Команды рассчитаны на root-сессию, поэтому sudo в них не используется.

Предварительный выпуск не предназначен для промышленной эксплуатации. Развёртывание оправдано там, где поломка после обновления остаётся неприятностью, а не происшествием.

Содержание
  1. Что определяет схему установки
  2. Сервер слушает только петлю
  3. Токен входа не заменяет авторизацию
  4. Режим workspace-write ограничивает только запись
  5. Каталог запуска считается рабочим корнем
  6. Шаг 1. Node.js и подготовка машины
  7. Память под сборку
  8. Если сборка падает по куче
  9. Шаг 2. Отдельный пользователь и каталоги
  10. Пользовательский префикс npm
  11. Шаг 3. Сборка из исходников
  12. Установочные сценарии зависимостей
  13. Закрепление ревизии
  14. Чем это отличается от npx и глобального пакета
  15. Шаг 4. Модели через OpenRouter
  16. Конфигурация через файл настроек
  17. Конфигурация через интерфейс
  18. Что недоступно без ключа DeepSeek
  19. Шаг 5. Служба systemd
  20. Разбор ключевых директив
  21. Шаг 6. Полные права агенту
  22. Зачем это нужно
  23. Защитные ключи юнита
  24. Проверка на Playwright
  25. Песочница: почему bash вообще отказывается выполняться
  26. Доступ к Docker
  27. Шаг 7. Публикация через Nginx Proxy Manager
  28. 7.1. Переброска порта на машине с dsh
  29. 7.2. Межсетевой экран и Fail2Ban
  30. 7.3. Узел в Nginx Proxy Manager
  31. 7.4. Если заслон доверия всё равно отклоняет запросы
  32. 7.5. Список доступа — тот самый пароль перед портом
  33. Когда домен не требуется
  34. Шаг 8. Вход по токену и диагностика по кодам ответа
  35. Коды ответа указывают на слой обрыва
  36. Шаг 9. Первая настройка интерфейса
  37. Дополнения: как выбирать и в каком порядке ставить
  38. Механика установки
  39. Минимальный набор для серверной установки
  40. Почему dsh-passwords здесь не нужен
  41. Что на сервере не нужно
  42. Рекомендуемый порядок
  43. Обновление без сюрпризов
  44. Когда достаточно пакета из npm
  45. Сводная таблица ошибок
  46. Безопасность и телеметрия
  47. Заключение

Что определяет схему установки

Четыре особенности каркаса задают всю дальнейшую конфигурацию. Пропуск любой из них приводит к типовым ошибкам, разобранным ниже.

Сервер слушает только петлю

Веб-приложение принимает --host, --port и повторяемый --trusted-host, но значение --host 0.0.0.0 умышленно не поддерживается и завершается ошибкой использования. По умолчанию адрес прослушивания — http://127.0.0.1:3080.

Отсюда главное следствие для публикации: Nginx Proxy Manager, работающий на другой машине, до этой петли не дотянется. На машине с dsh обязано слушать внешний адрес что-то своё — об этом шаг 7.

Токен входа не заменяет авторизацию

Интерфейс закрыт токеном, который печатается в журнал при запуске и меняется при каждом перезапуске. Без токена сервер отвечает 401 на любой запрос, включая curl с самой машины.

Однако это одноразовый пропуск, а не учётные записи и не разграничение прав: обладатель ссылки получает полный доступ и расходует средства на моделях. Список доступа в Nginx Proxy Manager обязателен.

Режим workspace-write ограничивает только запись

Новые сессии по умолчанию работают в режиме workspace-write: изменения через bash и файловую систему ограничены рабочим каталогом сессии и временными каталогами. Чтение, сетевой доступ и видимость процессов не ограничены.

Агент способен прочитать файл с учётными данными и отправить его во внешнюю сеть. Это заявленное поведение, а не изъян, — и именно оно определяет требования к изоляции машины.

Каталог запуска считается рабочим корнем

Во всех режимах рабочим корнем по умолчанию становится каталог запуска. Отсюда требование отдельного пустого каталога вместо домашнего каталога пользователя.

Итоговый план: отдельный системный пользователь с собственным префиксом npm, сборка из репозитория, служба systemd с защитными ключами, осознанно выданные права на Docker, переброска порта под межсетевым экраном, узел в Nginx Proxy Manager и только после этого — дополнения.

Шаг 1. Node.js и подготовка машины

Базовая установка среды выполнения из репозитория NodeSource:

apt update && apt install -y curl git build-essential
curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
apt install -y nodejs
npm install -g pnpm@10
node -v && pnpm -v

Сразу после установки проверяется, какие именно исполняемые файлы оказались в PATH:

which -a node
command -v pnpm

Проверка не избыточна. Если на машине уже присутствовал Node, установленный не через apt (типичный случай — /usr/local/bin), после NodeSource в системе окажутся две версии: /usr/bin/node из пакета и другая, стоящая раньше в PATH. Команда node -v покажет вторую, сборка пойдёт на ней же, а pnpm установится рядом с ней — не в /usr/bin.

Записанный по привычке путь /usr/bin/pnpm в юните systemd даст ошибку запуска:

dsh.service: Unable to locate executable '/usr/bin/pnpm': No such file or directory

Оба выведенных пути понадобятся при составлении юнита systemd на шаге 5. Их удобно сохранить сразу.

Пакет build-essential нужен зависимостям с нативной сборкой — они есть и у самого каркаса, и у части плагинов.

Память под сборку

Сборка дерева TypeScript — самая требовательная к памяти часть установки. Порядок такой: сначала посмотреть, что есть, и только потом что-то добавлять.

free -h
swapon --show

Если свободно от 4 ГБ, делать нечего — сборка пройдёт как есть. Подкачка нужна только на небольших машинах, где памяти меньше и раздел подкачки отсутствует:

fallocate -l 4G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
free -h

Если сборка падает по куче

Отдельный случай — ошибка JavaScript heap out of memory при формально свободной памяти. Node сам выбирает потолок кучи, исходя из объёма машины, и на скромной конфигурации выбирает скромно: процесс упирается в предел раньше, чем в физическую память.

Внешний флаг для этого не нужен: сборочный сценарий репозитория сам запускает компилятор строкой node --max-old-space-size=4096, и переменная NODE_OPTIONS снаружи лишь продублирует уже заданное значение. Если четырёх гигабайт потолка не хватает физически, помогает подкачка, а не флаг. Текущий потолок при необходимости смотрится так:

node -e 'console.log(require("v8").getHeapStatistics().heap_size_limit / 1048576 + " MB")'

Шаг 2. Отдельный пользователь и каталоги

Служба работает не от root, хотя на шаге 6 этому пользователю выдаются полные права. Смысл здесь не в изоляции, а в порядке: у агента собственные каталоги, собственный префикс npm и понятная принадлежность файлов — в журналах и в ps сразу видно, где работает он, а где вы.

adduser --system --group --home /opt/dsh --shell /bin/bash dsh
mkdir -p /opt/dsh/work /opt/dsh/.dsh
chown -R dsh:dsh /opt/dsh

Оба каталога обязательны: work — рабочий корень сессий, .dsh — будущий $DSH_HOME для настроек, учётных данных и профилей. При создании только первого запись settings.yaml на шаге 4 завершится ошибкой No such file or directory.

Пользовательский префикс npm

Агенту требуется ставить пакеты. Установка в проект (npm install, pnpm add) прав root не требует вообще: зависимости попадают в node_modules рабочего каталога. Для глобальных установок задаётся пользовательский префикс, иначе они растекутся по системным каталогам и перемешаются с пакетами дистрибутива:

sudo -u dsh -H bash -lc '
mkdir -p /opt/dsh/.npm-global
npm config set prefix /opt/dsh/.npm-global
echo "export PATH=/opt/dsh/.npm-global/bin:\$PATH" >> /opt/dsh/.profile
npm config get prefix
'

Строка дописывается именно в .profile, а не в .bashrc. Пользователь создан ключом --system, поэтому образцы из /etc/skel в его домашний каталог не скопированы и .bashrc ничем не подключается. Все команды в статье идут через bash -lc, то есть через оболочку входа, а она читает .profile. Ошибка проявляется позже и обманчиво: пакет установлен, а программа отвечает command not found.

Каталог /opt/dsh/.npm-global/bin обязательно попадает в PATH юнита на шаге 5, иначе служба не увидит программ, установленных агентом. Пакеты Python подключаются тем же способом — pip install --user или uv в домашнем каталоге.

Шаг 3. Сборка из исходников

Репозиторий клонируется в домашний каталог пользователя dsh, а не в рабочий корень сессий: смешивать исходники каркаса с каталогом, куда пишет агент, не стоит.

sudo -u dsh -H bash -lc '
cd /opt/dsh
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
pnpm run build
'

Клон весит около 135 МБ, установка тянет примерно тысячу пакетов, среди которых есть увесистые вложенные программы: сборки Codex и Claude Agent SDK занимают по сотне мегабайт каждая. Установка укладывается в минуту, сборка — в несколько.

В выводе появляются предупреждения, на результат не влияющие: циклические зависимости рабочего пространства, неподдерживаемая платформа linux-arm64 у одного из вложенных пакетов и пара строк вида Failed to create bin … apps/cli/lib/bin.js. Последние выглядят пугающе, но объясняются просто: ссылка на исполняемый файл создаётся до сборки, а сам файл появляется после неё.

Сборка обязательна: рабочий запуск веб-приложения требует собранных пакетов и артефактов интерфейса, без них загрузка профиля падает на ошибках разрешения модулей. Проверка результата:

sudo -u dsh -H bash -lc 'cd /opt/dsh/deepseek-harness && pnpm dsh --version'

Установочные сценарии зависимостей

pnpm 10 и новее по умолчанию не выполняет сценарии install и postinstall у зависимостей — разрешение объявляется списком onlyBuiltDependencies в pnpm-workspace.yaml. В репозитории каркаса этот список уже заполнен, поэтому вмешательства не требуется: в выводе видны строки Running install script для node-pty и koffi и Running postinstall script для subprocess-local. Именно они собирают нативный модуль терминала, обвязку к системным библиотекам и вспомогательную программу запуска дочерних процессов.

Проверять нужно обратное — отсутствие этих строк и предупреждение об игнорированных сборках. Тогда инструменты, работающие через оболочку, откажут уже в сессии при формально успешной сборке. Разрешение выдаётся интерактивно и установка повторяется:

sudo -u dsh -H bash -lc '
cd /opt/dsh/deepseek-harness
pnpm approve-builds
pnpm install
'

Это заметное отличие от установки готовым пакетом: там разрешение приходится выдавать самому — npm блокирует те же сценарии и печатает предупреждение install-scripts с готовой строкой --allow-scripts. В репозитории решение уже принято за вас.

Закрепление ревизии

При предварительном выпуске main меняется ежедневно, и обновление вслепую однажды приходится на ломающее изменение. Рабочий узел закрепляется на конкретной ревизии:

sudo -u dsh -H bash -lc '
cd /opt/dsh/deepseek-harness
git log --oneline -5
git rev-parse HEAD
'

Выведенный хеш записывается рядом с остальными сведениями об узле: по нему выполняется откат, если очередное обновление окажется неудачным. Ветка в качестве ориентира не годится — она движется.

Команда apt install dsh ставит посторонний пакет — распределённую оболочку для параллельного выполнения команд на группе машин. Совпадает только имя.

Чем это отличается от npx и глобального пакета

README предлагает два пути: npx @deepseek-ai/dsh web и запуск из клона. Первый годится для разовой пробы на своей машине — версия не закреплена, кеш подтягивается из реестра, для постоянной службы это лишняя зависимость от доступности npm. Есть и третий, в README не описанный: npm install -g @deepseek-ai/dsh, поскольку в манифесте объявлено поле bin. Он проще сборки и обновляется одной командой, но даёт только опубликованную версию — на предварительном выпуске она заметно отстаёт от ветки и не допускает собственных правок.

Разрыв виден прямо в выводе сборки: пакеты рабочего пространства собираются под версией 0.1.2-alpha.2, тогда как в реестре на тот же день лежит 0.1.1-rc.2.

Сборка выбрана именно поэтому: свежий код, возможность закрепиться на ревизии, возможность править каркас. Плата — минуты ожидания и обязательный шаг сборки после каждого обновления.

Шаг 4. Модели через OpenRouter

OpenRouter не входит в готовый набор поставщиков. Штатно вводом ключа настраиваются DeepSeek, Anthropic и OpenAI; Bedrock, Vertex, Azure и Codex требуют родных учётных данных, и одного поля ключа для них недостаточно.

OpenRouter добавляется как пользовательский поставщик. Минимальный набор: идентификатор строчными буквами, базовый адрес, протокол, учётные данные и хотя бы одна модель.

Конфигурация через файл настроек

sudo -u dsh -H bash -c 'umask 077; cat > /opt/dsh/.dsh/settings.yaml <<EOF
llm-pi-ai:
  providers:
    openrouter:
      apiKeyEnv: OPENROUTER_API_KEY
      api: openai-completions
      baseURL: https://openrouter.ai/api/v1
      compat:
        supportsDeveloperRole: false
        maxTokensField: max_tokens
      models:
        - id: z-ai/glm-5.3-flash
        - id: anthropic/claude-sonnet-4-5
        - id: google/gemini-2.5-pro
          input: [text, image]
EOF'
sudo -u dsh -H bash -c 'umask 077; cat > /opt/dsh/.dsh-env <<EOF
OPENROUTER_API_KEY=sk-or-v1-ключ
DSH_HOME=/opt/dsh/.dsh
DSH_TELEMETRY_DISABLED=1
EOF'

Идентификаторы моделей указываются ровно как у OpenRouter, без приставки openrouter/. Приставка требуется там, где запрос идёт через LiteLLM; здесь обращение к совместимому с OpenAI адресу прямое, и лишний префикс даст UNKNOWN_MODEL.

Ключи compat. Именно с них рекомендуется начинать, если шлюз отвергает запросы при верных ключе и адресе. У модели, объявленной рассуждающей, системная подсказка уходит в роли developer, а ограничение вывода передаётся как max_completion_tokens, и не всякий совместимый шлюз это принимает.

Пустое значение после двоеточия в compat недопустимо: такой ключ отвергается, а не игнорируется, поскольку пустота стёрла бы сведения из каталога, ничего не предложив взамен.

Параметр input. Модель, введённая вручную, считается только текстовой, пока не указано иное: опросить адрес о поддерживаемых видах содержимого невозможно. Попытка приложить изображение к такой модели отклоняется до отправки запроса.

Порядок поиска учётных данных: унаследованное окружение → $DSH_HOME/.credentials.yaml → файл .env каталога запуска → $DSH_HOME/.env.

Конфигурация через интерфейс

Тот же результат достигается позже через Settings → Models → Add a custom provider: Provider ID openrouter, базовый адрес https://openrouter.ai/api/v1, протокол openai-completions, ключ и Fetch available models. Опрос идёт по совместимому с OpenAI GET /models, у OpenRouter такой метод присутствует.

Ключ записывается только в одну сторону: после сохранения страница получает замаскированное описание, а не исходное значение.

Идентификатор поставщика постоянен — на него ссылаются запросы, сохранённые сессии, модели по умолчанию и ссылки на учётные данные. Переименование не поддерживается, доступно только создание нового и удаление старого.

Интерфейс пишет в тот же settings.yaml. Рекомендуется выбрать один способ конфигурации, а вручную дописывать только то, чего нет в форме: compat и input.

Что недоступно без ключа DeepSeek

Встроенный поиск использует DEEPSEEK_API_KEY и принимает DEEPSEEK_SEARCH_BASE_URL. Модели через OpenRouter работают, а инструмент web_search — нет. Инструмент web_fetch отключён в любом случае, пока слой правок не добавит поставщика и не включит его.

Если поиск нужен, отдельный ключ DeepSeek только под эту функцию обходится дешевле полного перехода на родного поставщика.

Шаг 5. Служба systemd

Сначала фиксируются фактические пути, определённые на шаге 1 — их подставит heredoc без кавычек. Домен в --trusted-host указывается тот же, что будет заведён в Nginx Proxy Manager на шаге 7:

PNPM_BIN="$(command -v pnpm)"
NODE_DIR="$(dirname "$(command -v node)")"
echo "pnpm=$PNPM_BIN  node_dir=$NODE_DIR"
cat > /etc/systemd/system/dsh.service <<EOF
[Unit]
Description=DeepSeek Harness web
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=dsh
Group=dsh
WorkingDirectory=/opt/dsh/work
Environment=PATH=/opt/dsh/.npm-global/bin:$NODE_DIR:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
EnvironmentFile=/opt/dsh/.dsh-env
ExecStart=$PNPM_BIN --dir /opt/dsh/deepseek-harness dsh web --port 3080 --no-open --trusted-host dsh.example.com
Restart=on-failure
RestartSec=5
TimeoutStopSec=15
KillSignal=SIGTERM

NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=read-only
ReadWritePaths=/opt/dsh

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now dsh
sleep 25
systemctl status dsh --no-pager
curl -sI http://127.0.0.1:3080 | head -1

Ожидаемый ответ — HTTP/1.1 401: без токена каркас отказывает, и это признак работающей службы, а не ошибки.

Пауза перед проверкой не запас вежливости, а необходимость. Запускающий сценарий поднимает исходники через tsx, и от старта до готовности проходит порядка пятнадцати секунд. В этом окне порт уже слушает, но маршруты ещё не зарегистрированы, и curl возвращает 404 Not Found — состояние, которое легко принять за поломку настроек. Через двадцать-тридцать секунд ответ меняется на 401. Ссылка с токеном появляется в журнале примерно тогда же.

Разбор ключевых директив

ExecStart через pnpm. Ключ --dir указывает на клон репозитория, поэтому рабочим каталогом службы остаётся /opt/dsh/work, а не каталог с исходниками. Путь к самому pnpm берётся из command -v, а не пишется по памяти: при двух установленных версиях Node он оказывается не в /usr/bin.

Environment=PATH — не косметика. У systemd собственный PATH, и без каталога Node запускающий сценарий pnpm со строкой #!/usr/bin/env node подхватит не ту версию, на которой выполнялась сборка. Первым в списке стоит /opt/dsh/.npm-global/bin из шага 2, иначе служба не найдёт программы, установленные агентом.

Защитные ключи (NoNewPrivileges, ProtectSystem, ProtectHome, ReadWritePaths) удерживают процесс службы в её каталоге. Первые два будут сняты на шаге 6 — без этого агент не сможет ставить системные пакеты; ReadWritePaths=/opt/dsh и остальные остаются, под ними находятся рабочий каталог сессий, исходники и префикс npm.

--trusted-host добавляет имена, принимаемые заслоном доверия браузера на /api. Без него интерфейс по домену открывается, но запросы к API отклоняются, и причина ошибочно ищется в настройках узла прокси. Флаг повторяемый: несколько доменов — несколько флагов.

Остановка. Дереву плагинов даётся до пяти секунд на разбор: первый SIGTERM начинает штатное завершение и выходит с кодом 0, второй сигнал завершает процесс немедленно. Значение TimeoutStopSec=15 не даёт systemd прервать завершение на середине.

Юнит вставляется исключительно конструкцией cat > файл <<'EOF'. Вставка содержимого секций напрямую в оболочку даёт серию ошибок вида [Unit]: command not found.

Шаг 6. Полные права агенту

Этот шаг выполняется после создания службы: команды ниже перезапускают юнит и проверяют доступ уже от работающего процесса.

Зачем это нужно

Части задач одних npm-пакетов мало. Показательный пример — Playwright: сам пакет и браузеры ставятся в домашний каталог и проблем не вызывают, а вот playwright install-deps тянет через apt системные библиотеки, без которых Chromium не запускается. То же касается сборки нативных модулей и любых утилит из репозиториев.

Узкий список путей в sudoers для этого не годится. Установщик вызывает не apt-get напрямую, а оболочку с командной строкой внутри, и sudo отклоняет её сообщением sudo: I'm sorry dsh. I'm afraid I can't do that. Добавлять в список /bin/sh бессмысленно — это тот же полный доступ, только менее очевидный. Да и сам apt-get с ключом -o позволяет задать настройку вида APT::Update::Pre-Invoke с произвольной командой от root. Поэтому права выдаются прямо:

cat > /etc/sudoers.d/dsh <<'EOF'
dsh ALL=(ALL) NOPASSWD: ALL
EOF
chmod 440 /etc/sudoers.d/dsh
visudo -c
sudo -u dsh sudo -n id

Последняя команда должна вывести uid=0(root).

Защитные ключи юнита

Одного правила недостаточно: ключи юнита блокируют повышение прав раньше, чем sudo дойдёт до проверки. NoNewPrivileges=true завершает sudo сообщением The 'no new privileges' flag is set, а ProtectSystem=full оставляет /usr доступным только для чтения даже для root внутри службы. Оба удаляются:

sed -i '/^NoNewPrivileges=true$/d; /^ProtectSystem=full$/d' /etc/systemd/system/dsh.service
systemctl daemon-reload
systemctl restart dsh
systemctl show dsh -p NoNewPrivileges -p ProtectSystem
sudo -u dsh sudo -n apt-get update

Ожидаются NoNewPrivileges=no, пустой ProtectSystem и успешное обновление списка пакетов. Ключи PrivateTmp, ProtectHome и ReadWritePaths остаются: работе apt они не мешают, а порядок в каталогах поддерживают.

После этого шага у агента root на машине. Отдельный пользователь, режим workspace-write и политика подтверждений держатся только на добросовестности того, что попадёт в контекст: одной вредоносной подсказки на открытой странице или одного плагина сомнительного происхождения достаточно, и команда выполнится с правами root.

Отсюда требование к остальному периметру: раз внутренние границы сняты, значение приобретают внешние — список доступа перед портом, межсетевой экран, пропускающий только адрес прокси, и осознанный отбор дополнений из шага «Дополнения». Машину с такой настройкой следует считать выделенной под агента: соседство с чужими рабочими средами, ключами поставщиков моделей и токенами хостинга репозиториев означает, что цена одной неудачной подсказки — все они разом.

Проверка на Playwright

sudo -u dsh -H bash -lc '
cd /opt/dsh/work/проект
npm i -D @playwright/test
npx playwright install-deps chromium
npx playwright install chromium
npx playwright screenshot --browser chromium https://example.com /tmp/t.png
'

Браузер ложится в /opt/dsh/.cache/ms-playwright и занимает около двухсот мегабайт. Полученный снимок означает, что цепочка рабочая: агент поставил системные зависимости сам и запустил браузер.

Песочница: почему bash вообще отказывается выполняться

Режим workspace-write не декларация, а работающее ограничение: каждая команда bash запускается внутри песочницы файловой системы. Строится она средствами операционной системы — bubblewrap или Landlock на Linux, sandbox-exec на macOS, ограниченный маркер доступа на Windows. Если ни один механизм не доступен, каркас отказывается выполнять команду:

sandbox mode "workspace-write" is requested but no sandbox backend
is usable on this host; refusing to run the command unconfined

Отказ обманчив тем, что выглядит как проблема прав или настроек сессии, хотя относится к ядру. Инструменты чтения при этом продолжают работать — они идут другим слоем, — и агент честно сообщает, что читать может, а выполнять нет.

Проверка, чего именно не хватает:

sysctl kernel.unprivileged_userns_clone user.max_user_namespaces kernel.apparmor_restrict_unprivileged_userns
grep -i landlock /boot/config-$(uname -r)

Типовая причина на Ubuntu — единица в kernel.apparmor_restrict_unprivileged_userns. Landlock при этом собран в ядре и стоит первым в CONFIG_LSM, пространства имён не урезаны, но создать их непривилегированному процессу AppArmor не даёт, и bubblewrap не поднимается. Лечится так:

sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
echo 'kernel.apparmor_restrict_unprivileged_userns=0' > /etc/sysctl.d/99-userns.conf
apt install -y bubblewrap
sudo -u dsh bwrap --ro-bind / / --dev /dev true && echo 'песочница работает'

Файл в /etc/sysctl.d закрепляет настройку после перезагрузки. Затем служба перезапускается, чтобы каркас заново определил доступные механизмы.

Проверяется песочница на живой сессии, а не по выводу команд: агенту даётся задание записать файл за пределами рабочего каталога, например touch /etc/proba. В режиме workspace-write он должен получить отказ. Созданный файл означает, что ограничение не действует.

Обратная сторона понятна из шага целиком: политика danger-full-access и подтверждения в положении never снимают песочницу вовсе. Вместе с полным sudo это означает, что любая команда из контекста выполняется немедленно и от root. Для узла, работающего без присмотра, песочницу стоит вернуть — она остаётся последним ограничителем.

Доступ к Docker

Отдельной настройки больше не требуется — с полным sudo агент добавит себя в группу сам. Если членство нужно сразу:

usermod -aG docker dsh
systemctl restart dsh
sudo -u dsh docker ps

В юните задан PrivateTmp=true, поэтому у службы собственный /tmp. Файл, положенный агентом в /tmp, демон Docker при монтировании не увидит. Каталоги сборки размещаются в /opt/dsh/work.

Шаг 7. Публикация через Nginx Proxy Manager

Nginx Proxy Manager закрывает разом четыре задачи, которых у dsh нет: имя, сертификат Let’s Encrypt с автоматическим продлением, принудительный HTTPS и пароль перед портом через список доступа. Настройка выполняется в веб-интерфейсе, файлы конфигурации nginx править не требуется.

Цепочка выглядит так: браузер → домен → узел в Nginx Proxy Manager → внешний адрес машины с dsh → переброска на 127.0.0.1:3080.

Последнее звено обязательно. Nginx Proxy Manager на другой машине до петли не дотянется физически, а dsh web внешний адрес не слушает. Это первая типовая ошибка при разнесённой инфраструктуре: узел заведён, домен отвечает, а в браузере 502.

7.1. Переброска порта на машине с dsh

Роль переброски — отдать петлю наружу на одном высоком порту, закрытом межсетевым экраном для всех, кроме адреса прокси. Достаточно socat:

apt install -y socat

cat > /etc/systemd/system/dsh-forward.service <<'EOF'
[Unit]
Description=dsh 3081 -> 127.0.0.1:3080
After=dsh.service
Requires=dsh.service

[Service]
ExecStart=/usr/bin/socat TCP-LISTEN:3081,fork,reuseaddr TCP:127.0.0.1:3080
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now dsh-forward
ss -tlnp | grep 3081

Служба привязана к dsh.service через Requires: остановка каркаса закрывает и внешний порт.

Если Nginx Proxy Manager работает на той же машине в контейнере, переброска не нужна вовсе — узел направляется на адрес мостовой сети Docker, а dsh остаётся на петле:

ip -4 addr show docker0

Полученный адрес (обычно 172.17.0.1) указывается в поле Forward Hostname / IP, порт — 3080. Значение 127.0.0.1 в этом поле не работает: внутри контейнера это его собственная петля.

7.2. Межсетевой экран и Fail2Ban

ufw allow proto tcp from АДРЕС_ПРОКСИ to any port 3081 comment 'dsh via npm'
ufw status numbered

Fail2Ban настраивается до того, как Nginx Proxy Manager начнёт обращаться к порту. Пока порт закрыт, повторяющиеся попытки прокси распознаются изолятором как сканирование, и его адрес попадает в бан. Запрещающее правило встаёт в ufw выше разрешающего — результатом становится 502 при полностью работоспособной службе.

sed -i 's|^ignoreip = .*|& АДРЕС_ПРОКСИ|' /etc/fail2ban/jail.local
fail2ban-client reload
grep ignoreip /etc/fail2ban/jail.local

Ключевая деталь: ignoreip предотвращает новые баны, но не снимает уже наложенный. Попавший в список адрес выпускается вручную:

fail2ban-client status ufw-scan
fail2ban-client set ufw-scan unbanip АДРЕС_ПРОКСИ

Ответ 1 означает снятый бан. Наличие десятков посторонних адресов в списке банов на машине хостера — норма, искать следует именно адрес прокси.

Правила ufw удаляются по содержимому, а не по номеру. Нумерация смещается по мере истечения банов Fail2Ban, и командой с устаревшим номером легко снести собственное разрешающее правило — с тем же результатом 502 и часом поисков.

7.3. Узел в Nginx Proxy Manager

Домен направляется A-записью на адрес машины с прокси. Далее Hosts → Proxy Hosts → Add Proxy Host.

Вкладка Details:

Поле Значение
Domain Names dsh.example.com — ровно то имя, что задано в --trusted-host
Scheme http — внутри цепочки шифрование не нужно
Forward Hostname / IP внешний адрес машины с dsh (или адрес моста docker, если прокси на той же машине)
Forward Port 3081 при переброске, 3080 при работе через мост
Cache Assets выключено — интерфейс отдаёт собственные заголовки
Block Common Exploits включено, но снимается первым при разборе отказов на /api
Websockets Support включено обязательно
Access List созданный список с паролем

Без поддержки веб-сокетов интерфейс откроется и даже примет запрос, но ответ модели не появится: страница держит постоянное соединение, и без него сессия не обновляется. Симптом выглядит как зависание, а не как ошибка.

Вкладка SSL: Request a new SSL Certificate, включить Force SSL и HTTP/2 Support, указать почту, согласиться с условиями Let’s Encrypt. HSTS включается по желанию и только после того, как домен заработал.

Работа допустима только по HTTPS. Веб-интерфейс использует Web Crypto API, доступный исключительно в защищённом контексте, и по обычному http страница падает с crypto.randomUUID is not a function. Force SSL обязателен, а не желателен.

Вкладка Advanced. Ответы модели приходят потоком и держат соединение минутами, поэтому стандартных таймаутов nginx не хватает — длинный ответ обрывается на середине:

proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_buffering off;
proxy_request_buffering off;
client_max_body_size 100m;

Отключённая буферизация здесь не тонкая настройка, а условие работы: с ней nginx копит ответ целиком и отдаёт его одним куском, из-за чего постепенное появление текста в интерфейсе пропадает.

7.4. Если заслон доверия всё равно отклоняет запросы

Бывает, что домен указан в --trusted-host, узел настроен верно, а интерфейс по-прежнему открывается пустым или отвечает отказом на /api. Тогда помогает приём, который стоит держать про запас: подменить заголовки так, будто запрос пришёл с самой машины.

location / {
    proxy_pass http://адрес_машины_dsh:3081;
    proxy_http_version 1.1;

    proxy_set_header Host 127.0.0.1:3080;
    proxy_set_header Origin http://127.0.0.1:3080;
    proxy_set_header Referer http://127.0.0.1:3080/;

    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";

    proxy_buffering off;
    proxy_cache off;
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
}

Блок вставляется в то же поле Custom Nginx Configuration и заменяет набор директив из предыдущего подраздела: таймауты и отключённая буферизация уже внутри него, а заголовки веб-сокетов заданы вручную.

Собственный location / перекрывает блок, который Nginx Proxy Manager создаёт сам, а вместе с ним и проверку списка доступа. Список остаётся выбранным в интерфейсе, но пароль не спрашивается, и вход держится на одном токене. Это осознанный размен: приём применяется тогда, когда без него интерфейс не работает вовсе.

7.5. Список доступа — тот самый пароль перед портом

Access Lists → Add Access List: имя произвольное, на вкладке Authorization заводится пара имени и пароля, на вкладке Access — правило Allow для нужных адресов или 0.0.0.0/0, если вход должен работать отовсюду. Флажок Satisfy Any означает «достаточно любого из условий»; при пароле для всех адресов его следует снять.

Получается два независимых слоя: пароль списка доступа отсекает посторонних до того, как запрос дойдёт до каркаса, а токен dsh отсекает тех, кто пароль всё же узнал. Ни один из них не заменяет другой: список доступа не различает пользователей внутри интерфейса, а токен общий и живёт до перезапуска.

Когда домен не требуется

Для разовой работы с одной машины вся эта цепочка заменяется одной командой:

ssh -N -L 3080:127.0.0.1:3080 пользователь@сервер

Интерфейс открывается по http://127.0.0.1:3080, петля считается защищённым контекстом, и Web Crypto API работает без сертификата. Отпадают узел, правило ufw и оговорки про Fail2Ban. Плата — доступ только оттуда, где есть ключ SSH: ни с телефона, ни из чужого браузера войти не получится. Именно поэтому основной схемой остаётся Nginx Proxy Manager.

Шаг 8. Вход по токену и диагностика по кодам ответа

Собственная проверка входа у веб-интерфейса есть: без токена сервер отвечает 401 Unauthorized на любой запрос, включая curl с самой машины. Ссылка с токеном печатается в журнал при запуске:

dsh web: http://127.0.0.1:3080/?token=<TOKEN>

Открывается собственный домен с тем же параметром:

https://dsh.example.com/?token=<TOKEN>

Токен меняется при каждом перезапуске службы и в $DSH_HOME не сохраняется. При проверке следует учитывать, что grep -r token /opt/dsh/.dsh даёт ложное совпадение с settings.yaml: слово встречается в строке maxTokensField: max_tokens.

Для повседневной работы удобен помощник:

cat >> /root/.bashrc <<'EOF'
dshurl() {
  echo "https://dsh.example.com/?token=$(journalctl -u dsh -n 40 --no-pager | grep -o 'token=[A-Za-z0-9_-]*' | tail -1 | cut -d= -f2)"
}
EOF
source /root/.bashrc
dshurl

Коды ответа указывают на слой обрыва

Наблюдаемый результат Место обрыва
401 от curl на петле штатное поведение: dsh запрашивает токен
Окно с запросом пароля браузера список доступа работает, цепочка до nginx цела
401 после ввода пароля цепочка цела целиком: нужен параметр ?token=
502 от Nginx Proxy Manager или Cloudflare источник недостижим: не работает переброска, ufw или Fail2Ban
Страница открылась, ответ не приходит не включена поддержка веб-сокетов
Ответ обрывается на середине таймауты и буферизация на вкладке Advanced
Запросы к /api отклонены домен не указан в --trusted-host либо мешает Block Common Exploits
Connection refused с машины прокси не работает dsh-forward.service
Таймаут с машины прокси межсетевой экран

Проверка выполняется с машины прокси, а не с той, где установлен dsh:

curl -sv --max-time 10 http://адрес_машины_dsh:3081 2>&1 | tail -20

Ответ 401 на этом шаге — хороший знак: он означает, что до каркаса запрос дошёл и разбираться дальше нужно уже в настройках узла.

Шаг 9. Первая настройка интерфейса

  1. Settings → Models — поставщик openrouter должен присутствовать в списке. Выбранная модель становится моделью по умолчанию для новых сессий.
  2. Choose workspace — добавляется и выбирается /opt/dsh/work. До выбора рабочего каталога поле ввода сессии недоступно.
  3. Пробная задача для проверки цепочки целиком.

Изменения моделей применяются со следующего запроса, перезапуск сервера не требуется. Однако сессия, уже отправившая запрос, остаётся на модели, записанной в её собственном журнале: после смены настроек начинается новый разговор.

Дополнения: как выбирать и в каком порядке ставить

Экосистема выглядит внушительно: несколько сотен репозиториев по метке dsh-plugin, пять конкурирующих сводных списков, десятки новых проектов еженедельно. При этом большинство проектов моложе месяца, у значительной части менее сотни звёзд, а сам каркас меняется под ними.

Плагин здесь не просто зависимость. Это модуль, загружаемый в живой контекст, способный регистрировать инструменты, службы, элементы интерфейса и перехватчики в конвейере вызовов. Он работает внутри процесса, у которого есть оболочка и ключи поставщиков.

Один из сводных списков прямо указывает, что ежедневный отбор ограничивается статической проверкой сценариев, зависимостей, точек входа и чувствительных операций — это не полный аудит и не гарантия совместимости.

Механика установки

Плагины ставятся не в каталог с исходниками, а в профиль внутри $DSH_HOME. Команда передаёт аргументы в pnpm с каталогом профиля в качестве рабочего, поэтому add, remove и update работают как обычно:

sudo -u dsh -H bash -lc 'DSH_HOME=/opt/dsh/.dsh pnpm --dir /opt/dsh/deepseek-harness dsh plugin --profile web add ПАКЕТ'
systemctl restart dsh

После каждого успешного запуска состав слоёв сверяется с установленным: зависимость, чей манифест объявляет ключ dsh.bundle.patch, входит в стек слоёв, зависимость без такого объявления остаётся обычной с однократным предупреждением, удалённая — покидает стек.

Две типовые ловушки:

  • Сборка при установке. Плагины из git, собирающиеся собственным сценарием prepare, блокируются pnpm 10. Первый add падает с подсказкой про allowBuilds и указанием на pnpm-workspace.yaml профиля — напечатанный ключ копируется туда, после чего команда повторяется.
  • Конфликт слоёв. Команда dsh plugin add добавляет в слой пакетов все зависимости профиля, объявившие dsh.bundle, и при пересечении с уже установленным каркас падает при запуске с duplicate loader entry id.

Отсюда рабочее правило: по одному пакету за раз, с перезапуском службы и проверкой журнала между установками.

Добавление и удаление плагинов работает только с самой машины, с 127.0.0.1:3080. Через домен интерфейс ведёт разговоры, но управлять плагинами не может: дополнения ставятся командой в консоли.

Минимальный набор для серверной установки

Проверка безопасности. dsh-security-audit работает только на чтение: настройки, происхождение плагинов, сессии, сетевая открытость. Ставится первым и запускается заново после каждого следующего дополнения.

Политика подтверждений. dsh-auto-mode (отказ по умолчанию, проверки защищённых путей и учётных данных) либо dsh-tool-approval (ручное подтверждение вызовов). Это прямое закрытие исходного ограничения: по умолчанию агент читает произвольные файлы и обращается в сеть без подтверждения.

Политика проверяется на живой сессии: агенту даётся задание прочитать файл с учётными данными. Молча выполненное чтение означает, что политика не работает.

Состав контекста. dsh-context показывает, из чего набрано контекстное окно и как оно меняется. При работе через OpenRouter это единственный вменяемый способ контролировать расход: счётчики баланса (dsh-cost-meter, dsh-billing, dsh-balance-meter) привязаны к счёту DeepSeek и покажут пустоту. Фактические расходы отслеживаются на странице активности OpenRouter.

Клиент MCP входит в поставку как зависимость для слоёв правок, но ни один сервер не включён по умолчанию: команда каждого сервера — доверенный исполняемый код за пределами песочницы агента. При наличии собственной автоматизации это самая ценная связка, но включается она осознанно, слоем правок.

Почему dsh-passwords здесь не нужен

Плагин dsh-passwords добавляет каркасу собственный заслон с входом, подучётными записями, ограничениями по токенам и времени и автоматическим сертификатом. При публикации через Nginx Proxy Manager большая часть этого дублируется, а сертификат прямо конфликтует со схемой: плагин занимает порты 80 и 443 и тянет собственный Let’s Encrypt, тогда как TLS уже завершается на прокси. Обход требует MCP_GATEWAY_AUTO_TLS=0 и высокого порта.

Плагин оправдан при нескольких пользователях с разными квотами — список доступа nginx такого не умеет. Для одного оператора пара «список доступа плюс токен» закрывает задачу меньшими средствами.

Плагины, правящие файлы каркаса накладкой, при сборке из исходников конфликтуют с git pull: изменённые файлы отслеживаются репозиторием, и обновление либо отказывается идти, либо затирает правки. Перед обновлением такие дополнения снимаются, после — ставятся заново.

Что на сервере не нужно

Терминальные интерфейсы, пусковые программы для Windows, настольные клиенты, управление браузером и компьютером (требуют рабочего стола), оформление и мини-игры. Плагин dsh-ssh (удалённое выполнение, SFTP, ProxyJump) выглядит привлекательно при парке машин, но фактически означает выдачу агенту ключей от остальных серверов — до настроенной и проверенной политики подтверждений его установка откладывается.

Рекомендуемый порядок

Проверка безопасности → политика подтверждений → проверка на живой сессии → панель контекста → MCP. Каждое дополнение закрепляется на версии, а не на ветке: при предварительном выпуске обновление плагина и обновление каркаса регулярно конфликтуют.

Обновление без сюрпризов

Обновление — это git, установка зависимостей и обязательная пересборка. Пропуск последнего шага не выдаёт ошибки: служба поднимется на прежних артефактах.

systemctl stop dsh
sudo -u dsh -H bash -lc '
cd /opt/dsh/deepseek-harness
git rev-parse HEAD
git pull
pnpm install
pnpm run build
'
systemctl start dsh
sleep 25
systemctl status dsh --no-pager
curl -sI http://127.0.0.1:3080 | head -1

Команда git rev-parse HEAD стоит перед git pull намеренно: работающая ревизия записывается до того, как её станет негде взять. Это единственный способ вернуться к рабочему состоянию при предварительном выпуске.

Откат выполняется тем же набором команд с переходом на записанный хеш:

systemctl stop dsh
sudo -u dsh -H bash -lc '
cd /opt/dsh/deepseek-harness
git checkout ЗАПИСАННЫЙ_ХЕШ
pnpm install
pnpm run build
'
systemctl start dsh

Пусковой механизм не проверяет свежесть артефактов, поэтому устаревшая сборка интерфейса продолжает работать до пересборки. Ситуация «обновление выполнено, изменений нет» практически всегда означает пропущенный pnpm run build. Узел в Nginx Proxy Manager при этом трогать не нужно: адрес и порт не меняются, отдаётся лишь 502 на время остановки.

Когда достаточно пакета из npm

Если правки в каркас не вносятся и отставание опубликованной версии от ветки не мешает, сборку можно заменить установкой npm install -g @deepseek-ai/dsh в тот же префикс из шага 2. Тогда отпадают подкачка, минуты ожидания и риск забыть пересборку, а обновление сводится к одной команде с точным номером версии. Шаги 2 и 5–9 при этом не меняются — кроме строки ExecStart, где вместо pnpm указывается /opt/dsh/.npm-global/bin/dsh.

Сводная таблица ошибок

Симптом Причина
Установлена посторонняя программа выполнено apt install dsh; каркас ставится только из репозитория или npm
Unable to locate executable '/usr/bin/pnpm' pnpm установлен рядом с другим Node; путь берётся из command -v pnpm
JavaScript heap out of memory памяти меньше, чем требует сборка: добавляется подкачка (потолок кучи сборочный сценарий задаёт сам)
«Обновление выполнено, изменений нет» пропущен pnpm run build после git pull
Ошибки разрешения модулей при старте артефакты не собраны: pnpm run build не выполнялся вовсе
Предупреждение об игнорированных сборках сценарии зависимостей пропущены; нужен pnpm approve-builds и повторный pnpm install
Инструменты с оболочкой отказывают в сессии не собраны node-pty и koffi: установка прошла без установочных сценариев
Пакеты встали не туда, куда ожидалось вызов sudo -u dsh без ключа -H: прочитана настройка root
EACCES при npm install -g от агента не задан пользовательский prefix для npm
Программа, установленная агентом, не найдена /opt/dsh/.npm-global/bin отсутствует в PATH юнита или PATH дописан в .bashrc вместо .profile
Не к чему откатиться после неудачного обновления ревизия не записана до git pull; ветка ориентиром не служит
[Unit]: command not found содержимое юнита вставлено в оболочку; требуется cat > файл <<'EOF'
settings.yaml: No such file or directory не создан $DSH_HOME (/opt/dsh/.dsh)
--host 0.0.0.0 завершается ошибкой не поддерживается умышленно; нужна переброска порта
Узел заведён, но отдаёт 502 на машине с dsh ничего не слушает внешний адрес: не запущен dsh-forward.service
502 при прокси на той же машине в поле Forward указан 127.0.0.1 вместо адреса моста docker
502 при работающей службе и открытом порте Fail2Ban забанил адрес прокси: нужны ignoreip и unbanip
Разрешающее правило ufw исчезло удаление по номеру после смещения нумерации истёкшими банами
Страница открылась, ответ модели не приходит в узле не включена Websockets Support
Длинный ответ обрывается стандартные таймауты nginx; задаются на вкладке Advanced
Текст появляется одним куском в конце включена буферизация; отключается proxy_buffering off
Интерфейс открывается, запросы к /api отклоняются домен не задан в --trusted-host либо мешает Block Common Exploits; при упорстве — подмена Host/Origin/Referer из 7.4
Пароль списка доступа не спрашивается собственный location / перекрывает блок, создаваемый Nginx Proxy Manager
crypto.randomUUID is not a function вход по http: не включён Force SSL
UNKNOWN_MODEL на OpenRouter лишняя приставка openrouter/ в идентификаторе модели
Шлюз отвергает все запросы при верном ключе требуются compat.supportsDeveloperRole: false и maxTokensField: max_tokens
Правка модели не применилась старая сессия держит модель из своего журнала; начинается новая
401 на любой запрос, включая петлю штатное поведение: требуется ?token= из журнала запуска
Токен не подходит после перезапуска токен новый; извлекается из журнала заново
dsh plugin add не выполняется в PATH пользователя нет pnpm
Первый add плагина из git падает pnpm 10 блокирует сборку; ключ из подсказки вносится в pnpm-workspace.yaml профиля
duplicate loader entry id при запуске пересечение слоёв после dsh plugin add
Плагин не ставится через домен управление плагинами работает только локально
Правки плагина пропали после обновления накладка на файлы каркаса стёрта заменой каталога модуля
sudo: The 'no new privileges' flag is set из юнита не удалены NoNewPrivileges и ProtectSystem; см. шаг 6
no sandbox backend is usable on this host единица в kernel.apparmor_restrict_unprivileged_userns; bubblewrap не поднимается
sudo: I'm sorry … I can't do that при install-deps установщик вызывает оболочку, а не apt-get; узкий список путей в sudoers для этого не годится
Файл из /tmp не виден в контейнере PrivateTmp=true: у службы собственный /tmp

Безопасность и телеметрия

Перед запуском рекомендуется прочитать SAFETY.md в репозитории — на этом разработчики настаивают в описании проекта.

Телеметрия по умолчанию остаётся локальной. При её включении в выгрузку попадают тексты сообщений, аргументы и результаты вызовов инструментов и пути рабочего каталога; правил вычистки чувствительных данных в поставке нет. Любое непустое значение DSH_TELEMETRY_DISABLED означает окончательный отказ от выгрузки.

Итоговый периметр складывается из четырёх независимых слоёв: отдельный непривилегированный пользователь, защитные ключи systemd, список доступа на прокси и межсетевой экран, пропускающий только адрес прокси. Отключение любого из них обесценивает остальные три.

Заключение

Сборка из исходников плюс Nginx Proxy Manager дают узел, который работает на свежем коде и при этом остаётся управляемым: ревизия закреплена и записана, откат выполняется теми же четырьмя командами, а имя, сертификат, принудительный HTTPS и пароль перед портом настраиваются в браузере и переживают перезапуски без правки конфигурационных файлов.

Тонкие места сосредоточены в двух точках. Первая — последнее звено цепочки: прокси на соседней машине до петли не дотянется, поэтому переброска порта, правило ufw и адрес прокси в ignoreip обязательны, иначе 502 при полностью исправной службе. Вторая — три настройки узла, без которых интерфейс выглядит сломанным при рабочем соединении: поддержка веб-сокетов, Force SSL и таймауты с отключённой буферизацией.

Третья точка — дисциплина обновления: git pull без pnpm run build тихо оставляет старый интерфейс, а ревизия, не записанная заранее, лишает возможности вернуться. Всё остальное — разграничение прав. Агент по умолчанию читает произвольные файлы и обращается в сеть, поэтому отдельный системный пользователь, защитные ключи юнита и осознанный выбор схемы доступа к Docker дают больше, чем любой набор дополнений.

Оцените статью
ctrllife.ru
Подписаться
Уведомить о
guest
0 комментариев
Старые
Новые Популярные
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x