SSD Nodes Learn 🎉 VPS dès $5.50/mois
Guides Matt ConnorPar Matt Connor · Mis à jour le 2026-08-21

Lancer dsh avec systemd sur un VPS sans terminal

Configurez dsh comme service systemd avec utilisateur dédié, version figée, redémarrage, logs journalctl et tunnel SSH vers l’interface web.

Exécuter dsh sans terminal sur un VPS

Exécuter dsh sans terminal sur un VPS nécessite un fichier d’unité systemd et un utilisateur dédié qui en sera propriétaire. dsh est le lanceur en ligne de commande de DeepSeek Harness, le runtime d’agent de DeepSeek, publié sous licence MIT en developer preview en août 2026. Le guide de démarrage rapide vous demande de saisir npx @deepseek-ai/dsh web, ce qui est correct, mais ce processus s’arrête dès que vous fermez votre session SSH (secure shell).

Un fichier d’unité règle quatre problèmes à la fois. Le service redémarre après un reboot. Sa sortie est écrite dans le journal au lieu de défiler hors de votre écran. Il s’exécute avec un compte qui n’est pas root. Enfin, il utilise la version que vous avez choisie. Ce point est plus important que d’habitude, car le projet amont l’indique en majuscules :

DeepSeek Harness est actuellement en developer preview et évolue rapidement. DES MODIFICATIONS INCOMPATIBLES AVEC LES VERSIONS PRÉCÉDENTES SONT À PRÉVOIR.

Ce guide part du principe que dsh fonctionne déjà lorsque vous le lancez manuellement. Si ce n’est pas le cas, commencez par installer DeepSeek Harness sur un VPS, puis revenez ici lorsque npx @deepseek-ai/dsh web sert une page.

Node d’abord, car npm ne vous avertira pas

node -v

Le paquet fourni par Ubuntu 24.04 est Node 18 (18.19.1 en août 2026), une version ancienne pour un paquet publié cette année. @deepseek-ai/dsh ne publie aucun champ engines. npm n’affiche donc aucun avertissement EBADENGINE lorsque votre version de Node est trop ancienne. L’échec survient à l’exécution, sous la forme d’une erreur de syntaxe ou d’un module intégré manquant. C’est un endroit bien plus défavorable pour le découvrir. Installez une version actuelle avec support à long terme (LTS) depuis 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 -v

node -v doit maintenant afficher une version v22. La ligne less est présente, car envoyer directement un script distant à bash exécute du code que vous n’avez pas lu.

Vérifiez que le service fonctionne avant d’écrire une unité

npx @deepseek-ai/dsh@0.1.0-rc.7 web

Laissez-le fonctionner. Depuis une deuxième session SSH :

curl -fsS http://127.0.0.1:3080/ -o /dev/null && echo up

up signifie que le profil web écoute sur l’interface loopback, à laquelle il se lie par défaut. curl: (7) Failed to connect to 127.0.0.1 port 3080: Connection refused signifie que ce n’est pas le cas ; le premier terminal vous en indique la raison. Arrêtez l’exécution manuelle avec Ctrl+C avant de continuer : une unité qui tente de se lier à un port déjà utilisé échoue avec Error: listen EADDRINUSE: address already in use 127.0.0.1:3080.

0.1.0-rc.7 correspondait à la version publiée le 18 August 2026. Vérifiez quelle est la version actuelle avec npm view @deepseek-ai/dsh version, puis verrouillez la version que vous décidez d’utiliser.

Installer la version figée, globalement

npx n’est pas l’outil adapté dans un fichier d’unité. Il résout la version du paquet au démarrage du processus. Ainsi, un redémarrage dans trois mois peut lancer une autre build d’un agent en phase de préversion, sans aucune modification de votre côté. Il nécessite également que le registre npm soit accessible au boot. Une machine fonctionnelle peut donc devenir une unité en échec le jour où le registre est lent. Installez le paquet une seule fois, avec une version consignée :

sudo npm install -g @deepseek-ai/dsh@0.1.0-rc.7
command -v dsh
npm ls -g --depth=0 @deepseek-ai/dsh

command -v dsh affiche /usr/bin/dsh lorsque npm provient de NodeSource, et /usr/local/bin/dsh lorsqu’il provient du paquet fourni par Ubuntu. Utilisez dans le fichier d’unité le chemin qui a réellement été affiché. npm ls -g affiche la version exacte. C’est cette information qu’il vous faudra dans six semaines si le comportement change et que vous ne vous souvenez plus de ce que vous avez installé.

