Como executar o dsh sem terminal num VPS com systemd
Configure o dsh como serviço systemd no VPS, com utilizador dedicado, versão fixa, Restart, logs no journalctl e túnel SSH para a interface.
Executar dsh sem terminal num VPS
Executar dsh sem terminal num VPS requer um ficheiro de unidade do systemd e um utilizador dedicado para ser o proprietário do serviço. dsh é o iniciador de linha de comandos do DeepSeek Harness, o runtime de agentes da DeepSeek, publicado sob a licença MIT em developer preview em agosto de 2026. Um harness é o programa que envolve o modelo, e não o próprio modelo. Por isso, o que está a colocar sob o systemd é o ciclo, as ferramentas e as permissões, não a inferência da DeepSeek. O quickstart indica que deve introduzir npx @deepseek-ai/dsh web, o que está correto, mas o processo também termina assim que fechar a sessão SSH (secure shell).
Um ficheiro de unidade resolve quatro problemas ao mesmo tempo. O serviço volta a iniciar depois de um reboot. A saída é enviada para o journal, em vez de desaparecer do terminal. O serviço é executado por uma conta que não é root. E a versão executada é a versão que escolheu. Isto é especialmente importante neste caso, porque o projeto upstream avisa em letras maiúsculas:
O DeepSeek Harness está atualmente em developer preview e está a evoluir rapidamente. HAVERÁ ALTERAÇÕES INCOMPATÍVEIS.
Este guia pressupõe que o dsh já funciona quando o executa manualmente. Se isso não acontecer, comece por instalar o DeepSeek Harness num VPS e retome o procedimento quando npx @deepseek-ai/dsh web servir uma página.
Node primeiro, porque o npm não o avisará
node -vO pacote do próprio Ubuntu 24.04 é o Node 18 (18.19.1 em agosto de 2026), que é antigo para um pacote publicado este ano. @deepseek-ai/dsh não publica nenhum campo engines, por isso o npm não apresenta nenhum aviso EBADENGINE quando a sua versão do Node é demasiado antiga. A falha surge apenas em tempo de execução, como um erro de sintaxe ou um componente integrado em falta. Esse é um momento muito pior para a detetar. Instale uma versão atual de suporte de longo prazo (LTS) a partir do 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 -vnode -v deve agora apresentar uma versão v22. A linha less existe porque encaminhar diretamente um script remoto para bash executa código que não leu.
Confirme que o serviço funciona antes de escrever uma unidade
npx @deepseek-ai/dsh@0.1.0-rc.7 webDeixe-o em execução. Numa segunda sessão SSH:
curl -fsS http://127.0.0.1:3080/ -o /dev/null && echo upup significa que o perfil web está a escutar no loopback, que é onde fica associado por predefinição. curl: (7) Failed to connect to 127.0.0.1 port 3080: Connection refused significa que não está, e o primeiro terminal está a indicar o motivo. Pare a execução manual com Ctrl+C antes de continuar: uma unidade que tenta associar-se a uma porta já utilizada por outro processo falha com Error: listen EADDRINUSE: address already in use 127.0.0.1:3080.
0.1.0-rc.7 era a versão publicada em 18 August 2026. Verifique qual é a versão atual com npm view @deepseek-ai/dsh version e, em seguida, fixe a versão que decidir executar.
Instale globalmente a versão fixada
npx é a ferramenta errada dentro de um ficheiro de unidade. Resolve a versão do pacote quando o processo é iniciado. Assim, um reinício daqui a três meses pode iniciar uma compilação diferente de um agente em fase de pré-visualização, sem qualquer alteração da sua parte. Também precisa de conseguir aceder ao registo do npm durante o arranque. Se o registo estiver lento nesse dia, uma máquina funcional transforma-se numa unidade com falha. Instale uma vez, com a versão registada:
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 apresenta /usr/bin/dsh quando o npm veio do NodeSource e /usr/local/bin/dsh quando veio do pacote próprio do Ubuntu. Use no ficheiro de unidade o caminho apresentado pelo comando. npm ls -g apresenta a versão exata. É essa a informação de que precisará daqui a seis semanas, quando o comportamento mudar e já não se lembrar do que instalou. Se a instalação falhar, se command -v dsh não apresentar nada depois disso ou se a versão devolvida não for a que pediu, resolva as falhas habituais de instalação e versão do dsh antes de escrever o ficheiro de unidade.
Um utilizador que é proprietário do serviço e de mais nada
O agente executa comandos de shell. Essa é a sua função. Executá-lo como root faz com que todas as chamadas de ferramentas sejam executadas como root. Por isso, crie uma conta própria para o agente, sem shell de login.
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 torna-se DSH_HOME, o diretório onde dsh mantém os perfis. Um perfil é uma pilha identificada de conjuntos de plugins, com a sua própria camada de patches. Os perfis web e headless são criados a partir dos templates fornecidos na primeira vez que são iniciados. Tudo o que adicionar posteriormente a essa pilha será executado por este utilizador, com o acesso próprio do agente aos ficheiros e à shell. Por isso, validar um plugin antes de o instalar faz parte da mesma tarefa que criar a conta. A primeira inicialização grava ficheiros e pode obter conjuntos de plugins. Faça-a manualmente, num terminal onde possa monitorizá-la.
sudo -u dsh env HOME=/var/lib/dsh DSH_HOME=/var/lib/dsh/harness /usr/bin/dsh --profile webDefina HOME explicitamente, em vez de confiar no que sudo faz com essa variável. A forma como sudo reescreve HOME para um comando sem login depende da definição set_home em /etc/sudoers. Se a definir incorretamente, a primeira execução cria diretórios de cache na sua própria home, pertencentes a dsh. Mais tarde, o serviço não conseguirá encontrar o seu próprio estado. Pare-o com Ctrl+C assim que a verificação curl devolver up.
O ficheiro da unidade
Escreva /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= recebe o caminho absoluto obtido com command -v dsh. O systemd procura um nome de comando simples numa lista fixa de caminhos, mas essa lista não é o PATH da sua shell. Por isso, um caminho absoluto elimina a ambiguidade.
WorkingDirectory= é o diretório usado para resolver caminhos relativos. Também é o diretório inicial de uma chamada de ferramenta que executa ls sem argumentos. Aponte-o para o workspace fornecido ao agente. Se o diretório não existir ou o utilizador do serviço não conseguir aceder-lhe, a unidade falha com status=200/CHDIR antes de o dsh ser executado.
ProtectHome=true oculta /home e /root do processo. Isso é seguro neste caso porque tudo o que o serviço utiliza está em /var/lib/dsh. Aponte o workspace para um caminho dentro de /home. Nesse caso, o agente informará que o diretório não existe. Isso pode ser confuso até se lembrar desta linha. ProtectSystem=full torna /usr, /boot e /etc apenas de leitura, algo que o serviço nunca precisa de alterar.
É tentador restringir ainda mais, mas normalmente isso está errado. ProtectSystem=strict torna todo o sistema de ficheiros apenas de leitura, exceto os pseudo-sistemas de ficheiros do kernel. Assim, a primeira chamada de ferramenta que tentar escrever num ficheiro falha com EROFS: read-only file system. Se quiser esse nível de restrição, adicione ReadWritePaths=/var/lib/dsh na mesma edição.
Qual Type= deve ser usado aqui
Type=exec, porque dsh permanece em primeiro plano e nunca faz fork. A vantagem em relação ao padrão é obter uma mensagem de erro real. Com Type=simple, o systemd considera o arranque bem-sucedido assim que o processo faz fork, antes de saber se o binário sequer existe. Assim, systemctl start dsh termina sem erro, e a falha aparece apenas no journal. Com Type=exec, o systemd espera que execve() seja concluído com sucesso. Portanto, um erro de digitação em ExecStart= faz falhar o comando que acabou de executar, e o erro fica visível nesse ponto.
As duas respostas erradas ficam bloqueadas. Type=forking faz o systemd esperar que um processo pai termine, mas o dsh nunca termina. Por isso, o arranque fica bloqueado até TimeoutStartSec expirar, o que ocorre após 90 segundos por padrão, e depois comunica Job for dsh.service failed because a timeout was exceeded.. Type=notify espera uma mensagem READY=1 através de sd_notify. Um processo Node que nunca envia essa mensagem fica bloqueado da mesma forma. A comparação completa dos tipos de serviço do systemd aborda os restantes casos, incluindo quando vale a pena configurar notify.
Regras de reinício que falham de forma explícita
Restart=on-failure reinicia após uma saída diferente de zero ou um sinal fatal, e deixa a unidade parada depois de uma saída normal. Esse é o comportamento pretendido para uma compilação de pré-visualização. Se dsh sair alguma vez com o código 0 porque leu uma configuração que não aceitou, a unidade para e permanece parada, e systemctl status dsh mostra inactive (dead), onde é possível ver o motivo. Restart=always transforma o mesmo evento num ciclo de reinícios que parece saudável à distância.
O limite de frequência é a parte que muitas pessoas omitem. Os valores predefinidos do systemd são cinco arranques em dez segundos e, com RestartSec=5s, nunca se atingem cinco arranques numa janela de dez segundos. Assim, uma unidade que falha ao arrancar reinicia para sempre, e apenas o journal regista o problema. StartLimitIntervalSec=300 com StartLimitBurst=5 significa que cinco falhas em cinco minutos são suficientes: o systemd desiste e coloca a unidade em failed, registando Start request repeated too quickly.. Limpe esse estado com sudo systemctl reset-failed dsh depois de corrigir a causa. Ambas as definições pertencem a [Unit], não a [Service], e o systemd ignora-as silenciosamente quando estão na secção errada.
Inicie o serviço e verifique-o
sudo systemctl daemon-reload
sudo systemctl enable --now dsh
systemctl status dshenable --now executa duas tarefas. enable faz o serviço voltar a arrancar depois de um reboot, e --now inicia-o neste boot. Um systemctl start isolado deixa de existir depois do próximo reboot, e as atualizações do kernel exigem reboots.
systemctl status dsh deve mostrar Active: active (running), uma linha Main PID e uma linha Memory:. Em seguida, confirme em que endereço está a escutar:
sudo ss -lntp | grep 3080O resultado esperado é 127.0.0.1:3080. Se vir 0.0.0.0:3080, alguma alteração modificou o endereço de bind e o seu agente está exposto à Internet pública. O nome do processo nessa saída é node, não dsh, porque o binário dsh é um script Node e, por isso, pgrep -x dsh não encontra nada. Use systemctl show -p MainPID dsh.
Em seguida, faça um reboot. Um serviço que nunca sobreviveu a um reboot ainda não é um serviço.
sudo rebootVolte a ligar-se e execute systemctl is-active dsh. O comando mostra active.
Leitura dos logs com journalctl
Tudo o que o dsh escreve em stdout e stderr fica registado no journal com o nome da unidade.
journalctl -u dsh -f
journalctl -u dsh -n 200 --no-pager
journalctl -u dsh --since "10 min ago" -p err-f segue as linhas novas, -n mostra as últimas N e -p err filtra por prioridade. SyslogIdentifier=dsh na unidade explica por que essas linhas aparecem marcadas como dsh em vez de node. Isto é importante na primeira vez que consultar a saída do journal sem filtrar por unidade.
Confirme que o journal persiste entre reboots antes de precisar dele:
journalctl -u dsh -b -1Se o comando mostrar Specifying boot ID or boot offset has no effect, no persistent journal was found, o journal está em /run e é apagado a cada reboot. Crie o diretório e reinicie o daemon:
sudo mkdir -p /var/log/journal
sudo systemctl restart systemd-journaldAceda à interface por um túnel SSH, não por uma porta pública
dsh disponibiliza a interface web (user interface) em 127.0.0.1:3080 e recusa disponibilizá-la noutro local. Peça --host 0.0.0.0 e verá:
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 insteadIsto não é uma limitação a contornar. A API web (application programming interface) controla o agente, e o agente executa comandos da shell. Por isso, uma porta acessível equivale a uma shell no seu VPS para qualquer pessoa que a encontre. Os responsáveis pela manutenção indicam a ausência de autenticação remota como motivo para a ligação ficar fixa ao loopback. Vale a pena ler O que significa realmente a linha 127.0.0.1:3080 no output de arranque antes de tentar alterá-la. Em vez disso, encaminhe a porta a partir do seu próprio computador:
ssh -N -L 3080:127.0.0.1:3080 you@203.0.113.10-L 3080:127.0.0.1:3080 abre a porta 3080 no seu portátil e envia tudo o que chegar nessa porta para 127.0.0.1:3080, conforme resolvido no VPS. -N significa não executar nenhum comando remoto, pelo que a sessão serve apenas para manter o túnel aberto. Deixe-a em execução e abra http://127.0.0.1:3080/ no navegador. É aí que introduz a chave da API DeepSeek, em Settings e depois Models, e escolhe o diretório do workspace. Aponte o workspace para /var/lib/dsh/workspace, o diretório pertencente ao utilizador do serviço, caso contrário as ferramentas de ficheiros do agente falham com EACCES: permission denied.
Se a porta 3080 estiver ocupada no seu portátil, o ssh informa-o:
bind [127.0.0.1]:3080: Address already in use
channel_setup_fwd_listener_tcpip: cannot listen to port: 3080Escolha outra porta local com ssh -N -L 3081:127.0.0.1:3080 you@203.0.113.10 e navegue para http://127.0.0.1:3081/. Guarde o comando em ~/.ssh/config no seu próprio computador:
Host dsh-vps
HostName 203.0.113.10
User you
LocalForward 3080 127.0.0.1:3080Depois disso, ssh -N dsh-vps é o comando completo. Este túnel é agora a única porta de entrada para o seu agente. Por isso, é o daemon SSH que o protege: use apenas chaves, desative a autenticação por palavra-passe e aplique o restante de reforço da segurança do SSH no seu VPS com mais rigor do que o habitual. Se esse VPS evoluir para uma pequena rede privada, com uma base de dados ou um servidor de staging atrás dele, anunciar esses endereços à sua tailnet com um subnet router evita criar um encaminhamento por serviço. No entanto, a ligação ao loopback do dsh significa que a interface continua a ser acedida através de um túnel.
A chave não deve ficar no ficheiro da unidade. Os valores de Environment= são apresentados por systemctl show dsh -p Environment, que qualquer utilizador no sistema pode executar. Se um plugin instalado por si precisar de uma chave no ambiente, coloque-a em /etc/dsh.env, com modo 600 e propriedade de root, e faça referência a ela com EnvironmentFile=/etc/dsh.env. O systemd lê esse ficheiro como root no momento da execução, e systemctl show não apresenta o respetivo conteúdo. O ficheiro em disco onde cada definição é efetivamente guardada, e o que sai do seu sistema quando aponta o dsh para um endpoint Ollama local em vez da API DeepSeek, são os temas de configurar as chaves, os modelos e os endpoints do dsh.
Quanto custa executar
A inferência ocorre na API da DeepSeek, não no seu VPS. O seu servidor consome recursos com o processo Node, a interface que serve e cada comando que o agente decide executar. Os dois primeiros custos são estáveis e baixos. O terceiro não tem qualquer limite definido neste ficheiro de unidade.
Meça o valor mínimo no seu próprio servidor, em vez de confiar num valor obtido noutro sistema:
systemctl show dsh -p MemoryCurrent
systemd-cgtop -1 --depth 2MemoryCurrent é expresso em bytes. Monitorize-o enquanto o agente está a trabalhar, não quando está ocioso.
As chamadas de ferramentas são processos descendentes do serviço. Por isso, ficam no mesmo grupo de controlo e contam para os mesmos limites. Um agente que execute npm install ou uma suite de testes na área de trabalho pode usar muito mais memória do que o próprio harness. Num VPS com 1 GB, é nesse ponto que surgem as falhas: o kernel escolhe um processo e termina-o, e journalctl -k | grep -i "out of memory" mostra a linha Out of memory: Killed process, que identifica o processo escolhido. Muitas vezes, esse processo não é o que causou o problema.
A solução é definir deliberadamente um limite. MemoryMax= e CPUQuota= na secção [Service] mantêm o impacto dentro da unidade. Assim, uma compilação descontrolada é terminada, em vez de bloquear todo o servidor. Limitar a memória e a CPU com systemd explica os valores e o comportamento em caso de falha. O disco também cresce, devido ao histórico das sessões em DSH_HOME e ao que o agente grava na área de trabalho. Por isso, inclua du -sh /var/lib/dsh na ferramenta que já utiliza para monitorizar o disco.
Se pretende um agente interativo ao qual possa ligar-se e do qual possa desligar-se, um serviço não é o formato adequado. Executar um agente numa sessão tmux persistente é uma opção mais adequada. Execute o dsh como unidade quando quiser que esteja sempre ativo e acessível através de um túnel.
Modos de falha e as mensagens que verá
status=203/EXEC. o systemd não conseguiu executar o ficheiro e os logs Failed to locate executable /usr/local/bin/dsh: No such file or directory. O caminho em ExecStart= não corresponde ao que command -v dsh apresentou. Esta é a falha que Type=exec comunica no momento systemctl start, em vez de a ocultar.
status=217/USER. A conta em User= não existe. Confirme com id dsh.
status=200/CHDIR. WorkingDirectory= está em falta ou o utilizador do serviço não consegue aceder-lhe. sudo -u dsh ls /var/lib/dsh/workspace reproduz o problema diretamente.
Error: listen EADDRINUSE: address already in use 127.0.0.1:3080. Algo já está a utilizar a porta, normalmente o processo npx deixado aberto noutro terminal. sudo ss -lntp | grep 3080 identifica o processo.
EACCES: permission denied seguido de um caminho. A propriedade dentro de /var/lib/dsh está incorreta, normalmente porque a primeira execução foi feita como root ou com o HOME errado. sudo chown -R dsh:dsh /var/lib/dsh corrige o problema.
Start request repeated too quickly. A unidade atingiu o limite de tentativas de arranque e desistiu. O erro real está nas linhas anteriores. Execute sudo systemctl reset-failed dsh antes de tentar novamente.
A unidade é active (running), mas o browser não apresenta nada. Execute o teste no VPS: se curl -fsS http://127.0.0.1:3080/ -o /dev/null && echo up apresentar up nesse local, o serviço está saudável e o problema está no encaminhamento da porta.
Atualizar de forma deliberada
Fixar uma versão significa que a atualização é uma ação sua, não algo que acontece sem controlo. Leia primeiro as notas da versão, porque o aviso do próprio projeto upstream sobre alterações incompatíveis é precisamente a razão para fixar a versão. Faça uma cópia de segurança do diretório de estado e, em seguida, troque a versão:
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-pagerReverter é o mesmo npm install -g com a versão anterior, além de restaurar esse arquivo tar, o que só funciona se tiver feito essa cópia. Um runtime de agente em fase de pré-visualização é precisamente o tipo de software em que uma atualização pode reescrever o formato da configuração sem aviso.
FAQ
Por que o dsh para quando fecho a minha sessão SSH?
Porque npx @deepseek-ai/dsh web é um processo em primeiro plano pertencente à sua sessão de início de sessão, por isso é terminado quando a sessão acaba. Uma unidade systemd pertence, em vez disso, ao sistema init, razão pela qual continua em execução depois de desligar e volta a iniciar após um reboot. sudo systemctl enable --now dsh é o par de passos que lhe dá ambos: enable para o reboot e --now para este boot.
Devo usar Type=simple ou Type=exec para o dsh?
Type=exec. O dsh é executado em primeiro plano e nunca cria um processo filho, por isso ambos funcionam, mas Type=exec faz o systemd esperar que execve() termine com sucesso antes de considerar o arranque bem-sucedido. Um caminho incorreto em ExecStart= falha então systemctl start com status=203/EXEC visível. Com Type=simple, o mesmo erro devolve sucesso e fica oculto no journal. Type=forking e Type=notify estão ambos incorretos neste caso, e ambos ficam bloqueados até TimeoutStartSec expirar após 90 segundos.
Como abro a interface web do dsh a partir do meu portátil?
Encaminhe a porta por SSH: ssh -N -L 3080:127.0.0.1:3080 you@your-vps e abra http://127.0.0.1:3080/ no navegador. Não tente associar o serviço a um endereço público. O dsh rejeita --host 0.0.0.0 com 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, porque a API web pode fazer o agente executar comandos shell e não existe autenticação remota à frente dela.
Posso executar o dsh como root para simplificar as permissões?
Não. O harness existe para executar comandos e escrever ficheiros, por isso os privilégios do serviço são também os privilégios do agente. Crie uma conta de sistema com useradd --system --shell /usr/sbin/nologin dsh, atribua-lhe a propriedade de /var/lib/dsh e adicione NoNewPrivileges=true à unidade. Se depois encontrar EACCES: permission denied, a causa habitual é uma execução anterior como root ter deixado ficheiros pertencentes a root; sudo chown -R dsh:dsh /var/lib/dsh remove-os.
Que versão do dsh devo fixar na unidade?
A versão que npm view @deepseek-ai/dsh version apresentar quando configurar o serviço, instalada com npm install -g @deepseek-ai/dsh@<that version> e registada num local que possa consultar. 0.1.0-rc.7 era a versão atual em 18 de agosto de 2026. O importante não é o número, mas o facto de npx, sem uma versão, resolver o pacote no momento do arranque; assim, um reinício não supervisionado pode transferi-lo silenciosamente para uma build com um formato de configuração diferente.