Executar dsh numa VPS com systemd sem terminal
Configure o dsh como serviço systemd numa VPS, com utilizador dedicado, versão fixa, Restart, logs no journalctl e túnel SSH para a interface.
Executar o dsh sem terminal numa VPS
Executar o dsh sem terminal numa VPS requer apenas um ficheiro de unidade do systemd e um utilizador dedicado para o executar. 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 August 2026. O quickstart indica que deve executar npx @deepseek-ai/dsh web. Isso está correto, mas o processo também termina assim que fechar a sessão SSH (secure shell).
Um ficheiro de unidade resolve quatro problemas de uma só vez. O serviço volta a arrancar depois de um reboot. O output é enviado para o journal em vez de desaparecer do terminal. O processo é 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 upstream avisa explicitamente:
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 não funcionar, comece por instalar o DeepSeek Harness numa VPS e volte quando npx @deepseek-ai/dsh web servir uma página.
Primeiro o Node, porque o npm não irá avisar
node -vO pacote fornecido pelo 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. Em vez disso, a falha ocorre em tempo de execução, como um erro de sintaxe ou um módulo integrado em falta. É um momento muito pior para detetar o problema. 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.
Comprove que funciona antes de escrever uma unit
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 na interface de 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 unit que tente associar-se a uma porta já ocupada 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 fixe depois 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 inicia. Por isso, 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 do seu lado. Também precisa de conseguir aceder ao registo npm durante o arranque. Se o registo estiver lento nesse dia, uma máquina que funcionava passa a ter uma unidade com falha. Instale uma vez, usando uma versão que tenha registado:
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 efetivamente 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.
Um utilizador que é proprietário do serviço e de mais nada
O agente executa comandos 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 início de sessão.
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 guarda 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. Esse primeiro arranque grava ficheiros e pode obter conjuntos de plugins. Por isso, faça-o manualmente, para poder monitorizá-lo.
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. O comportamento de sudo ao reescrever HOME para um comando que não é de início de sessão depende da definição set_home em /etc/sudoers. Se a configuração estiver errada, a primeira execução cria diretórios de cache na sua home pessoal, pertencentes a dsh. Mais tarde, o serviço não consegue 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 de caminhos fixa, mas essa lista não é o seu PATH da shell. Por isso, um caminho absoluto elimina a possibilidade de erro.
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 que disponibiliza ao agente. Se o diretório não existir ou o utilizador do serviço não puder aceder-lhe, a unidade falha com status=200/CHDIR antes de o dsh ser executado.
ProtectHome=true oculta /home e /root do processo. Isto é seguro neste caso porque tudo o que o serviço utiliza está localizado em /var/lib/dsh. Aponte o workspace para um caminho sob /home. Nesse caso, o agente comunicará que o diretório não existe. Isto pode causar confusão até se considerar esta linha. ProtectSystem=full torna /usr, /boot e /etc só de leitura. O serviço nunca precisa de escrever nesses locais.
Ir mais longe é tentador e, normalmente, está errado. ProtectSystem=strict torna todo o sistema de ficheiros só de leitura, exceto os pseudo-sistemas de ficheiros do kernel. Por isso, a primeira chamada de ferramenta que escrever num ficheiro falha com EROFS: read-only file system. Se precisar desse nível de isolamento, 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 cria um processo filho. A vantagem em relação ao comportamento predefinido é obter uma mensagem de erro real. Com Type=simple, o systemd considera o arranque bem-sucedido assim que o processo cria um processo filho, antes de saber se o binário sequer existe. Por isso, systemctl start dsh termina sem erro, e a falha só aparece no journal. Com Type=exec, o systemd espera que execve() seja concluído com sucesso. Assim, um erro de digitação em ExecStart= faz falhar o comando que acabou de executar, e o erro fica visível nesse comando.
As duas respostas erradas ficam bloqueadas. Type=forking diz ao systemd para esperar que um processo pai termine, mas o dsh nunca termina. Por isso, o arranque fica bloqueado até TimeoutStartSec expirar (90 segundos por predefiniçã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, mas um processo Node que nunca envia essa mensagem fica bloqueado da mesma forma. A comparação completa dos tipos de serviço do systemd explica os restantes casos, incluindo quando vale a pena configurar notify.
Regras de reinício que falham de forma evidente
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 com o código 0 depois de ler uma configuração que não aceitou, a unidade para e permanece parada, e systemctl status dsh mostra inactive (dead), onde pode consultar o motivo. Restart=always transforma o mesmo evento num ciclo de reinício que parece saudável à distância.
O limite de frequência é a parte que muitas pessoas omitem. As predefinições do systemd permitem cinco arranques em dez segundos. Com RestartSec=5s, nunca são atingidos cinco arranques num intervalo de dez segundos. Assim, uma unidade que falha ao arrancar reinicia indefinidamente e só 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 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 estar ativo depois do reboot seguinte, e as atualizações do kernel implicam reboots.
systemctl status dsh deve mostrar Active: active (running), um Main PID e uma linha Memory:. Em seguida, confirme onde está à escuta:
sudo ss -lntp | grep 3080O resultado esperado é 127.0.0.1:3080. Se aparecer 0.0.0.0:3080, algo alterou 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, portanto pgrep -x dsh não encontra nada. Use systemctl show -p MainPID dsh em vez disso.
Depois, faça um reboot. Um serviço que nunca sobreviveu a um reboot ainda não é um serviço.
sudo rebootLigue-se novamente e execute systemctl is-active dsh. O comando mostra active.
Ler os logs com journalctl
Tudo o que 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, -p err filtra por prioridade. SyslogIdentifier=dsh na unidade explica por que essas linhas têm a etiqueta dsh em vez de node, o que é importante na primeira vez que consulta 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 esse comando mostrar Specifying boot ID or boot offset has no effect, no persistent journal was found, o journal está em /run e é eliminado 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 em 127.0.0.1:3080 e recusa disponibilizá-la noutro local. Peça --host 0.0.0.0 e o comando termina 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 insteadIsto não é uma limitação a contornar. A API web controla o agente, e o agente executa comandos shell. Portanto, uma porta acessível dá uma shell no seu VPS a qualquer pessoa que a encontre. Os responsáveis indicam a ausência de autenticação remota como motivo para o bind estar fixo ao loopback. Encaminhe a porta a partir da sua própria máquina:
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 aí para 127.0.0.1:3080, conforme resolvido no VPS. -N significa não executar nenhum comando remoto, pelo que a sessão apenas mantém o túnel aberto. Deixe-o 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 da área de trabalho. Aponte a área de trabalho para /var/lib/dsh/workspace, o diretório pertencente ao utilizador do serviço, ou 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 na sua própria máquina:
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 passa a ser a única porta de acesso ao seu agente, pelo que o daemon SSH é o elemento que o protege: use apenas chaves, desative a autenticação por palavra-passe e aplique o restante de endurecimento do SSH no seu VPS com mais rigor do que o habitual.
A chave não deve ficar no ficheiro da unidade. Os valores Environment= são apresentados por systemctl show dsh -p Environment, que qualquer utilizador no sistema pode executar. Se um plugin instalado precisar de uma chave no ambiente, coloque-a em /etc/dsh.env, com o modo 600 e pertencente a 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 seu conteúdo.
O custo de execução
A inferência ocorre na API da DeepSeek, não no seu VPS. O seu servidor paga pelo processo Node, pela interface que serve e por cada comando que o agente decide executar. Os dois primeiros custos são estáveis e baixos. O terceiro não é limitado por nada neste ficheiro de unidade.
Meça o valor mínimo no seu próprio servidor em vez de confiar num número obtido no servidor de outra pessoa:
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á inativo.
As chamadas de ferramentas são processos filhos 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 suíte de testes dentro do workspace pode usar muito mais memória do que o próprio harness. Num VPS com 1 GB, é aí que surgem os problemas: o kernel escolhe um processo e termina-o, e journalctl -k | grep -i "out of memory" mostra a linha Out of memory: Killed process com o nome do processo escolhido. Esse processo muitas vezes não é o que causou o problema.
A solução é definir um limite de forma intencional. MemoryMax= e CPUQuota= na secção [Service] mantêm o impacto dentro da unidade. Assim, um build descontrolado é terminado em vez de bloquear o servidor inteiro. Limitar a memória e a CPU com systemd explica os valores e o comportamento em caso de falha. O disco também aumenta, devido ao histórico das sessões em DSH_HOME e ao que o agente escreve no workspace. Por isso, inclua du -sh /var/lib/dsh na ferramenta que já utiliza para monitorizar o espaço em disco.
Se pretende um agente interativo ao qual possa ligar-se e desligar-se, um serviço não é o formato adequado. Nesse caso, executar um agente numa sessão tmux persistente é uma opção melhor. Execute o dsh como unidade quando quiser que esteja sempre ativo e acessível através de um túnel.
Modos de falha e 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 entrar nesse diretório. sudo -u dsh ls /var/lib/dsh/workspace reproduz o problema diretamente.
Error: listen EADDRINUSE: address already in use 127.0.0.1:3080. Já existe algo a ocupar a porta, normalmente o processo npx deixado em execução 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-a.
Start request repeated too quickly. A unidade atingiu o limite de frequência de arranque e desistiu. O erro real está nas linhas anteriores. Execute sudo systemctl reset-failed dsh antes de tentar novamente.
A unidade está active (running), mas o navegador não mostra nada. Execute a verificação na VPS: se curl -fsS http://127.0.0.1:3080/ -o /dev/null && echo up apresentar up aí, o serviço está saudável e o problema está no encaminhamento da porta.
Atualizar de forma controlada
Fixar uma versão significa que a atualização é uma ação deliberada, não algo que acontece sem controlo. Leia primeiro as notas da versão, porque o aviso do próprio projeto sobre alterações incompatíveis é precisamente o motivo para fixar a versão. Faça uma cópia de segurança do diretório de estado e, em seguida, altere 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 é executar o mesmo npm install -g com a versão antiga e restaurar esse arquivo tar. Isto só funciona se tiver criado esse arquivo. 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 sessão SSH?
Porque npx @deepseek-ai/dsh web é um processo em primeiro plano pertencente à sua sessão de login, por isso é terminado quando a sessão acaba. Uma unidade systemd pertence ao sistema init, razão pela qual continua em execução depois de se desligar e volta a arrancar após um reboot. sudo systemctl enable --now dsh é o conjunto de passos que lhe dá os dois comportamentos: 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 fork, por isso ambos funcionam, mas Type=exec faz o systemd esperar que execve() tenha 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 errados neste caso, e ambos ficam bloqueados até TimeoutStartSec expirar, após 90 segundos.
Como abro a interface web do dsh no meu laptop?
Encaminhe a porta através de SSH: ssh -N -L 3080:127.0.0.1:3080 you@your-vps, e abra http://127.0.0.1:3080/ no browser. 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 à sua frente.
Posso executar o dsh como root para simplificar as permissões?
Não. O harness existe para executar comandos e escrever ficheiros, por isso as permissões do serviço são também as permissões 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, e sudo chown -R dsh:dsh /var/lib/dsh resolve o problema.
Que versão do dsh devo fixar na unidade?
A versão que npm view @deepseek-ai/dsh version indicar quando configurar o serviço, instalada com npm install -g @deepseek-ai/dsh@<that version> e registada num local que consiga consultar. 0.1.0-rc.7 era a versão atual em 18 August 2026. O ponto não é o número, mas o facto de npx sem uma versão resolver o pacote no momento do arranque, o que significa que um reinício não supervisionado pode transferi-lo silenciosamente para uma build com um formato de configuração diferente.