Un utilisateur dédié au service, et rien d’autre

L’agent exécute des commandes shell. C’est son rôle. L’exécuter en tant que root transforme chaque appel d’outil en appel root. Créez donc un compte dédié sans shell de connexion.

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 devient DSH_HOME, le répertoire dans lequel dsh conserve les profils. Un profil est un ensemble nommé de bundles de plugins, complété par votre propre couche de correctifs. Les profils web et headless sont générés à partir des templates fournis lors de leur premier démarrage. Ce premier démarrage écrit des fichiers et peut récupérer des bundles. Effectuez-le donc manuellement afin de pouvoir le surveiller.

sudo -u dsh env HOME=/var/lib/dsh DSH_HOME=/var/lib/dsh/harness /usr/bin/dsh --profile web

Définissez explicitement HOME au lieu de faire confiance à la façon dont sudo le traite. La réécriture de HOME par sudo pour une commande non interactive dépend du paramètre set_home dans /etc/sudoers. Si ce paramètre est incorrect, la première exécution crée des répertoires de cache dans votre répertoire personnel, avec dsh comme propriétaire. Le service ne pourra ensuite plus retrouver son propre état. Arrêtez-le avec Ctrl+C dès que le contrôle curl renvoie up.

Le fichier d’unité

Écrivez /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.target

ExecStart= utilise le chemin absolu obtenu avec command -v dsh. systemd recherche un nom de commande seul dans une liste de chemins fixe. Cette liste n’est pas le PATH de votre shell. Un chemin absolu évite donc toute ambiguïté.

WorkingDirectory= est le répertoire utilisé pour résoudre les chemins relatifs. C’est aussi le répertoire de départ d’un appel d’outil qui exécute ls sans argument. Indiquez le workspace que vous fournissez à l’agent. Si ce répertoire n’existe pas ou si l’utilisateur du service ne peut pas y accéder, l’unité échoue avec status=200/CHDIR avant même l’exécution de dsh.

ProtectHome=true masque /home et /root au processus. C’est sûr ici, car tout ce que le service utilise se trouve sous /var/lib/dsh. Si vous définissez le workspace sur un chemin situé sous /home, l’agent indiquera que le répertoire n’existe pas. Ce comportement est déroutant tant que vous ne vous souvenez pas de cette ligne. ProtectSystem=full rend /usr, /boot et /etc accessibles en lecture seule. Le service n’a jamais besoin d’y écrire.

Il est tentant d’aller plus loin, mais c’est généralement une erreur. ProtectSystem=strict rend l’ensemble du système de fichiers accessible en lecture seule, à l’exception des pseudo-systèmes de fichiers du noyau. Le premier appel d’outil qui écrit un fichier échoue alors avec EROFS: read-only file system. Si vous avez besoin de ce niveau d’isolation, ajoutez ReadWritePaths=/var/lib/dsh dans la même modification.

Quel Type= faut-il utiliser ici

Type=exec, car dsh reste au premier plan et ne fait jamais de fork. Cela présente un avantage par rapport au comportement par défaut : vous obtenez un véritable message d’erreur. Avec Type=simple, systemd considère le démarrage comme réussi dès que le processus a fait un fork, avant même de savoir si le binaire existe. Ainsi, systemctl start dsh se termine correctement et l’échec apparaît uniquement dans le journal. Avec Type=exec, systemd attend que execve() réussisse. Une faute de frappe dans ExecStart= fait donc échouer la commande que vous venez de saisir, et vous voyez immédiatement l’erreur.

Les deux mauvaises réponses bloquent toutes les deux. Type=forking indique à systemd d’attendre qu’un processus parent se termine. Or dsh ne se termine jamais. Le démarrage reste donc bloqué jusqu’à l’expiration de TimeoutStartSec (90 secondes par défaut), puis signale Job for dsh.service failed because a timeout was exceeded.. Type=notify attend un message READY=1 via sd_notify. Un processus Node qui n’en envoie jamais reste bloqué de la même manière. Comparaison complète des types de services systemd présente les autres cas, notamment les situations où la configuration de notify est utile.

Des règles de redémarrage qui signalent clairement les échecs

Restart=on-failure redémarre le service après une sortie différente de zéro ou la réception d’un signal fatal, et laisse l’unité arrêtée après une sortie normale. C’est le comportement recherché pour une build de prévisualisation. Si dsh se termine un jour avec le code 0 après avoir lu une configuration qu’il ne prend pas en charge, l’unité s’arrête et reste arrêtée, et systemctl status dsh affiche inactive (dead), où vous pouvez le voir. Restart=always transforme ce même événement en boucle de redémarrage qui semble saine à distance.

