SSD Nodes Learn Hosting plans →
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-08-27

Как запустить dsh как systemd сервис на VPS

Настройте dsh для работы в фоновом режиме через systemd unit. В руководстве описано создание выделенного пользователя, правила перезапуска и логирование через journalctl.

Запуск dsh в фоновом режиме на VPS без терминала

Запуск dsh в фоновом режиме на VPS требует создания одного unit-файла systemd и выделенного пользователя для его работы. dsh — это интерфейс командной строки для DeepSeek Harness, среды выполнения агентов от DeepSeek, выпущенной под лицензией MIT в рамках developer preview в августе 2026 года. Harness — это программа, работающая вокруг модели, а не сама модель, поэтому под управлением systemd вы размещаете цикл выполнения, инструменты и права доступа, а не процесс инференса DeepSeek. В руководстве по быстрому старту предлагается ввести npx @deepseek-ai/dsh web, что верно, но этот процесс завершается сразу после закрытия SSH-сессии.

Unit-файл решает четыре задачи одновременно. Сервис автоматически перезапускается после перезагрузки системы. Его вывод направляется в журнал, а не прокручивается перед глазами. Он работает от имени пользователя, отличного от root. Кроме того, используется выбранная вами версия, что здесь критически важно, так как разработчики прямо предупреждают об этом:

DeepSeek Harness в настоящее время находится в стадии developer preview и активно развивается. БУДУТ ВНОСИТЬСЯ ИЗМЕНЕНИЯ, НАРУШАЮЩИЕ СОВМЕСТИМОСТЬ.

В этом руководстве предполагается, что dsh уже работает у вас при ручном запуске. Если это не так, начните с установки DeepSeek Harness на VPS и вернитесь, когда npx @deepseek-ai/dsh web начнет открывать страницу.

Сначала Node, так как npm не выдаст предупреждение

node -v

В Ubuntu 24.04 поставляется Node 18 (версия 18.19.1 на август 2026 года), что является устаревшим вариантом для пакета, выпущенного в этом году. В @deepseek-ai/dsh не указано поле engines, поэтому npm не выводит предупреждение EBADENGINE, если ваша версия Node слишком старая. Ошибка возникнет только во время выполнения в виде синтаксического сбоя или отсутствия встроенной функции, что значительно затрудняет отладку. Установите актуальный релиз с долгосрочной поддержкой (LTS) из репозитория NodeSource:

curl -fsSL https://deb.nodesource.com/setup_22.x -o /tmp/nodesource_setup.sh
less /tmp/nodesource_setup.sh
sudo -E bash /tmp/nodesource_setup.sh
sudo apt install -y nodejs
node -v

Команда node -v теперь должна выводить версию v22. Строка less присутствует, так как передача удаленного скрипта напрямую в bash приводит к выполнению кода, который вы не просматривали.

Проверьте работу сервиса перед созданием unit-файла

npx @deepseek-ai/dsh@0.1.0-rc.7 web

Оставьте этот процесс запущенным. Откройте вторую SSH-сессию и выполните:

curl -fsS http://127.0.0.1:3080/ -o /dev/null && echo up

up означает, что веб-профиль ожидает соединений на loopback-интерфейсе, что является поведением по умолчанию. curl: (7) Failed to connect to 127.0.0.1 port 3080: Connection refused означает, что сервис не запущен, а первый терминал указывает на причину. Остановите процесс вручную с помощью Ctrl+C, прежде чем продолжать: unit, который пытается занять порт, уже используемый другим процессом, завершится с ошибкой Error: listen EADDRINUSE: address already in use 127.0.0.1:3080.

0.1.0-rc.7 была опубликованной версией на 18 августа 2026. Проверьте актуальную версию с помощью npm view @deepseek-ai/dsh version, а затем зафиксируйте ту версию, которую планируете использовать.

Установка зафиксированной версии глобально

npx — неподходящий инструмент для использования внутри unit-файла. Он определяет версию пакета в момент запуска процесса, поэтому перезапуск через три месяца может привести к запуску другой сборки агента на стадии preview без каких-либо действий с вашей стороны. Кроме того, при загрузке системе требуется доступ к npm registry, что превращает исправно работающую машину в нерабочий юнит в день, когда реестр работает медленно. Выполните установку один раз, указав версию, которую вы зафиксировали:

