dsh headless draaien op VPS met systemd
Voorkom dat dsh stopt na het sluiten van uw SSH-sessie. Leer hoe u DeepSeek Harness als systemd-service configureert met een dedicated user, restart-regels en logboekbeheer.
Draai dsh headless op een VPS, niet in een terminal
Het headless draaien van dsh op een VPS vereist slechts één systemd-unitbestand en een toegewezen gebruiker die het proces beheert. dsh is de command-line launcher voor DeepSeek Harness, de agent-runtime van DeepSeek, die in augustus 2026 als developer preview onder de MIT-licentie is uitgebracht. Een harness is het programma rondom het model, niet het model zelf. U plaatst dus de loop, de tools en de rechten onder systemd, niet de inferentie van DeepSeek. De quickstart instrueert u om npx @deepseek-ai/dsh web te typen; dit is correct, maar het proces stopt zodra u uw SSH-sessie (secure shell) sluit.
Een unitbestand lost direct vier zaken op. De service start automatisch na een reboot. De output wordt naar de journal geschreven in plaats van dat deze voorbij scrolt. Het proces draait onder een account dat niet root is. Bovendien draait de versie die u heeft gekozen, wat hier belangrijker is dan gebruikelijk, aangezien de upstream-ontwikkelaars dit met hoofdletters aangeven:
DeepSeek Harness bevindt zich momenteel in developer preview en wordt snel doorontwikkeld. ER ZULLEN WIJZIGINGEN PLAATSVINDEN DIE DE COMPATIBILITEIT VERBREKEN.
Deze handleiding gaat ervan uit dat dsh al handmatig voor u werkt. Als dit niet het geval is, begin dan met het installeren van DeepSeek Harness op een VPS en keer terug zodra npx @deepseek-ai/dsh web een pagina serveert.
Installeer eerst Node, omdat npm u niet zal waarschuwen
node -vHet standaardpakket van Ubuntu 24.04 bevat Node 18 (18.19.1 per augustus 2026), wat verouderd is voor een pakket dat dit jaar is uitgebracht. @deepseek-ai/dsh publiceert geen engines-veld, waardoor npm geen EBADENGINE-waarschuwing geeft wanneer uw Node-versie te oud is. De fout treedt pas op tijdens runtime, in de vorm van een syntaxfout of een ontbrekende ingebouwde functie; dit is een ongunstig moment om dit te ontdekken. Installeer een actuele long term support (LTS)-release via 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 zou nu een v22-versie moeten weergeven. De less-regel is aanwezig omdat het direct doorsturen (piping) van een extern script naar bash code uitvoert die u niet heeft gelezen.
Controleer of het proces draait voordat u een unit schrijft
npx @deepseek-ai/dsh@0.1.0-rc.7 webLaat dit proces draaien. Open een tweede SSH-sessie:
curl -fsS http://127.0.0.1:3080/ -o /dev/null && echo upup betekent dat het webprofiel luistert op de loopback-interface, de standaardinstelling voor binding. curl: (7) Failed to connect to 127.0.0.1 port 3080: Connection refused betekent dat dit niet het geval is; de eerste terminal geeft aan waarom. Stop de handmatige uitvoering met Ctrl+C voordat u verdergaat: een unit die probeert een poort te binden die al in gebruik is, faalt met Error: listen EADDRINUSE: address already in use 127.0.0.1:3080.
0.1.0-rc.7 was de gepubliceerde versie op 18 augustus 2026. Controleer wat de huidige versie is met npm view @deepseek-ai/dsh version en zet vervolgens de versie vast die u wilt gebruiken.
Installeer de versie die u heeft vastgezet, globaal
npx is het verkeerde hulpmiddel binnen een unit-bestand. Het bepaalt de pakketversie op het moment dat het proces start, waardoor een herstart over drie maanden een andere build van een preview-agent kan laden zonder dat u iets heeft aangepast. Bovendien moet de npm-registry tijdens het opstarten bereikbaar zijn, waardoor een werkende machine een falende unit wordt op de dag dat de registry traag is. Installeer het eenmalig, op een versie die u heeft genoteerd:
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 toont /usr/bin/dsh wanneer npm afkomstig is van NodeSource, en /usr/local/bin/dsh wanneer het afkomstig is van het eigen pakket van Ubuntu. Gebruik het pad dat daadwerkelijk werd getoond in het unit-bestand. npm ls -g toont de exacte versie; dit is het antwoord dat u over zes weken nodig heeft wanneer het gedrag verandert en u niet meer weet wat u heeft geïnstalleerd. Als de installatie mislukt, of command -v dsh daarna niets toont, of als de versie die u terugkrijgt niet de versie is waar u om vroeg, doorloop dan de gebruikelijke dsh installatie- en versieproblemen voordat u het unit-bestand schrijft.
Een gebruiker die eigenaar is van de service en niets anders
De agent voert shell-commando's uit. Dat is zijn taak. Als u deze als root uitvoert, wordt elke tool-aanroep een root-aanroep. Geef de agent daarom een eigen account zonder login-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 wordt DSH_HOME, de directory waar dsh profielen in bewaart. Een profiel is een benoemde stack van plugin-bundels met uw eigen patch-laag daarbovenop. De profielen web en headless bouwen zichzelf bij de eerste keer opstarten op basis van meegeleverde templates. Alles wat u later aan die stack toevoegt, draait als deze gebruiker met de eigen bestands- en shell-toegang van de agent. Daarom hoort het controleren van een plugin voordat u deze installeert bij dezelfde taak als het aanmaken van het account. Die eerste opstart schrijft bestanden en kan bundels ophalen, dus voer dit handmatig uit zodat u het proces kunt monitoren.
sudo -u dsh env HOME=/var/lib/dsh DSH_HOME=/var/lib/dsh/harness /usr/bin/dsh --profile webStel HOME expliciet in in plaats van te vertrouwen op wat sudo ermee doet. Of sudo de variabele HOME herschrijft voor een non-login commando hangt namelijk af van de set_home-instelling in /etc/sudoers. Als u dit fout doet, plaatst de eerste run cache-directories in uw home-directory die eigendom zijn van dsh, waardoor de service later zijn eigen status niet kan vinden. Stop het proces met Ctrl+C zodra de curl-controle up retourneert.
Het unit-bestand
Schrijf /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= gebruikt het absolute pad dat u heeft verkregen via command -v dsh. systemd doorzoekt een vaste lijst met paden voor een kale opdrachtnaam, maar die lijst is niet de PATH van uw shell, dus een absoluut pad voorkomt onduidelijkheid.
WorkingDirectory= is de locatie waar relatieve paden worden opgelost, en het is de plek waar een tool-aanroep die ls uitvoert zonder argumenten begint. Wijs dit naar de werkmap die u aan de agent geeft. Als de map ontbreekt of als de servicegebruiker deze niet kan betreden, faalt de unit met status=200/CHDIR nog voordat dsh wordt uitgevoerd.
ProtectHome=true verbergt /home en /root voor het proces. Dat is hier veilig omdat alles wat de service aanraakt zich onder /var/lib/dsh bevindt. Wijs de werkmap naar een pad onder /home en de agent zal rapporteren dat de map niet bestaat, wat verwarrend is totdat u zich deze regel herinnert. ProtectSystem=full maakt /usr, /boot en /etc alleen-lezen, aangezien de service hier nooit naar hoeft te schrijven.
Verder gaan is verleidelijk en meestal onjuist. ProtectSystem=strict maakt het volledige bestandssysteem alleen-lezen, afgezien van de pseudo-bestandssystemen van de kernel, waardoor de eerste tool-aanroep die een bestand schrijft faalt met EROFS: read-only file system. Als u dat niveau wenst, voeg dan ReadWritePaths=/var/lib/dsh toe in dezelfde bewerking.
Welk Type= hoort hier thuis
Type=exec, omdat dsh op de voorgrond blijft en nooit een fork uitvoert. Het voordeel ten opzichte van de standaardinstelling is een daadwerkelijke foutmelding. Bij Type=simple markeert systemd de start als succesvol zodra het proces is geforkt, nog voordat bekend is of het binaire bestand überhaupt bestaat. Hierdoor keert systemctl start dsh foutloos terug en wordt de mislukking pas in de journal zichtbaar. Bij Type=exec wacht systemd tot execve() succesvol is afgerond, waardoor een typefout in ExecStart= direct leidt tot een foutmelding in het commando dat u zojuist heeft ingevoerd, zodat u deze direct ziet.
De twee onjuiste antwoorden leiden beide tot een hangend proces. Type=forking instrueert systemd om te wachten tot een ouderproces wordt afgesloten, maar dsh sluit nooit af. De start blokkeert daarom totdat TimeoutStartSec verloopt (standaard 90 seconden) en rapporteert vervolgens een Job for dsh.service failed because a timeout was exceeded.. Type=notify wacht op een READY=1-bericht via sd_notify; een Node-proces dat dit nooit verstuurt, loopt op dezelfde wijze vast. De volledige vergelijking van systemd service types behandelt de overige opties, inclusief wanneer het de moeite waard is om notify te configureren.
Herstartregels die luidruchtig falen
Restart=on-failure herstart bij een exitcode die niet nul is of bij een fataal signaal, en laat de unit met rust na een correcte afsluiting. Dit is het gedrag dat u wenst voor een preview-build. Als dsh ooit afsluit met 0 omdat het een configuratie heeft gelezen die niet correct is, stopt de unit en blijft deze gestopt, waarna systemctl status dsh de status inactive (dead) toont waar u dit kunt zien. Restart=always verandert diezelfde gebeurtenis in een herstartlus die er van een afstand gezond uitziet.
De limiet voor de herstartfrequentie is het onderdeel dat mensen vaak vergeten. De standaardinstellingen van systemd zijn vijf starts binnen tien seconden, en met RestartSec=5s bereikt u nooit vijf starts binnen een venster van tien seconden. Hierdoor herstart een unit die crasht bij het opstarten oneindig door en is dit alleen zichtbaar in de journal. StartLimitIntervalSec=300 met StartLimitBurst=5 betekent dat vijf fouten binnen vijf minuten voldoende zijn: systemd geeft het op en parkeert de unit in de status failed, waarbij Start request repeated too quickly. wordt gelogd. Wis deze status met sudo systemctl reset-failed dsh zodra u de oorzaak heeft verholpen. Beide instellingen horen thuis in [Unit], niet in [Service], en systemd negeert ze geruisloos als ze in de verkeerde sectie staan.
Start de service en controleer de status
sudo systemctl daemon-reload
sudo systemctl enable --now dsh
systemctl status dshenable --now voert twee taken uit. enable zorgt ervoor dat de service na een herstart weer actief wordt, en --now start de service in de huidige sessie. Een kale systemctl start is na de volgende herstart verdwenen, en kernel-updates vereisen herstarts.
systemctl status dsh hoort Active: active (running), een Main PID en een Memory:-regel te tonen. Controleer daarna op welke poort de service luistert:
sudo ss -lntp | grep 3080U wilt 127.0.0.1:3080 zien. Als u 0.0.0.0:3080 ziet, heeft iets het bind-adres aangepast en is uw agent bereikbaar via het openbare internet. De procesnaam in die uitvoer is node, niet dsh, omdat het dsh-binary een Node-script is; daarom vindt pgrep -x dsh niets. Gebruik in plaats daarvan systemctl show -p MainPID dsh.
Herstart vervolgens het systeem. Een service die een herstart niet overleeft, is nog geen volwaardige service.
sudo rebootMaak opnieuw verbinding en voer systemctl is-active dsh uit. Dit geeft active weer.
Logs lezen met journalctl
Alles wat dsh naar stdout en stderr schrijft, komt in de journal terecht onder de unit-naam.
journalctl -u dsh -f
journalctl -u dsh -n 200 --no-pager
journalctl -u dsh --since "10 min ago" -p err-f volgt nieuwe regels, -n toont de laatste N regels, -p err filtert op prioriteit. SyslogIdentifier=dsh in de unit is de reden waarom deze regels worden getagd als dsh in plaats van node; dit is van belang wanneer u voor het eerst journal-output leest die niet op unit is gefilterd.
Controleer of de journal reboots overleeft voordat u deze nodig heeft:
journalctl -u dsh -b -1Als dit Specifying boot ID or boot offset has no effect, no persistent journal was found weergeeft, bevindt de journal zich in /run en wordt deze bij elke reboot gewist. Maak de directory aan en herstart de daemon:
sudo mkdir -p /var/log/journal
sudo systemctl restart systemd-journaldBereik de UI via een SSH-tunnel, niet via een publieke poort
dsh serveert de web-UI (user interface) op 127.0.0.1:3080 en weigert deze elders aan te bieden. Vraagt u om --host 0.0.0.0, dan stopt het proces met de volgende melding:
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 insteadDit is geen beperking die u moet omzeilen. De web-API (application programming interface) stuurt de agent aan, en de agent voert shell-commando's uit. Een bereikbare poort betekent dus een shell op uw VPS voor iedereen die deze vindt. De ontwikkelaars geven aan dat het ontbreken van externe authenticatie de reden is dat de bind vaststaat op loopback. Wat die 127.0.0.1:3080-regel in de opstartuitvoer daadwerkelijk betekent is het lezen waard voordat u probeert dit te wijzigen. Forward de poort in plaats daarvan vanaf uw eigen machine:
ssh -N -L 3080:127.0.0.1:3080 you@203.0.113.10-L 3080:127.0.0.1:3080 opent poort 3080 op uw laptop en stuurt alles wat daar aankomt door naar 127.0.0.1:3080, zoals opgelost op de VPS. -N betekent dat er geen extern commando wordt uitgevoerd, waardoor de sessie alleen de tunnel openhoudt. Laat dit draaien en open http://127.0.0.1:3080/ in uw browser. Hier voert u de DeepSeek API-sleutel in, onder Settings en vervolgens Models, en hier kiest u de workspace-directory. Wijs de workspace aan op /var/lib/dsh/workspace, de directory waarvan de service-gebruiker de eigenaar is, anders falen de bestandstools van de agent met EACCES: permission denied.
Als poort 3080 bezet is op uw laptop, meldt ssh dit:
bind [127.0.0.1]:3080: Address already in use
channel_setup_fwd_listener_tcpip: cannot listen to port: 3080Kies een andere lokale poort met ssh -N -L 3081:127.0.0.1:3080 you@203.0.113.10 en navigeer vervolgens naar http://127.0.0.1:3081/. Bespaar uzelf het typwerk in ~/.ssh/config op uw eigen machine:
Host dsh-vps
HostName 203.0.113.10
User you
LocalForward 3080 127.0.0.1:3080Daarna is ssh -N dsh-vps het volledige commando. Deze tunnel is nu de enige toegangspoort tot uw agent, dus de SSH-daemon is wat deze beschermt: gebruik alleen sleutels, geen wachtwoordauthenticatie, en de rest van het beveiligen van SSH op uw VPS is hier nog sterker van toepassing. Als die VPS uitgroeit tot een klein privénetwerk, met een database of een staging-omgeving erachter, bespaart het adverteren van die adressen aan uw tailnet met een subnet router u een forward per service, hoewel de loopback-bind van dsh betekent dat de UI zelf nog steeds via een tunnel binnenkomt.
De sleutel hoort niet thuis in het unit-bestand. Environment=-waarden worden afgedrukt door systemctl show dsh -p Environment, wat elke gebruiker op de server kan uitvoeren. Als een plugin die u installeert een sleutel in de omgeving nodig heeft, plaats deze dan in /etc/dsh.env met modus 600, eigendom van root, en verwijs ernaar met EnvironmentFile=/etc/dsh.env. systemd leest dat bestand als root tijdens de uitvoering, en systemctl show drukt de inhoud ervan niet af. Welk bestand op schijf elke instelling daadwerkelijk bevat, en wat uw server verlaat wanneer u dsh naar een lokaal Ollama-eindpunt wijst in plaats van naar de API van DeepSeek, is het onderwerp van het configureren van dsh-sleutels, modellen en eindpunten.
Wat de kosten zijn voor het draaien
Inference vindt plaats via de API van DeepSeek, niet op uw VPS. Uw server betaalt voor het Node-proces, de UI die het serveert en elk commando dat de agent besluit uit te voeren. De eerste twee zijn stabiel en klein. De derde wordt door niets in dit unit-bestand begrensd.
Meet de ondergrens op uw eigen server in plaats van te vertrouwen op een cijfer van iemand anders:
systemctl show dsh -p MemoryCurrent
systemd-cgtop -1 --depth 2MemoryCurrent is in bytes. Observeer dit terwijl de agent werkt, niet terwijl deze inactief is.
Tool-aanroepen zijn onderliggende processen van de service, dus ze vallen in dezelfde control group en tellen mee voor dezelfde limieten. Een agent die npm install of een testsuite uitvoert binnen de werkruimte, kan aanzienlijk meer geheugen gebruiken dan de harness zelf. Op een 1 GB VPS is dat waar problemen ontstaan: de kernel kiest een proces en beëindigt dit, en journalctl -k | grep -i "out of memory" toont de Out of memory: Killed process-regel waarin staat welk proces is gekozen. Dat proces is vaak niet het proces dat het probleem veroorzaakte.
De oplossing is een limiet die u bewust instelt. MemoryMax= en CPUQuota= in de sectie [Service] houden de schade binnen de unit, zodat een op hol geslagen build wordt beëindigd in plaats van dat de hele server vastloopt. Geheugen en CPU beperken met systemd behandelt de getallen en het gedrag bij falen. De schijfruimte groeit ook, door sessiegeschiedenis onder DSH_HOME en door alles wat de agent schrijft in de werkruimte, dus voeg du -sh /var/lib/dsh toe aan de tools die u al gebruikt om schijfgebruik te monitoren.
Als u een interactieve agent wilt waar u aan kunt koppelen en van kunt loskoppelen, dan is een service niet de juiste vorm, en past een agent draaien in een persistente tmux-sessie beter. Draai dsh als een unit wanneer u wilt dat deze altijd actief en bereikbaar is via een tunnel.
Foutmodi en de meldingen die u zult zien
status=203/EXEC. systemd kon het bestand niet uitvoeren en logt Failed to locate executable /usr/local/bin/dsh: No such file or directory. Het pad in ExecStart= komt niet overeen met wat command -v dsh heeft weergegeven. Dit is de fout die Type=exec rapporteert op het moment van systemctl start in plaats van deze te verbergen.
status=217/USER. Het account in User= bestaat niet. Controleer dit met id dsh.
status=200/CHDIR. WorkingDirectory= ontbreekt, of de servicegebruiker heeft geen toegang tot deze map. sudo -u dsh ls /var/lib/dsh/workspace reproduceert dit direct.
Error: listen EADDRINUSE: address already in use 127.0.0.1:3080. Iets anders gebruikt de poort al, meestal een npx-proces dat is achtergebleven in een andere terminal. sudo ss -lntp | grep 3080 toont de naam van het proces.
EACCES: permission denied gevolgd door een pad. Het eigenaarschap onder /var/lib/dsh is onjuist, meestal omdat een eerste uitvoering plaatsvond als root of met de verkeerde HOME. sudo chown -R dsh:dsh /var/lib/dsh herstelt dit.
Start request repeated too quickly. De unit heeft de startlimiet bereikt en is gestopt met proberen. De werkelijke fout staat in de regels daarboven. Voer sudo systemctl reset-failed dsh uit voordat u het opnieuw probeert.
Unit is active (running) maar de browser toont niets. Voer de controle uit op de VPS: als curl -fsS http://127.0.0.1:3080/ -o /dev/null && echo up daar up weergeeft, is de service in orde en ligt het probleem bij de port forwarding.
Doelgericht upgraden
Met pinning bepaalt u zelf wanneer een upgrade plaatsvindt; het is geen proces dat u overkomt. Lees eerst de release notes, aangezien de waarschuwing van de upstream-ontwikkelaar over wijzigingen die compatibiliteit verbreken de enige reden is voor de pin. Maak een back-up van de state-directory en wissel vervolgens de versie:
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-pagerEen rollback verloopt op dezelfde wijze met npm install -g en de oude versie, aangevuld met het terugzetten van de tarball. Dit werkt alleen als u daadwerkelijk een back-up heeft gemaakt. Een agent-runtime in de preview-fase is bij uitstek software waarbij een upgrade de configuratie-indeling onder uw handen kan wijzigen.
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.