La limite de fréquence est souvent oubliée. Les valeurs par défaut de systemd autorisent cinq démarrages en dix secondes. Avec RestartSec=5s, vous n’atteignez jamais cinq démarrages sur une fenêtre de dix secondes. Une unité qui plante au démarrage redémarre donc indéfiniment, et seul le journal le signale. StartLimitIntervalSec=300 avec StartLimitBurst=5 signifie que cinq échecs en cinq minutes suffisent : systemd abandonne et place l’unité dans failed, en journalisant Start request repeated too quickly.. Effacez cet état avec sudo systemctl reset-failed dsh après avoir corrigé la cause. Ces deux paramètres doivent se trouver dans [Unit], et non dans [Service]. systemd les ignore silencieusement s’ils sont placés dans la mauvaise section.

Démarrez-le, puis vérifiez-le

sudo systemctl daemon-reload
sudo systemctl enable --now dsh
systemctl status dsh

enable --now effectue deux opérations. enable permet de relancer le service après un redémarrage, et --now le démarre pour le boot en cours. Un systemctl start seul ne persiste pas après le prochain redémarrage, et les mises à jour du kernel nécessitent des redémarrages.

systemctl status dsh doit afficher Active: active (running), une Main PID et une ligne Memory:. Vérifiez ensuite sur quelle adresse il est en écoute :

sudo ss -lntp | grep 3080

Vous devez obtenir 127.0.0.1:3080. Si vous voyez 0.0.0.0:3080, quelque chose a modifié l’adresse d’écoute et votre agent est exposé sur Internet. Le nom du processus dans cette sortie est node, et non dsh, car le binaire dsh est un script Node ; pgrep -x dsh ne trouve donc rien. Utilisez plutôt systemctl show -p MainPID dsh.

Redémarrez ensuite une fois le serveur. Un service qui n’a jamais survécu à un redémarrage n’est pas encore un véritable service.

sudo reboot

Reconnectez-vous, puis exécutez systemctl is-active dsh. Cette commande affiche active.

Lire les journaux avec journalctl

Tout ce que dsh écrit sur stdout et stderr est enregistré dans le journal sous le nom de l’unité.

journalctl -u dsh -f
journalctl -u dsh -n 200 --no-pager
journalctl -u dsh --since "10 min ago" -p err

-f suit les nouvelles lignes, -n affiche les dernières N lignes et -p err filtre selon la priorité. SyslogIdentifier=dsh dans l’unité explique pourquoi ces lignes portent l’étiquette dsh au lieu de node. Cela compte lorsque vous consultez pour la première fois une sortie du journal qui n’est pas filtrée par unité.

Vérifiez que le journal est conservé après les redémarrages avant d’en avoir besoin :

journalctl -u dsh -b -1

Si la commande affiche Specifying boot ID or boot offset has no effect, no persistent journal was found, le journal est stocké dans /run et il est supprimé à chaque redémarrage. Créez le répertoire, puis redémarrez le daemon :

sudo mkdir -p /var/log/journal
sudo systemctl restart systemd-journald

Accédez à l’interface via un tunnel SSH, pas via un port public

dsh sert l’interface web sur 127.0.0.1:3080 et refuse de la servir ailleurs. Demandez --host 0.0.0.0 et il s’arrête avec ceci :

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

Ce n’est pas une limitation à contourner. L’API web pilote l’agent, et l’agent exécute des commandes shell. Un port accessible expose donc un shell sur votre VPS à toute personne qui le découvre. Les mainteneurs expliquent que l’authentification distante n’est pas implémentée, ce qui justifie la liaison fixe à loopback. Transférez plutôt le port depuis votre propre machine :

ssh -N -L 3080:127.0.0.1:3080 you@203.0.113.10

-L 3080:127.0.0.1:3080 ouvre le port 3080 sur votre laptop et envoie tout ce qui y arrive vers 127.0.0.1:3080, après résolution sur le VPS. -N signifie qu’aucune commande distante ne doit être exécutée ; la session sert uniquement à maintenir le tunnel ouvert. Laissez-la fonctionner, puis ouvrez http://127.0.0.1:3080/ dans votre navigateur. C’est ici que vous saisissez la clé d’API DeepSeek, sous Settings puis Models, et que vous choisissez le répertoire de travail. Définissez le répertoire de travail sur /var/lib/dsh/workspace, le répertoire appartenant à l’utilisateur du service, sinon les outils de gestion de fichiers de l’agent échouent avec EACCES: permission denied.