sudo npm install -g @deepseek-ai/dsh@0.1.0-rc.7
command -v dsh
npm ls -g --depth=0 @deepseek-ai/dsh

command -v dsh выводит /usr/bin/dsh, если npm был установлен из NodeSource, и /usr/local/bin/dsh, если из штатного пакета Ubuntu. Используйте в unit-файле путь, который был выведен командой. npm ls -g выводит точную версию — это именно те данные, которые вам понадобятся через шесть недель, когда поведение изменится, а вы не сможете вспомнить, что именно установили. Если установка завершается ошибкой, или command -v dsh после неё ничего не выводит, или полученная версия не совпадает с запрошенной, разберитесь с типичными ошибками установки и версионирования dsh, прежде чем создавать unit-файл.

Пользователь, владеющий только сервисом

Агент выполняет shell-команды. Это его основная задача. Запуск от имени root превращает каждый вызов инструмента в вызов от root, поэтому создайте для агента отдельную учетную запись без доступа к оболочке входа (login shell).

sudo useradd --system --create-home --home-dir /var/lib/dsh --shell /usr/sbin/nologin dsh
sudo install -d -o dsh -g dsh -m 750 /var/lib/dsh/harness /var/lib/dsh/workspace
id dsh

/var/lib/dsh/harness становится DSH_HOME — это каталог, в котором dsh хранит профили. Профиль представляет собой именованный стек наборов плагинов с вашим собственным слоем патчей поверх них, а профили web и headless автоматически собираются из поставляемых шаблонов при первом запуске. Всё, что вы добавите в этот стек позже, будет выполняться от имени этого пользователя с его правами доступа к файлам и shell, поэтому проверка плагина перед установкой является такой же частью работы, как и создание учетной записи. При первом запуске создаются файлы и могут загружаться наборы данных, поэтому выполните его вручную, чтобы иметь возможность контролировать процесс.

sudo -u dsh env HOME=/var/lib/dsh DSH_HOME=/var/lib/dsh/harness /usr/bin/dsh --profile web

Установите HOME явно, не полагаясь на то, как с этим работает sudo, поскольку поведение sudo при перезаписи HOME для команд без входа в систему зависит от настройки set_home в /etc/sudoers. Если допустить ошибку, при первом запуске кэш-директории будут созданы в вашем домашнем каталоге с владельцем dsh, и впоследствии сервис не сможет найти собственное состояние. Остановите процесс с помощью Ctrl+C, как только проверка curl вернет up.

Файл юнита

Напишите /etc/systemd/system/dsh.service:

[Unit]
Description=DeepSeek Harness (dsh) web profile
Documentation=https://github.com/deepseek-ai/deepseek-harness
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=300
StartLimitBurst=5

[Service]
Type=exec
User=dsh
Group=dsh
WorkingDirectory=/var/lib/dsh/workspace
Environment=HOME=/var/lib/dsh
Environment=DSH_HOME=/var/lib/dsh/harness
ExecStart=/usr/bin/dsh --profile web
Restart=on-failure
RestartSec=5s
TimeoutStopSec=30s
SyslogIdentifier=dsh
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true

[Install]
WantedBy=multi-user.target

ExecStart= принимает абсолютный путь, который вы получили с помощью command -v dsh. systemd ищет имя команды без пути в фиксированном списке директорий, но этот список не совпадает с PATH вашей оболочки, поэтому использование абсолютного пути исключает ошибки.

WorkingDirectory= — это директория, в которой разрешаются относительные пути и из которой запускается инструмент, вызывающий ls без аргументов. Укажите здесь рабочую директорию, которую вы предоставляете агенту. Если директория отсутствует или у пользователя сервиса нет прав на вход в неё, юнит завершится с ошибкой status=200/CHDIR ещё до запуска dsh.

