Esegui dsh su VPS con systemd senza terminale
Configura dsh come servizio systemd su un VPS: utente dedicato, versione bloccata, riavvio automatico, log con journalctl e tunnel SSH per la UI.
Esegui 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 a riga di comando per DeepSeek Harness, il runtime per agenti di DeepSeek, pubblicato con licenza MIT in developer preview nell'agosto 2026. Un harness è il programma che circonda il modello, non il modello stesso. Pertanto, ciò che esegui tramite systemd sono il ciclo di esecuzione, gli strumenti e le autorizzazioni, non l'inferenza di DeepSeek. La guida introduttiva indica di digitare npx @deepseek-ai/dsh web, ed è corretto. Tuttavia, il processo termina non appena chiudi la sessione SSH (secure shell).
Un file di unità risolve quattro aspetti contemporaneamente. Il servizio viene riavviato dopo un reboot. Il suo output viene scritto nel journal invece di scorrere oltre il terminale. Il servizio viene eseguito con un account diverso da root. Inoltre, viene eseguita la versione scelta, un aspetto ancora più importante in questo caso 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 avvii manualmente. In caso contrario, inizia da installare DeepSeek Harness su un VPS e torna qui quando npx @deepseek-ai/dsh web serve una pagina.
Prima Node, perché npm non segnala il problema
node -vIl pacchetto fornito da Ubuntu 24.04 contiene 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 verifica invece durante l'esecuzione, sotto forma di errore di sintassi o di funzionalità integrata mancante. È un momento molto peggiore per rilevare il problema. 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 stampare una versione v22. La riga less serve perché reindirizzare direttamente uno script remoto a bash esegue codice che non hai letto.
Verifica che funzioni prima di scrivere una unit
npx @deepseek-ai/dsh@0.1.0-rc.7 webLascia il processo 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, che è quella usata per impostazione predefinita. curl: (7) Failed to connect to 127.0.0.1 port 3080: Connection refused indica che non lo è e il primo terminale mostra il motivo. Arresta l'esecuzione manuale con Ctrl+C prima di procedere: una unit che tenta di mettersi in ascolto su una porta già occupata da un altro processo non si avvia 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 è attualmente disponibile con npm view @deepseek-ai/dsh version, quindi blocca la versione che hai scelto 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. Di conseguenza, un riavvio tra tre mesi può avviare una build diversa di un agent in fase di anteprima, senza alcuna modifica da parte tua. Inoltre, all'avvio deve poter raggiungere il registro npm. Se il registro è lento, una macchina funzionante può quindi trasformarsi in un'unità non avviabile. Esegui una sola installazione, specificando una versione annotata:
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. Usa nel file unit il percorso effettivamente stampato. npm ls -g stampa la versione esatta. È il dato che ti servirà tra sei settimane, quando il comportamento cambia e non ricordi più che cosa avevi installato. Se l'installazione non riesce, se in seguito command -v dsh non stampa nulla oppure se la versione restituita non corrisponde a quella richiesta, consulta i consueti problemi di installazione e versione di dsh prima di scrivere il file unit.
Un utente proprietario del servizio e di nient'altro
L'agente esegue comandi shell. Questo è il suo compito. Eseguirlo come root fa sì che ogni chiamata agli strumenti venga 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, e i profili web e headless vengono creati automaticamente a partire dai template forniti la prima volta che li avvii. Qualsiasi elemento aggiunto successivamente a quello stack viene eseguito con questo utente, usando l'accesso dell'agente ai propri file e alla propria shell; per questo verificare un plugin prima di installarlo fa parte dello stesso lavoro della creazione dell'account. Il primo avvio scrive file e potrebbe scaricare bundle, quindi eseguilo manualmente quando puoi monitorarne l'attività.
sudo -u dsh env HOME=/var/lib/dsh DSH_HOME=/var/lib/dsh/harness /usr/bin/dsh --profile webImposta HOME in modo esplicito invece di affidarti a ciò che sudo ne fa, perché la riscrittura di HOME da parte di sudo per un comando non di login dipende dall'impostazione set_home in /etc/sudoers. Se il valore è errato, la prima esecuzione crea directory di cache nella tua home directory, assegnandone la proprietà a dsh; in seguito il servizio non riesce a trovare il proprio stato. Arrestalo con Ctrl+C non appena il controllo curl restituisce up.
Il file dell’unità
Scrivere /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= richiede il percorso assoluto ottenuto da command -v dsh. systemd cerca un nome di comando semplice in un elenco di percorsi predefinito, ma tale elenco non corrisponde a PATH della shell. Un percorso assoluto elimina quindi ogni ambiguità.
WorkingDirectory= indica il percorso di risoluzione dei percorsi relativi. È anche la directory iniziale di una chiamata a uno strumento che esegue ls senza argomenti. Impostarla sulla workspace assegnata 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 utilizza si trova sotto /var/lib/dsh. Se si imposta la workspace su un percorso sotto /home, l’agente segnalerà che la directory non esiste. Il motivo può risultare poco chiaro finché non si considera questa riga. ProtectSystem=full rende /usr, /boot e /etc di sola lettura, anche se il servizio non deve mai scrivervi.
Applicare restrizioni ulteriori è allettante, ma di solito è una scelta errata. ProtectSystem=strict rende di sola lettura l’intero filesystem, 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. Per applicare questo livello di isolamento, aggiungere ReadWritePaths=/var/lib/dsh nella stessa modifica.
A quale Type= appartiene questa configurazione
Type=exec, perché dsh resta in primo piano e non esegue il fork. Il vantaggio rispetto all’impostazione predefinita è un messaggio di errore reale. Con Type=simple, systemd considera l’avvio riuscito non appena il processo ha eseguito il fork, prima di sapere se il binario esiste. 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. Un errore di battitura in ExecStart= fa quindi fallire il comando appena immesso e l’errore è visibile direttamente.
Le due risposte errate causano entrambe un blocco. Type=forking indica a systemd di attendere l’uscita di un processo padre. dsh non termina, quindi l’avvio resta 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. Un processo Node che non ne invia alcuno si blocca nello stesso modo. Il confronto completo dei tipi di servizio systemd illustra gli altri casi, incluso quando vale la pena configurare notify.
Regole di riavvio con arresto esplicito in caso di errore
Restart=on-failure esegue il riavvio dopo un'uscita diversa da zero o dopo un segnale fatale, ma lascia l'unità arrestata dopo un'uscita corretta. È 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 verificare l'evento. Restart=always trasforma lo stesso evento in un ciclo di riavvii che da lontano sembra funzionare correttamente.
Il limite di frequenza è l'impostazione che spesso viene omessa. I valori predefiniti di systemd consentono cinque avvii in dieci secondi. Con RestartSec=5s non si raggiungono mai cinque avvii nell'intervallo di dieci secondi, quindi un'unità che termina in errore all'avvio viene riavviata all'infinito 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.. Dopo aver corretto la causa, cancellare questo stato con sudo systemctl reset-failed dsh. Entrambe le impostazioni appartengono a [Unit], non a [Service]; nella sezione errata systemd le ignora senza segnalazioni.
Avvialo, quindi verifica
sudo systemctl daemon-reload
sudo systemctl enable --now dsh
systemctl status dshenable --now svolge due funzioni. enable fa ripartire il servizio dopo un riavvio, 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 riga 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 compare 0.0.0.0:3080, qualcosa ha modificato l'indirizzo di bind e l'agent è esposto su Internet. Il nome del processo nell'output è node, non dsh, perché il binario dsh è uno script Node; per questo pgrep -x dsh non trova nulla. Usa invece systemctl show -p MainPID dsh.
Riavvia quindi il server una volta. Un servizio che non ha mai superato un riavvio non è ancora un servizio affidabile.
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 per priorità. SyslogIdentifier=dsh nell'unità fa sì che quelle righe siano contrassegnate come dsh invece di node. Questo è importante la prima volta che si legge l'output del journal senza filtrarlo per unità.
Verificare che il journal persista dopo i riavvii prima che sia necessario consultarlo:
journalctl -u dsh -b -1Se il comando restituisce Specifying boot ID or boot offset has no effect, no persistent journal was found, il journal si trova in /run e viene eliminato a ogni riavvio. Creare la directory e riavviare il daemon:
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 (user interface) su 127.0.0.1:3080 e rifiuta di fornirla altrove. Se si richiede --host 0.0.0.0, il comando termina 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 (application programming interface) controlla l'agent e l'agent esegue comandi shell. Una porta raggiungibile equivale quindi a una shell sul VPS per chiunque la individui. I manutentori indicano come motivo del bind fisso sull'interfaccia di loopback il fatto che l'autenticazione remota non sia ancora implementata. Vale la pena leggere Cosa significa realmente la riga 127.0.0.1:3080 nell'output di avvio prima di tentare di modificarla. Inoltrate invece la porta dal vostro 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. Lasciatela in esecuzione e aprite http://127.0.0.1:3080/ nel browser. Qui si inserisce la chiave API di DeepSeek, nel percorso Settings quindi Models, e si seleziona la directory dell'area di lavoro. Impostate l'area di lavoro su /var/lib/dsh/workspace, la directory di proprietà dell'utente del servizio; in caso contrario gli strumenti per i file dell'agent terminano 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: 3080Scegliete una porta locale diversa con ssh -N -L 3081:127.0.0.1:3080 you@203.0.113.10, quindi aprite http://127.0.0.1:3081/ nel browser. Salvate il comando digitato in ~/.ssh/config sul vostro 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 all'agent, quindi è il demone SSH a proteggerlo: usate solo chiavi, disabilitate l'autenticazione tramite password e applicate le altre indicazioni di messa in sicurezza di SSH sul VPS con particolare attenzione. Se quel VPS diventa una piccola rete privata, con un database o un server di staging dietro di esso, pubblicizzare quegli indirizzi alla vostra tailnet con un subnet router evita di dover configurare un inoltro per ogni servizio, anche se il bind di dsh sull'interfaccia di loopback fa sì che l'interfaccia continui a essere accessibile tramite un tunnel.
La chiave non deve essere inserita nel file dell'unità. I valori di Environment= vengono visualizzati da systemctl show dsh -p Environment, che qualsiasi utente sul sistema può eseguire. Se un plugin installato richiede una chiave nell'ambiente, inseritela in /etc/dsh.env, con modalità 600 e proprietà di root, e fatevi riferimento con EnvironmentFile=/etc/dsh.env. systemd legge quel file come root al momento dell'esecuzione e systemctl show non ne visualizza il contenuto. Il file su disco in cui viene effettivamente salvata ogni impostazione e ciò che lascia il sistema quando si configura dsh per usare un endpoint Ollama locale invece dell'API di DeepSeek sono gli argomenti di configurazione delle chiavi, dei modelli e degli endpoint di dsh.
Costi di esecuzione
L'inferenza viene eseguita tramite l'API di DeepSeek, non sul tuo VPS. Il server esegue il processo Node, l'interfaccia che pubblica e ogni comando che l'agente decide di eseguire. I primi due consumi sono costanti e contenuti. Il terzo non è limitato da alcuna impostazione in questo file di unità.
Misura il valore minimo sul tuo server invece di fidarti di un dato rilevato su un altro sistema:
systemctl show dsh -p MemoryCurrent
systemd-cgtop -1 --depth 2MemoryCurrent è espresso in byte. Monitoralo mentre l'agente è al lavoro, non quando è inattivo.
Le chiamate agli strumenti sono processi figli del servizio. Pertanto vengono inserite nello stesso gruppo di controllo e sono soggette agli stessi limiti. Un agente che esegue npm install o una suite di test nella directory di lavoro può utilizzare molta più memoria del processo di orchestrazione. Su un VPS da 1 GB è in questo punto che possono verificarsi 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 che identifica il processo scelto. Spesso non si tratta del processo che ha causato il problema.
La soluzione consiste nell'impostare deliberatamente un limite. MemoryMax= e CPUQuota= nella sezione [Service] mantengono l'impatto all'interno dell'unità. In questo modo una compilazione 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 l'utilizzo del disco aumenta, a causa della cronologia delle sessioni in DSH_HOME e dei file che l'agente scrive nella directory di lavoro. Pertanto aggiungi du -sh /var/lib/dsh allo strumento che già utilizzi per monitorare lo spazio su disco.
Se vuoi 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 all'ora systemctl start, invece di essere nascosto.
status=217/USER. L'account in User= non esiste. Verificare con id dsh.
status=200/CHDIR. WorkingDirectory= manca 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é la prima esecuzione è stata effettuata come root o con un valore errato di HOME. 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 mostra up, il servizio funziona correttamente 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 imprevisto. Leggere prima le note di rilascio, perché l’avviso del progetto upstream sulle modifiche che possono interrompere la compatibilità è proprio il motivo per cui si blocca la versione. Eseguire il backup della directory di stato, quindi sostituire 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, oltre al ripristino di quell’archivio tar, che è possibile solo se ne è stata creata una copia. Un runtime dell’agente in fase di anteprima è esattamente il tipo di software in cui un aggiornamento può riscrivere il formato della configurazione senza preavviso.
FAQ
Perché dsh si arresta 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 termina. 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 consente di ottenere entrambi i risultati: 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 fork, 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 visualizzato direttamente. Con Type=simple lo stesso errore restituisce invece un esito positivo e rimane nascosto nel journal. Type=forking e Type=notify sono entrambi errati in questo caso e restano entrambi in attesa fino alla scadenza di TimeoutStartSec 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 associare 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 dispone di autenticazione remota.
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 dell'agent. Crea un account di sistema con useradd --system --shell /usr/sbin/nologin dsh, assegnagli la proprietà di /var/lib/dsh e aggiungi NoNewPrivileges=true all'unità. Se in seguito compare 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 August 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.