Si le port 3080 est déjà utilisé sur votre laptop, ssh l’indique :

bind [127.0.0.1]:3080: Address already in use
channel_setup_fwd_listener_tcpip: cannot listen to port: 3080

Choisissez un autre port local avec ssh -N -L 3081:127.0.0.1:3080 you@203.0.113.10, puis ouvrez http://127.0.0.1:3081/. Enregistrez cette commande dans ~/.ssh/config sur votre propre machine :

Host dsh-vps
  HostName 203.0.113.10
  User you
  LocalForward 3080 127.0.0.1:3080

Ensuite, ssh -N dsh-vps constitue toute la commande. Ce tunnel est désormais le seul accès à votre agent. Le daemon SSH est donc ce qui le protège : clés uniquement, pas d’authentification par mot de passe, et le reste du durcissement de SSH sur votre VPS s’applique avec encore plus de rigueur que d’habitude.

La clé ne doit pas se trouver dans le fichier d’unité. Les valeurs Environment= sont affichées par systemctl show dsh -p Environment, que n’importe quel utilisateur de la machine peut exécuter. Si un plugin que vous installez a besoin d’une clé dans l’environnement, placez-la dans /etc/dsh.env, avec le mode 600 et root comme propriétaire, puis référencez-la avec EnvironmentFile=/etc/dsh.env. systemd lit ce fichier en tant que root au moment de l’exécution, et systemctl show n’en affiche pas le contenu.

Coût d’exécution

L’inférence s’effectue via l’API de DeepSeek, et non sur votre VPS. Votre serveur consomme des ressources pour le processus Node, l’interface qu’il sert et chaque commande que l’agent décide d’exécuter. Les deux premières consommations sont faibles et stables. La troisième n’est limitée par aucune directive de ce fichier d’unité.

Mesurez la consommation minimale sur votre propre serveur au lieu de vous fier à une valeur mesurée sur un autre serveur :

systemctl show dsh -p MemoryCurrent
systemd-cgtop -1 --depth 2

MemoryCurrent est exprimé en octets. Surveillez cette valeur pendant que l’agent travaille, et non lorsqu’il est inactif.

Les appels aux outils sont des processus enfants du service. Ils appartiennent donc au même groupe de contrôle et sont soumis aux mêmes limites. Un agent qui exécute npm install ou une suite de tests dans l’espace de travail peut utiliser beaucoup plus de mémoire que le harness lui-même. Sur un VPS de 1 GB, c’est à ce moment que les problèmes surviennent : le noyau sélectionne un processus et le termine, et journalctl -k | grep -i "out of memory" affiche la ligne Out of memory: Killed process qui indique le processus choisi. Ce processus n’est souvent pas celui qui a causé le problème.

La solution consiste à définir volontairement une limite. MemoryMax= et CPUQuota= dans la section [Service] maintiennent les dégâts à l’intérieur de l’unité. Une compilation incontrôlée est ainsi terminée au lieu de bloquer tout le serveur. Limiter la mémoire et le CPU avec systemd explique les valeurs à utiliser et le comportement en cas d’échec. L’espace disque augmente également, à cause de l’historique des sessions sous DSH_HOME et de ce que l’agent écrit dans l’espace de travail. Ajoutez donc du -sh /var/lib/dsh à l’outil que vous utilisez déjà pour surveiller l’espace disque.

Si vous recherchez un agent interactif auquel vous pouvez vous connecter et vous déconnecter, un service n’est pas adapté. Exécuter un agent dans une session tmux persistante convient mieux. Exécutez dsh en tant qu’unité lorsque vous voulez qu’il reste toujours disponible et accessible via un tunnel.

Modes d’échec et messages affichés

status=203/EXEC. systemd n’a pas pu exécuter le fichier et les journaux indiquent Failed to locate executable /usr/local/bin/dsh: No such file or directory. Le chemin indiqué par ExecStart= ne correspond pas à celui affiché par command -v dsh. C’est l’échec que Type=exec signale à l’instant systemctl start au lieu de le masquer.

status=217/USER. Le compte indiqué dans User= n’existe pas. Vérifiez avec id dsh.

status=200/CHDIR. WorkingDirectory= est absent ou l’utilisateur du service ne peut pas y accéder. sudo -u dsh ls /var/lib/dsh/workspace reproduit directement le problème.