ProtectHome=true скрывает /home и /root от процесса. Это безопасно, так как всё, с чем работает сервис, находится внутри /var/lib/dsh. Если вы укажете рабочую директорию по пути внутри /home, агент сообщит, что директория не существует; это может сбить с толку, пока вы не вспомните об этой строке. ProtectSystem=full переводит /usr, /boot и /etc в режим «только для чтения», так как сервису никогда не требуется запись в эти директории.

Соблазнительно пойти дальше, но обычно это ошибка. ProtectSystem=strict делает всю файловую систему доступной только для чтения, за исключением псевдофайловых систем ядра, поэтому первый же вызов инструмента, пытающийся записать файл, завершится с ошибкой EROFS: read-only file system. Если вам нужен такой уровень защиты, добавьте ReadWritePaths=/var/lib/dsh в ту же правку.

Какой тип Type= здесь подходит

Type=exec, так как dsh остается в foreground и не выполняет fork. Преимущество перед значением по умолчанию заключается в получении реального сообщения об ошибке. При Type=simple systemd считает запуск успешным сразу после выполнения fork, еще до того, как станет известно о существовании бинарного файла. Поэтому systemctl start dsh завершается успешно, а ошибка отображается только в журнале. При Type=exec systemd ожидает завершения execve(), поэтому опечатка в ExecStart= приведет к ошибке выполнения команды, которую вы только что ввели, и вы сразу её увидите.

Два других варианта приведут к зависанию. Type=forking указывает systemd ожидать завершения родительского процесса, а dsh никогда не завершается, поэтому запуск блокируется до истечения TimeoutStartSec (по умолчанию 90 секунд), после чего выдается ошибка Job for dsh.service failed because a timeout was exceeded.. Type=notify ожидает сообщение READY=1 через sd_notify, и процесс Node, который его не отправляет, зависает аналогичным образом. Полное сравнение типов сервисов systemd охватывает остальные случаи, включая ситуации, когда настройка notify оправдана.

Правила перезапуска, которые срабатывают с ошибкой

Restart=on-failure выполняет перезапуск при ненулевом коде завершения или фатальном сигнале и оставляет юнит в покое после корректного завершения работы. Это поведение, которое требуется для предварительной сборки. Если dsh когда-либо завершается с кодом 0 из-за того, что прочитал некорректную конфигурацию, юнит останавливается и остается в этом состоянии, а systemctl status dsh показывает inactive (dead), где вы можете это увидеть. Restart=always превращает это же событие в цикл перезапусков, который издалека выглядит как нормальная работа.

Ограничение частоты — это та часть, которую часто упускают. По умолчанию в systemd установлено пять запусков за десять секунд, и с RestartSec=5s вы никогда не достигнете пяти запусков за десятисекундный интервал, поэтому юнит, который падает при старте, перезапускается бесконечно, и об этом знает только журнал. StartLimitIntervalSec=300 вместе с StartLimitBurst=5 означает, что пяти сбоев за пять минут достаточно: systemd сдается и переводит юнит в состояние failed, записывая в журнал Start request repeated too quickly.. Очистите это состояние с помощью sudo systemctl reset-failed dsh после того, как устраните причину. Оба параметра должны находиться в [Unit], а не в [Service], иначе systemd проигнорирует их без предупреждения, если они указаны в неверной секции.

Запуск и проверка

sudo systemctl daemon-reload
sudo systemctl enable --now dsh
systemctl status dsh

enable --now выполняет две задачи. enable обеспечивает автоматический запуск службы после перезагрузки, а --now запускает её в текущем сеансе. Служба, запущенная только через systemctl start, перестанет работать после перезагрузки, а обновления ядра требуют перезагрузки системы.

systemctl status dsh должен отобразить Active: active (running), Main PID и строку Memory:. Затем проверьте, на каком адресе ожидаются соединения:

sudo ss -lntp | grep 3080

Вам нужен 127.0.0.1:3080. Если вы видите 0.0.0.0:3080, значит, адрес привязки был изменён, и ваш агент доступен из публичной сети. Имя процесса в выводе — node, а не dsh, так как бинарный файл dsh является скриптом Node, поэтому pgrep -x dsh ничего не находит. Используйте systemctl show -p MainPID dsh.

