Come eseguire dsh senza terminale su un VPS con systemd
Configura dsh come servizio systemd su un VPS: utente dedicato, versione bloccata, riavvio automatico, log con journalctl e tunnel SSH per l'interfaccia.
Eseguire dsh senza terminale su un VPS
Eseguire dsh senza terminale su un VPS richiede un solo file di unità systemd e un utente dedicato che ne sia il proprietario. dsh è il launcher da riga di comando per DeepSeek Harness, il runtime per agenti di DeepSeek, pubblicato con licenza MIT in developer preview ad agosto 2026. La guida rapida indica di digitare npx @deepseek-ai/dsh web. È corretto, ma il processo termina non appena si chiude la sessione SSH (secure shell).
Un file di unità risolve quattro aspetti contemporaneamente. Il servizio viene riavviato dopo un reboot. L'output viene scritto nel journal invece di scorrere sul terminale. Il processo viene eseguito con un account diverso da root. Inoltre, viene eseguita la versione scelta. Questo aspetto è ancora più importante del solito perché il progetto upstream lo dichiara esplicitamente:
DeepSeek Harness è attualmente in developer preview e viene aggiornato rapidamente. CI SARANNO MODIFICHE CHE ROMPERANNO LA COMPATIBILITÀ.
Questa guida presuppone che dsh funzioni già quando lo esegui manualmente. In caso contrario, inizia da installare DeepSeek Harness su un VPS e torna qui quando npx @deepseek-ai/dsh web pubblica una pagina.
Prima Node, perché npm non ti avviserà
node -vIl pacchetto fornito da Ubuntu 24.04 è Node 18 (18.19.1 ad agosto 2026), una versione obsoleta per un pacchetto pubblicato quest'anno. @deepseek-ai/dsh non pubblica alcun campo engines, quindi npm non mostra alcun avviso EBADENGINE quando la versione di Node è troppo vecchia. L'errore si manifesta invece in fase di esecuzione, come errore di sintassi o modulo integrato mancante. È un momento molto peggiore per rilevarlo. Installa una versione corrente con supporto a lungo termine (LTS) da 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 dovrebbe ora visualizzare una versione v22. La riga less serve perché reindirizzare direttamente uno script remoto a bash esegue codice che non hai letto.
Verifica che venga eseguito prima di scrivere un'unità
npx @deepseek-ai/dsh@0.1.0-rc.7 webLascia il comando in esecuzione. Da una seconda sessione SSH:
curl -fsS http://127.0.0.1:3080/ -o /dev/null && echo upup indica che il profilo web è in ascolto sull'interfaccia di loopback, su cui viene associato per impostazione predefinita. curl: (7) Failed to connect to 127.0.0.1 port 3080: Connection refused indica il contrario; il primo terminale spiega il motivo. Interrompi l'esecuzione manuale con Ctrl+C prima di procedere: un'unità che tenta di associare una porta già utilizzata da un altro processo non viene avviata e restituisce Error: listen EADDRINUSE: address already in use 127.0.0.1:3080.
0.1.0-rc.7 era la versione pubblicata il 18 agosto 2026. Verifica quale versione è attuale con npm view @deepseek-ai/dsh version, quindi fissa la versione che decidi di eseguire.
Installa globalmente la versione fissata
npx è lo strumento sbagliato all'interno di un file unit. Risolve la versione del pacchetto quando il processo viene avviato, quindi un riavvio tra tre mesi può avviare una build diversa di un agent in anteprima, senza alcuna modifica da parte tua. Inoltre, all'avvio deve poter raggiungere il registro npm; se quel giorno il registro è lento, una macchina funzionante può trasformarsi in un'unità non riuscita. Esegui l'installazione una sola volta, specificando la versione che hai annotato:
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 stampa /usr/bin/dsh quando npm proviene da NodeSource e /usr/local/bin/dsh quando proviene dal pacchetto fornito da Ubuntu. Nel file unit usa il percorso effettivamente stampato. npm ls -g stampa la versione esatta. È il dato che ti serve tra sei settimane, quando il comportamento cambia e non ricordi più che cosa avevi installato.
Un utente che possiede il servizio e nient'altro
L'agente esegue comandi shell. È il suo compito. Se lo esegui come root, ogni chiamata agli strumenti viene eseguita come root. Assegnagli quindi un account dedicato senza shell di 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 diventa DSH_HOME, la directory in cui dsh conserva i profili. Un profilo è uno stack denominato di bundle di plugin con un proprio livello di patch. I profili web e headless vengono creati automaticamente a partire dai template forniti al primo avvio. Il primo avvio scrive file e può scaricare bundle, quindi eseguilo manualmente quando puoi monitorarne l'esecuzione.
sudo -u dsh env HOME=/var/lib/dsh DSH_HOME=/var/lib/dsh/harness /usr/bin/dsh --profile webImposta esplicitamente HOME invece di affidarti al modo in cui sudo lo gestisce, perché la riscrittura di HOME da parte di sudo per un comando non di login dipende dall'impostazione set_home in /etc/sudoers. Se la configurazione è errata, alla prima esecuzione le directory della cache vengono create nella tua home directory e appartengono a dsh. In seguito il servizio non riesce a trovare il proprio stato. Interrompilo con Ctrl+C quando il controllo curl restituisce up.
Il file dell'unità
Scrivi /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= usa il percorso assoluto ottenuto da command -v dsh. systemd cerca un elenco di percorsi predefinito quando viene specificato il nome semplice di un comando, ma questo elenco non corrisponde a PATH della shell. Un percorso assoluto elimina quindi ogni ambiguità.
WorkingDirectory= è la directory da cui vengono risolti i percorsi relativi ed è il punto di partenza di una chiamata a uno strumento che esegue ls senza argomenti. Impostala sulla directory di lavoro che assegni all'agente. Se la directory non esiste o l'utente del servizio non può accedervi, l'unità termina con status=200/CHDIR prima ancora che venga eseguito dsh.
ProtectHome=true nasconde /home e /root al processo. In questo caso è sicuro, perché tutto ciò che il servizio usa si trova sotto /var/lib/dsh. Se imposti la directory di lavoro su un percorso sotto /home, l'agente segnalerà che la directory non esiste. Il motivo può risultare poco evidente finché non si considera questa riga. ProtectSystem=full rende /usr, /boot e /etc di sola lettura, cosa che il servizio non deve mai modificare.
È naturale voler applicare ulteriori restrizioni, ma in genere è la scelta sbagliata. ProtectSystem=strict rende l'intero filesystem di sola lettura, ad eccezione dei pseudo-filesystem del kernel. Di conseguenza, la prima chiamata a uno strumento che scrive un file termina con EROFS: read-only file system. Se vuoi applicare questo livello di isolamento, aggiungi ReadWritePaths=/var/lib/dsh nella stessa modifica.
A quale Type= appartiene
Type=exec, perché dsh resta in primo piano e non esegue fork. Il vantaggio rispetto al valore predefinito è un messaggio di errore effettivo. Con Type=simple, systemd considera l'avvio riuscito non appena il processo ha eseguito il fork, prima di sapere se il binario esiste davvero; di conseguenza systemctl start dsh termina senza errori e il problema compare soltanto nel journal. Con Type=exec, systemd attende che execve() abbia esito positivo, quindi un errore di battitura in ExecStart= fa fallire il comando appena eseguito, mostrando subito il problema.
Le due risposte errate causano entrambe un blocco. Type=forking indica a systemd di attendere la terminazione di un processo padre, ma dsh non termina mai; l'avvio resta quindi bloccato finché non scade TimeoutStartSec (90 secondi per impostazione predefinita), dopodiché segnala Job for dsh.service failed because a timeout was exceeded. Type=notify attende un messaggio READY=1 tramite sd_notify, ma un processo Node che non ne invia alcuno resta bloccato nello stesso modo. Il confronto completo dei tipi di servizio systemd illustra gli altri aspetti, compreso quando vale la pena configurare notify.
Regole di riavvio che segnalano chiaramente gli errori
Restart=on-failure esegue il riavvio dopo un’uscita diversa da zero o dopo un segnale fatale e lascia l’unità ferma dopo un’uscita regolare. Questo è il comportamento desiderato per una build di anteprima. Se dsh termina con codice 0 perché ha letto una configurazione non valida, l’unità si arresta e rimane arrestata; systemctl status dsh mostra inactive (dead), dove è possibile verificarlo. Restart=always trasforma lo stesso evento in un ciclo di riavvii che, a distanza, sembra normale.
Il limite di frequenza è l’aspetto che spesso viene omesso. I valori predefiniti di systemd consentono cinque avvii nell’arco di dieci secondi e, con RestartSec=5s, non si raggiungono mai cinque avvii in una finestra di dieci secondi. Di conseguenza, un’unità che si arresta durante l’avvio viene riavviata senza fine e soltanto il journal registra l’accaduto. StartLimitIntervalSec=300 insieme a StartLimitBurst=5 significa che cinque errori in cinque minuti sono sufficienti: systemd rinuncia e lascia l’unità nello stato failed, registrando Start request repeated too quickly.. Cancellare questo stato con sudo systemctl reset-failed dsh dopo aver corretto la causa. Entrambe le impostazioni appartengono a [Unit], non a [Service], e systemd le ignora senza messaggi se si trovano nella sezione errata.
Avvialo, quindi controlla
sudo systemctl daemon-reload
sudo systemctl enable --now dsh
systemctl status dshenable --now svolge due funzioni. enable riavvia il servizio dopo un riavvio del sistema, mentre --now lo avvia durante questo boot. Un semplice systemctl start non persiste dopo il riavvio successivo, e gli aggiornamenti del kernel richiedono i riavvii.
systemctl status dsh dovrebbe mostrare Active: active (running), una Main PID e una riga Memory:. Verifica quindi su quale indirizzo è in ascolto:
sudo ss -lntp | grep 3080Devi ottenere 127.0.0.1:3080. Se visualizzi 0.0.0.0:3080, qualcosa ha modificato l'indirizzo di bind e il tuo agent è esposto a Internet. Il nome del processo nell'output è node, non dsh, perché il binario dsh è uno script Node, quindi pgrep -x dsh non trova nulla. Usa invece systemctl show -p MainPID dsh.
Riavvia quindi il sistema una volta. Un servizio che non ha mai superato un riavvio non è ancora realmente un servizio.
sudo rebootRiconnettiti ed esegui systemctl is-active dsh. Il comando stampa active.
Lettura dei log con journalctl
Tutto ciò che dsh scrive su stdout e stderr viene registrato nel journal con il nome dell'unità.
journalctl -u dsh -f
journalctl -u dsh -n 200 --no-pager
journalctl -u dsh --since "10 min ago" -p err-f segue le nuove righe, -n mostra le ultime N righe, -p err filtra in base alla priorità. SyslogIdentifier=dsh nell'unità è il motivo per cui quelle righe sono contrassegnate come dsh invece di node. Questo è importante la prima volta che si legge l'output del journal senza filtrarlo per unità.
Verificate che il journal sopravviva ai riavvii prima di averne bisogno:
journalctl -u dsh -b -1Se il comando restituisce Specifying boot ID or boot offset has no effect, no persistent journal was found, il journal risiede in /run e viene eliminato a ogni riavvio. Create la directory e riavviate il demone:
sudo mkdir -p /var/log/journal
sudo systemctl restart systemd-journaldAccedere all'interfaccia tramite un tunnel SSH, non tramite una porta pubblica
dsh fornisce l'interfaccia web su 127.0.0.1:3080 e rifiuta di servirla altrove. Se si richiede --host 0.0.0.0, si arresta con questo messaggio:
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 insteadNon è una limitazione da aggirare. L'API web controlla l'agent e l'agent esegue comandi shell, quindi una porta raggiungibile equivale a una shell sul VPS per chiunque la individui. I manutentori indicano come motivo del bind fisso al loopback il fatto che l'autenticazione remota non sia ancora implementata. Inoltra invece la porta dal tuo computer:
ssh -N -L 3080:127.0.0.1:3080 you@203.0.113.10-L 3080:127.0.0.1:3080 apre la porta 3080 sul laptop e invia tutto ciò che vi arriva a 127.0.0.1:3080, risolto sul VPS. -N indica di non eseguire alcun comando remoto, quindi la sessione mantiene aperto soltanto il tunnel. Lascialo in esecuzione e apri http://127.0.0.1:3080/ nel browser. Qui inserisci la chiave API di DeepSeek, in Settings e poi Models, e selezioni la directory del workspace. Imposta il workspace su /var/lib/dsh/workspace, la directory di proprietà dell'utente del servizio, altrimenti gli strumenti per i file dell'agent falliscono con EACCES: permission denied.
Se la porta 3080 è già occupata sul laptop, ssh lo segnala:
bind [127.0.0.1]:3080: Address already in use
channel_setup_fwd_listener_tcpip: cannot listen to port: 3080Scegli un'altra porta locale con ssh -N -L 3081:127.0.0.1:3080 you@203.0.113.10, quindi apri http://127.0.0.1:3081/ nel browser. Salva il comando digitato in ~/.ssh/config sul tuo computer:
Host dsh-vps
HostName 203.0.113.10
User you
LocalForward 3080 127.0.0.1:3080Dopodiché, ssh -N dsh-vps è l'intero comando. Questo tunnel è ora l'unico accesso al tuo agent, quindi è il demone SSH a proteggerlo: solo chiavi, nessuna autenticazione con password, e il resto delle misure di hardening di SSH sul VPS si applica con un livello di rigore ancora maggiore.
La chiave non deve essere inserita nel file unit. I valori Environment= vengono stampati da systemctl show dsh -p Environment, che qualsiasi utente sul sistema può eseguire. Se un plugin installato richiede una chiave nell'ambiente, inseriscila in /etc/dsh.env con permessi 600 e proprietà di root, quindi referenziala con EnvironmentFile=/etc/dsh.env. systemd legge quel file come root al momento dell'esecuzione e systemctl show non ne stampa il contenuto.
Costi di esecuzione
L’inferenza avviene tramite l’API di DeepSeek, non sul VPS. Il server esegue il processo Node, serve l’interfaccia e lancia ogni comando che l’agente decide di eseguire. I primi due consumi sono stabili e ridotti. Il terzo non ha alcun limite definito in questo file unit.
Misura il valore minimo sul tuo server, invece di affidarti a un dato rilevato su un altro sistema:
systemctl show dsh -p MemoryCurrent
systemd-cgtop -1 --depth 2MemoryCurrent è espresso in byte. Controllalo mentre l’agente sta lavorando, non quando è inattivo.
Le chiamate agli strumenti sono processi figli del servizio. Per questo finiscono nello stesso control group e sono soggette agli stessi limiti. Un agente che esegue npm install o una suite di test nella workspace può consumare molta più memoria del processo harness. Su un VPS con 1 GB è questo il punto in cui si verificano i problemi: il kernel seleziona un processo e lo termina, mentre journalctl -k | grep -i "out of memory" mostra la riga Out of memory: Killed process con il nome del processo scelto. Spesso non è il processo che ha causato il problema.
La soluzione consiste nell’impostare esplicitamente un limite. MemoryMax= e CPUQuota= nella sezione [Service] mantengono l’impatto all’interno dell’unità. In questo modo una build fuori controllo viene terminata, invece di bloccare l’intero server. Limitare memoria e CPU con systemd descrive i valori da usare e il comportamento in caso di errore. Anche lo spazio su disco aumenta, a causa della cronologia delle sessioni in DSH_HOME e dei file che l’agente scrive nella workspace. Inserisci quindi du -sh /var/lib/dsh nel sistema che usi già per monitorare lo spazio su disco.
Se ti serve un agente interattivo a cui collegarti e da cui scollegarti, un servizio non è la soluzione adatta. In questo caso è preferibile eseguire un agente in una sessione tmux persistente. Esegui dsh come unit quando vuoi che sia sempre attivo e raggiungibile tramite un tunnel.
Modalità di errore e stringhe visualizzate
status=203/EXEC. systemd non ha potuto eseguire il file e i log Failed to locate executable /usr/local/bin/dsh: No such file or directory. Il percorso in ExecStart= non corrisponde a quello mostrato da command -v dsh. Questo è l’errore segnalato da Type=exec al momento systemctl start, invece di essere nascosto.
status=217/USER. L’account in User= non esiste. Verificarlo con id dsh.
status=200/CHDIR. WorkingDirectory= non è disponibile oppure l’utente del servizio non può accedervi. sudo -u dsh ls /var/lib/dsh/workspace riproduce direttamente il problema.
Error: listen EADDRINUSE: address already in use 127.0.0.1:3080. Un altro processo utilizza già la porta, in genere perché l’esecuzione di npx è rimasta aperta in un altro terminale. sudo ss -lntp | grep 3080 identifica il processo.
EACCES: permission denied seguito da un percorso. La proprietà dei file in /var/lib/dsh è errata, normalmente perché una prima esecuzione è stata effettuata come root o con HOME errato. sudo chown -R dsh:dsh /var/lib/dsh risolve il problema.
Start request repeated too quickly. L’unità ha raggiunto il limite di frequenza degli avvii e ha interrotto i tentativi. L’errore effettivo si trova nelle righe precedenti. Eseguire sudo systemctl reset-failed dsh prima di riprovare.
L’unità è active (running), ma il browser non mostra nulla. Eseguire il controllo sul VPS: se curl -fsS http://127.0.0.1:3080/ -o /dev/null && echo up stampa up, il servizio è operativo e il problema riguarda il port forwarding.
Aggiornare intenzionalmente
Il version pinning fa sì che un aggiornamento sia un'operazione eseguita dall'amministratore, non un evento che si verifica senza controllo. Leggete prima le note di rilascio, perché l'avviso del progetto upstream sulle modifiche che possono compromettere la compatibilità è proprio il motivo per cui si blocca la versione. Eseguite il backup della directory di stato, quindi sostituite la versione:
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-pagerIl rollback segue la stessa procedura npm install -g con la versione precedente e richiede inoltre il ripristino dell'archivio tar, possibile soltanto se ne avete creato una copia. Un runtime dell'agente in fase di anteprima è proprio il tipo di software in cui un aggiornamento può riscrivere il formato di configurazione senza preavviso.
FAQ
Perché dsh si interrompe quando chiudo la sessione SSH?
Perché npx @deepseek-ai/dsh web è un processo in primo piano appartenente alla sessione di accesso, quindi viene terminato quando la sessione finisce. Un'unità systemd appartiene invece al sistema init, perciò continua a funzionare dopo la disconnessione e viene riavviata dopo un reboot. sudo systemctl enable --now dsh è la coppia di passaggi che offre entrambe le cose: enable per il reboot, --now per questo boot.
Devo usare Type=simple o Type=exec per dsh?
Type=exec. dsh viene eseguito in primo piano e non crea processi figli, quindi entrambe le opzioni funzionano, ma Type=exec fa attendere systemd che execve() abbia esito positivo prima di considerare riuscito l'avvio. Un percorso errato in ExecStart= fa quindi fallire systemctl start con status=203/EXEC visibile immediatamente. Con Type=simple lo stesso errore restituisce un esito positivo e rimane nascosto nel journal. Type=forking e Type=notify sono entrambi errati in questo caso e rimangono in attesa finché TimeoutStartSec scade dopo 90 secondi.
Come apro l'interfaccia Web di dsh dal laptop?
Inoltra la porta tramite SSH: ssh -N -L 3080:127.0.0.1:3080 you@your-vps, quindi apri http://127.0.0.1:3080/ nel browser. Non provare a collegare il servizio a un indirizzo pubblico. dsh rifiuta --host 0.0.0.0 con 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, perché l'API Web può fare eseguire comandi shell all'agent e non è presente alcuna autenticazione remota davanti a essa.
Posso eseguire dsh come root per semplificare la gestione dei permessi?
No. L'harness serve a eseguire comandi e scrivere file, quindi i privilegi del servizio sono anche i privilegi disponibili all'agent. Crea un account di sistema con useradd --system --shell /usr/sbin/nologin dsh, assegnagli /var/lib/dsh e aggiungi NoNewPrivileges=true all'unità. Se in seguito ottieni EACCES: permission denied, la causa usuale è un'esecuzione precedente come root che ha lasciato file di proprietà di root; sudo chown -R dsh:dsh /var/lib/dsh risolve il problema.
Quale versione di dsh devo fissare nell'unità?
Quella indicata da npm view @deepseek-ai/dsh version quando configuri il servizio, installata con npm install -g @deepseek-ai/dsh@<that version> e registrata in un punto che potrai ritrovare. 0.1.0-rc.7 era la versione corrente il 18 agosto 2026. Il numero non è l'aspetto importante: npx senza una versione risolve il pacchetto al momento dell'avvio, quindi un riavvio non presidiato può spostarti silenziosamente su una build con un formato di configurazione diverso.