Error: listen EADDRINUSE: address already in use 127.0.0.1:3080. Un autre processus utilise déjà le port, généralement parce que l’exécution de npx est restée ouverte dans un autre terminal. sudo ss -lntp | grep 3080 indique le processus concerné.

EACCES: permission denied suivi d’un chemin. Le propriétaire des fichiers sous /var/lib/dsh est incorrect, généralement parce qu’une première exécution a été effectuée avec root ou avec le mauvais HOME. sudo chown -R dsh:dsh /var/lib/dsh corrige le problème.

Start request repeated too quickly. L’unité a atteint la limite de fréquence de démarrage, puis a abandonné. L’erreur réelle se trouve dans les lignes précédentes. Exécutez sudo systemctl reset-failed dsh avant de réessayer.

L’unité est active (running), mais le navigateur n’affiche rien. Exécutez le contrôle sur le VPS : si curl -fsS http://127.0.0.1:3080/ -o /dev/null && echo up affiche up à cet endroit, le service fonctionne correctement et le problème vient de la redirection de port.

Mettre à niveau volontairement

Le pinning signifie qu’une mise à niveau est une action que vous contrôlez, et non un événement qui vous est imposé. Lisez d’abord les notes de version, car l’avertissement de l’upstream concernant les changements qui peuvent rompre la compatibilité est précisément la raison du pinning. Sauvegardez le répertoire d’état, puis remplacez la version :

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-pager

Revenir à la version précédente consiste à effectuer le même npm install -g avec l’ancienne version, puis à restaurer cette archive tar, ce qui ne fonctionne que si vous l’avez créée. Un runtime d’agent en phase de préversion est précisément le type de logiciel dont une mise à niveau peut réécrire le format de configuration sans vous prévenir.

FAQ

Pourquoi dsh s’arrête-t-il quand je ferme ma session SSH ?

Parce que npx @deepseek-ai/dsh web est un processus au premier plan associé à votre session de connexion. Il est donc arrêté à la fin de la session. Une unité systemd est associée au système init. Elle continue donc de fonctionner après votre déconnexion et redémarre après un reboot. sudo systemctl enable --now dsh est la paire d’étapes qui vous donne les deux : enable pour le reboot et --now pour le boot en cours.

Dois-je utiliser Type=simple ou Type=exec pour dsh ?

Type=exec. dsh s’exécute au premier plan et ne fait jamais de fork. Les deux fonctionnent donc, mais Type=exec fait attendre systemd la réussite de execve() avant de considérer le démarrage comme réussi. Un chemin incorrect dans ExecStart= fait alors échouer systemctl start, avec status=203/EXEC affiché directement. Avec Type=simple, la même erreur renvoie un succès et reste masquée dans le journal. Type=forking et Type=notify sont tous deux incorrects ici. Ils restent tous deux bloqués jusqu’à l’expiration de TimeoutStartSec après 90 secondes.

Comment ouvrir l’interface web de dsh depuis mon ordinateur portable ?

Transférez le port via SSH : ssh -N -L 3080:127.0.0.1:3080 you@your-vps, puis ouvrez http://127.0.0.1:3080/ dans votre navigateur. N’essayez pas de lier le service à une adresse publique. dsh rejette --host 0.0.0.0 avec 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, car l’API web peut demander à l’agent d’exécuter des commandes shell et aucune authentification distante ne le protège.

Puis-je exécuter dsh en tant que root pour simplifier la gestion des permissions ?

Non. Le harness sert à exécuter des commandes et à écrire des fichiers. Les privilèges du service sont donc également ceux de l’agent. Créez un compte système avec useradd --system --shell /usr/sbin/nologin dsh, attribuez-lui la propriété de /var/lib/dsh et ajoutez NoNewPrivileges=true à l’unité. Si vous obtenez ensuite EACCES: permission denied, la cause habituelle est une exécution précédente en tant que root qui a laissé des fichiers appartenant à root. sudo chown -R dsh:dsh /var/lib/dsh corrige ce problème.

Quelle version de dsh dois-je figer dans l’unité ?

Celle que npm view @deepseek-ai/dsh version indique lors de la configuration du service, installée avec npm install -g @deepseek-ai/dsh@<that version> et enregistrée à un endroit que vous retrouverez. 0.1.0-rc.7 était la version actuelle le 18 août 2026. Le nombre importe peu. Le point important est que npx sans version résout le package au moment du démarrage. Un redémarrage automatique peut donc vous faire passer silencieusement à un build utilisant un format de configuration différent.