После этого выполните перезагрузку. Служба, которая не запускается автоматически после перезагрузки, ещё не готова к эксплуатации.

sudo reboot

Подключитесь снова и выполните systemctl is-active dsh. Команда выведет active.

Чтение логов с помощью journalctl

Все, что записывает dsh в stdout и stderr, попадает в журнал под именем юнита.

journalctl -u dsh -f
journalctl -u dsh -n 200 --no-pager
journalctl -u dsh --since "10 min ago" -p err

-f отслеживает новые строки, -n показывает последние N строк, -p err фильтрует вывод по приоритету. SyslogIdentifier=dsh в юните — это причина, по которой строки имеют тег dsh, а не node; это важно при первом чтении журнала, если вывод не отфильтрован по имени юнита.

Проверьте, сохраняется ли журнал после перезагрузки, прежде чем он вам понадобится:

journalctl -u dsh -b -1

Если команда выводит Specifying boot ID or boot offset has no effect, no persistent journal was found, значит, журнал находится в /run и удаляется при каждой перезагрузке. Создайте директорию и перезапустите демон:

sudo mkdir -p /var/log/journal
sudo systemctl restart systemd-journald

Доступ к UI через SSH-туннель вместо публичного порта

dsh предоставляет веб-интерфейс (UI) на 127.0.0.1:3080 и отказывается работать на других адресах. Если запросить --host 0.0.0.0, процесс завершится с ошибкой:

error: --host 0.0.0.0 is intentionally not supported yet for safety: it would expose remote code execution to the network; use 127.0.0.1 instead

Это не ограничение, которое нужно обходить. Веб-API (интерфейс прикладного программирования) управляет агентом, а агент выполняет shell-команды. Таким образом, доступный извне порт — это готовый shell на вашем VPS для любого, кто его обнаружит. Разработчики объясняют привязку к loopback-интерфейсу отсутствием встроенной удалённой аутентификации. Что на самом деле означает строка 127.0.0.1:3080 в выводе при запуске стоит прочитать, прежде чем пытаться изменить этот адрес. Вместо этого пробросьте порт со своей локальной машины:

ssh -N -L 3080:127.0.0.1:3080 you@203.0.113.10

-L 3080:127.0.0.1:3080 открывает порт 3080 на вашем ноутбуке и перенаправляет всё, что туда поступает, на 127.0.0.1:3080, как если бы запрос был разрешён на стороне VPS. -N означает, что удалённая команда не выполняется, сессия нужна только для поддержания туннеля. Оставьте её запущенной и откройте http://127.0.0.1:3080/ в браузере. Там, в разделе Settings, а затем Models, вы введёте API-ключ DeepSeek и выберете рабочую директорию. Укажите в качестве рабочей директории /var/lib/dsh/workspace — каталог, владельцем которого является сервисный пользователь, иначе файловые инструменты агента завершатся с ошибкой EACCES: permission denied.

Если порт 3080 занят на вашем ноутбуке, ssh сообщит об этом:

bind [127.0.0.1]:3080: Address already in use
channel_setup_fwd_listener_tcpip: cannot listen to port: 3080

Выберите другой локальный порт с помощью ssh -N -L 3081:127.0.0.1:3080 you@203.0.113.10, а затем перейдите по адресу http://127.0.0.1:3081/. Чтобы не вводить команду каждый раз, сохраните её в ~/.ssh/config на своей машине:

Host dsh-vps
  HostName 203.0.113.10
  User you
  LocalForward 3080 127.0.0.1:3080

После этого ssh -N dsh-vps будет достаточно для запуска. Теперь этот туннель — единственный вход к вашему агенту, поэтому его защищает SSH-демон: используйте только ключи, отключите аутентификацию по паролю, а все рекомендации из укрепления безопасности SSH на вашем VPS здесь применимы с удвоенной силой. Если ваш VPS станет частью небольшой частной сети с базой данных или тестовым сервером, анонсирование этих адресов в вашей tailnet через subnet router избавит вас от необходимости настраивать проброс для каждого сервиса, хотя из-за привязки dsh к loopback сам UI всё равно будет доступен только через туннель.

