Как запустить dsh как systemd сервис на VPS
Настройте dsh для работы в фоновом режиме через systemd. В статье приведена конфигурация unit-файла, правила Restart и настройка SSH-туннеля для доступа к UI без прав root.
Запуск dsh в фоновом режиме на VPS без терминала
Запуск dsh в фоновом режиме на VPS требует создания одного unit-файла systemd и отдельного пользователя, от имени которого будет работать процесс. dsh — это интерфейс командной строки для DeepSeek Harness, среды выполнения агентов DeepSeek, выпущенной под лицензией MIT в рамках developer preview в августе 2026 года. В руководстве по быстрому старту предлагается ввести 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 приводит к выполнению кода, который вы не просматривали.
Убедитесь в работоспособности перед созданием юнита
npx @deepseek-ai/dsh@0.1.0-rc.7 webОставьте процесс запущенным. Из второй SSH-сессии выполните:
curl -fsS http://127.0.0.1:3080/ -o /dev/null && echo upup означает, что веб-профиль ожидает соединений на loopback-интерфейсе, что является стандартным поведением привязки. curl: (7) Failed to connect to 127.0.0.1 port 3080: Connection refused означает, что это не так, и в первом терминале указана причина. Остановите ручной запуск с помощью Ctrl+C перед дальнейшими действиями: юнит, который пытается занять порт, уже используемый другим процессом, завершится с ошибкой 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-файла. Он определяет версию пакета в момент запуска процесса, поэтому перезапуск через три месяца может привести к запуску другой сборки агента на стадии предварительного тестирования без каких-либо действий с вашей стороны. Кроме того, при загрузке требуется доступ к npm registry, что превращает исправно работающую машину в нерабочий юнит в день, когда реестр работает медленно. Выполните установку один раз, указав версию, которую вы зафиксировали:
sudo npm install -g @deepseek-ai/dsh@0.1.0-rc.7
command -v dsh
npm ls -g --depth=0 @deepseek-ai/dshcommand -v dsh выводит /usr/bin/dsh, если npm был установлен из NodeSource, и /usr/local/bin/dsh, если из собственного пакета Ubuntu. Используйте путь, который был выведен, в unit-файле. npm ls -g выводит точную версию, что является ответом, который вам понадобится через шесть недель, когда поведение изменится, а вы не сможете вспомнить, что именно установили.
Пользователь, владеющий только сервисом
Агент выполняет shell-команды. Это его основная задача. Запуск от имени root делает каждый вызов инструмента вызовом с правами суперпользователя, поэтому создайте для него отдельную учетную запись без доступа к 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 собираются из поставляемых шаблонов при первом запуске. Этот первый запуск создает файлы и может загружать наборы, поэтому выполняйте его вручную, чтобы контролировать процесс.
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.targetExecStart= принимает абсолютный путь, который вы получили с помощью 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 dshenable --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 отсутствием встроенной удаленной аутентификации. Вместо этого перенаправьте порт со своей машины:
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 применяйте с особой строгостью.
Ключу не место в unit-файле. Значения Environment= выводятся командой systemctl show dsh -p Environment, которую может запустить любой пользователь в системе. Если плагину, который вы устанавливаете, требуется ключ в переменных окружения, поместите его в /etc/dsh.env с правами 600, владельцем которого является root, и укажите путь к нему через EnvironmentFile=/etc/dsh.env. systemd считывает этот файл от имени root во время выполнения, а systemctl show не выводит его содержимое.
Стоимость эксплуатации
Вычисления происходят через API DeepSeek, а не на вашем VPS. Ваш сервер оплачивает работу процесса Node, предоставляемый им интерфейс и каждую команду, которую агент решает выполнить. Первые два компонента потребляют стабильный и небольшой объем ресурсов. Третий ничем не ограничен в данном unit-файле.
Определите базовое потребление на своем сервере самостоятельно, вместо того чтобы доверять чужим цифрам:
systemctl show dsh -p MemoryCurrent
systemd-cgtop -1 --depth 2MemoryCurrent измеряется в байтах. Отслеживайте этот показатель во время работы агента, а не в режиме ожидания.
Вызовы инструментов являются дочерними процессами службы, поэтому они попадают в ту же контрольную группу (cgroup) и учитываются в рамках тех же лимитов. Агент, запускающий npm install или набор тестов внутри рабочей директории, может потреблять значительно больше памяти, чем сама оболочка. На VPS с 1 ГБ ОЗУ именно здесь возникают сбои: ядро выбирает процесс и завершает его, а 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, так как предупреждения разработчиков о нарушении обратной совместимости являются основной причиной для фиксации. Создайте резервную копию каталога с состоянием (state directory), а затем замените версию:
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 с использованием старой версии и последующим восстановлением архива, что возможно только при наличии предварительно созданной копии. Среда выполнения агента на стадии preview — это именно тот тип ПО, где обновление может изменить формат конфигурации без вашего участия.
FAQ
Why does dsh stop when I close my SSH session?
Because npx @deepseek-ai/dsh web is a foreground process owned by your login session, so it is torn down when the session ends. A systemd unit is owned by the init system instead, which is why it keeps running after you disconnect and starts again after a reboot. sudo systemctl enable --now dsh is the pair of steps that gives you both: enable for the reboot, --now for this boot.
Should I use Type=simple or Type=exec for dsh?
Type=exec. dsh runs in the foreground and never forks, so both work, but Type=exec makes systemd wait for execve() to succeed before it calls the start successful. A wrong path in ExecStart= then fails systemctl start with status=203/EXEC in front of you. With Type=simple the same mistake returns success and hides in the journal. Type=forking and Type=notify are both wrong here, and both hang until TimeoutStartSec expires after 90 seconds.
How do I open the dsh web UI from my laptop?
Forward the port over SSH: ssh -N -L 3080:127.0.0.1:3080 you@your-vps, then open http://127.0.0.1:3080/ in your browser. Do not try to bind the service to a public address. dsh rejects --host 0.0.0.0 with 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, because the web API can make the agent run shell commands and there is no remote authentication in front of it.
Can I run dsh as root to keep permissions simple?
No. The harness exists to run commands and write files, so whatever privileges the service has are privileges the agent has. Create a system account with useradd --system --shell /usr/sbin/nologin dsh, own /var/lib/dsh with it, and add NoNewPrivileges=true to the unit. If you hit EACCES: permission denied afterwards, the usual cause is an earlier run as root leaving root-owned files behind, and sudo chown -R dsh:dsh /var/lib/dsh clears it.
Which version of dsh should I pin in the unit?
Whatever npm view @deepseek-ai/dsh version reports when you set the service up, installed with npm install -g @deepseek-ai/dsh@<that version> and recorded somewhere you will find it. 0.1.0-rc.7 was current on 18 August 2026. The point is not the number, it is that npx with no version resolves the package at start time, so an unattended restart can silently move you onto a build with a different config format.