Ключ не должен находиться в файле юнита. Значения Environment= выводятся командой systemctl show dsh -p Environment, которую может запустить любой пользователь в системе. Если устанавливаемому плагину нужен ключ в переменных окружения, поместите его в /etc/dsh.env с правами доступа 600, владельцем которого является root, и сошлитесь на него через EnvironmentFile=/etc/dsh.env. systemd читает этот файл от имени root во время выполнения, а systemctl show не выводит его содержимое. В какой именно файл на диске попадает каждая настройка и что именно уходит с вашего сервера, когда вы направляете dsh на локальный эндпоинт Ollama вместо API DeepSeek, описано в настройке ключей, моделей и эндпоинтов dsh.

Стоимость эксплуатации

Инференс выполняется через API DeepSeek, а не на вашем VPS. Ваш сервер оплачивает работу процесса Node, обслуживаемого им интерфейса и каждой команды, которую агент решает выполнить. Первые два фактора стабильны и незначительны. Третий ничем не ограничен в данном unit-файле.

Определите базовое потребление на своем сервере, вместо того чтобы доверять чужим цифрам:

systemctl show dsh -p MemoryCurrent
systemd-cgtop -1 --depth 2

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

Вызовы инструментов являются дочерними процессами сервиса, поэтому они попадают в ту же контрольную группу (cgroup) и учитываются в рамках тех же лимитов. Агент, запускающий npm install или набор тестов внутри рабочей директории, может потреблять значительно больше памяти, чем сама оболочка. На VPS с 1 GB ОЗУ именно здесь возникают сбои: ядро выбирает процесс и завершает его, а journalctl -k | grep -i "out of memory" показывает строку Out of memory: Killed process с указанием того, что было выбрано. Часто это не тот процесс, который вызвал проблему.

Решение заключается в установке осознанных ограничений. Параметры MemoryMax= и CPUQuota= в секции [Service] локализуют ущерб внутри юнита, поэтому вышедшая из-под контроля сборка будет завершена, вместо того чтобы привести к зависанию всего сервера. В ограничении памяти и CPU с помощью systemd описаны значения и поведение при сбоях. Дисковое пространство также расходуется: из-за истории сессий в DSH_HOME и любых данных, которые агент записывает в рабочую область, поэтому добавьте du -sh /var/lib/dsh в используемую вами систему мониторинга диска.

Если вам нужен интерактивный агент, к которому можно подключаться и отключаться, формат сервиса не подходит, и лучше использовать запуск агента в постоянной сессии tmux. Запускайте dsh как юнит, если он должен быть постоянно активен и доступен через туннель.

Типичные сбои и сообщения об ошибках

status=203/EXEC. systemd не может запустить файл, и в логах отображается Failed to locate executable /usr/local/bin/dsh: No such file or directory. Путь в ExecStart= не совпадает с тем, что вывел command -v dsh. Это ошибка, о которой Type=exec сообщает во время systemctl start, вместо того чтобы скрывать её.

status=217/USER. Учетная запись в User= не существует. Проверьте это с помощью id dsh.

status=200/CHDIR. WorkingDirectory= отсутствует, или у пользователя сервиса нет прав на вход в эту директорию. sudo -u dsh ls /var/lib/dsh/workspace позволяет воспроизвести эту проблему напрямую.

Error: listen EADDRINUSE: address already in use 127.0.0.1:3080. Порт уже занят, обычно процессом npx, который остался запущенным в другом терминале. sudo ss -lntp | grep 3080 позволяет узнать имя процесса.

EACCES: permission denied с указанием пути. Права владения в /var/lib/dsh установлены неверно, обычно из-за того, что первый запуск был выполнен от имени root или с неверным HOME. sudo chown -R dsh:dsh /var/lib/dsh исправляет эту проблему.

Start request repeated too quickly. Юнит достиг лимита частоты запусков и прекратил попытки. Истинная причина ошибки находится в строках выше. Выполните sudo systemctl reset-failed dsh перед повторной попыткой.

Юнит находится в состоянии active (running), но браузер ничего не отображает. Выполните проверку на VPS: если curl -fsS http://127.0.0.1:3080/ -o /dev/null && echo up выводит up, значит сервис работает корректно, а проблема заключается в пробросе портов.

Целенаправленное обновление

Фиксация версии (pinning) означает, что обновление — это ваше осознанное действие, а не случайное событие. Сначала изучите release notes, так как предупреждения разработчиков о нарушениях обратной совместимости являются основной причиной для фиксации. Создайте резервную копию каталога с данными, а затем замените версию:

sudo systemctl stop dsh
sudo tar czf /root/dsh-home-$(date +%F).tgz -C /var/lib/dsh harness
sudo npm install -g @deepseek-ai/dsh@0.1.0-rc.7
sudo systemctl start dsh
journalctl -u dsh -n 50 --no-pager

Откат выполняется аналогично npm install -g с использованием старой версии и последующим восстановлением архива, что возможно только при наличии предварительно созданной копии. Среда выполнения агента на этапе предварительного просмотра — это именно тот тип ПО, где обновление может изменить формат конфигурации без вашего ведома.

FAQ

Почему dsh останавливается, когда я закрываю SSH-сессию?

Потому что npx @deepseek-ai/dsh web — это процесс, работающий в интерактивном режиме и привязанный к вашей сессии входа, поэтому он завершается при её окончании. Юнит systemd принадлежит системе инициализации, поэтому он продолжает работать после отключения и запускается снова после перезагрузки. sudo systemctl enable --now dsh — это пара действий, которая обеспечивает оба условия: enable для автозапуска после перезагрузки, --now для запуска прямо сейчас.

Что лучше использовать для dsh: Type=simple или Type=exec?

Type=exec. dsh работает в интерактивном режиме и не создает дочерние процессы (fork), поэтому подходят оба варианта, но Type=exec заставляет systemd дождаться успешного выполнения execve(), прежде чем считать запуск успешным. Ошибка в пути в ExecStart= приведет к сбою systemctl start с выводом status=203/EXEC прямо перед вами. При использовании Type=simple та же ошибка вернет статус успеха и скроется в журнале. Type=forking и Type=notify в данном случае не подходят, так как оба варианта будут ожидать завершения до тех пор, пока TimeoutStartSec не истечет через 90 секунд.

Как открыть веб-интерфейс dsh с моего ноутбука?

Перенаправьте порт через SSH: ssh -N -L 3080:127.0.0.1:3080 you@your-vps, затем откройте http://127.0.0.1:3080/ в браузере. Не пытайтесь привязывать сервис к публичному IP-адресу. dsh отклоняет --host 0.0.0.0 с ошибкой error: --host 0.0.0.0 is intentionally not supported yet for safety: it would expose remote code execution to the network; use 127.0.0.1 instead, так как веб-API позволяет агенту выполнять shell-команды, а встроенная удаленная аутентификация отсутствует.

Можно ли запускать dsh от имени root для упрощения прав доступа?

Нет. Инструментарий предназначен для выполнения команд и записи файлов, поэтому любые привилегии сервиса становятся привилегиями агента. Создайте системную учетную запись с помощью useradd --system --shell /usr/sbin/nologin dsh, назначьте её владельцем /var/lib/dsh и добавьте NoNewPrivileges=true в юнит. Если после этого вы столкнетесь с EACCES: permission denied, наиболее вероятная причина — предыдущий запуск от имени root, после которого остались файлы с соответствующим владельцем; sudo chown -R dsh:dsh /var/lib/dsh исправит эту ситуацию.

Какую версию dsh следует зафиксировать в юните?

Ту, которую сообщает npm view @deepseek-ai/dsh version на момент настройки сервиса, установленную через npm install -g @deepseek-ai/dsh@<that version> и задокументированную там, где вы сможете её найти. Версия 0.1.0-rc.7 была актуальна на 18 августа 2026 года. Суть не в номере версии, а в том, что npx без указания версии разрешает пакет во время запуска, поэтому автоматическая перезагрузка может незаметно перевести вас на сборку с другим форматом конфигурации.