SSD Nodes Learn Hosting plans →
Guides Matt ConnorPar Matt Connor · Mis à jour le 2026-08-27

Faire tourner dsh sans terminal sur un VPS avec systemd

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

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 est propriétaire. dsh est le lanceur en ligne de commande de DeepSeek Harness, le runtime d’agent de DeepSeek, publié sous licence MIT en version developer preview en août 2026. Un harness est le programme qui entoure le modèle, et non le modèle lui-même. Avec systemd, vous placez donc sous son contrôle la boucle, les outils et les permissions, et non l’inférence de DeepSeek. 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 points à la fois. Le service redémarre après un reboot. Sa sortie est envoyée au journal au lieu de défiler à l’écran. Il s’exécute avec un compte qui n’est pas root. Enfin, il utilise la version que vous avez sélectionnée. Ce dernier 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 VONT ÊTRE INTRODUITES.

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 une fois que npx @deepseek-ai/dsh web sert une page.

D’abord Node, 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), ce qui est ancien 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 alors à l’exécution, sous la forme d’une erreur de syntaxe ou d’un module intégré manquant. C’est un bien plus mauvais moment pour le découvrir. Installez une version actuelle à 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 parce que rediriger directement un script distant vers bash exécute du code que vous n’avez pas lu.

Vérifiez qu’il fonctionne avant d’écrire une unité

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

Laissez ce processus en cours. 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 est à l’é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, et le premier terminal vous indique pourquoi. 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 était la version publiée le 18 August 2026. Vérifiez la version actuelle avec npm view @deepseek-ai/dsh version, puis épinglez la version que vous choisissez d’exécuter.

Installez globalement la version que vous avez figée

npx n’est pas le bon outil 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 preview, sans aucune modification de votre côté. Il nécessite également que le registre npm soit accessible au démarrage. Une machine fonctionnelle peut donc se retrouver avec une unité en échec le jour où le registre ralentit. Installez-le une seule fois, avec une version que vous avez noté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 s’est réellement affiché. npm ls -g affiche la version exacte. C’est la valeur qu’il vous faudra dans six semaines si le comportement change et que vous ne vous souvenez plus de ce que vous avez installé. Si l’installation échoue, si command -v dsh n’affiche ensuite rien, ou si la version obtenue ne correspond pas à celle demandée, consultez les problèmes habituels d’installation et de version de dsh avant d’écrire le fichier d’unité.

Un utilisateur qui possède le 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 avec les privilèges de 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 ses profils. Un profil est un ensemble nommé de bundles de plugins, complété par votre propre couche de correctifs. Les profils web et headless se construisent à partir des modèles fournis lors de leur premier démarrage. Tout ce que vous ajoutez ensuite à cet ensemble s’exécute avec cet utilisateur, avec l’accès de l’agent à ses propres fichiers et à son shell. Vérifier un plugin avant de l’installer fait donc partie de la même tâche que la création du compte. Le premier démarrage écrit des fichiers et peut récupérer des bundles. Effectuez-le 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 manière dont sudo le traite, car la réécriture de HOME par sudo pour une commande sans connexion dépend du paramètre set_home dans /etc/sudoers. Si cette valeur est incorrecte, 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 peut 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= prend le chemin absolu obtenu avec command -v dsh. systemd recherche un nom de commande simple dans une liste de chemins fixe. Cette liste n’est pas celle de votre PATH shell. Un chemin absolu évite donc toute ambiguïté.

WorkingDirectory= définit 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-y l’espace de travail fourni à l’agent. Si le 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 du processus. C’est sûr ici, car tout ce que le service utilise se trouve sous /var/lib/dsh. Si vous définissez l’espace de travail sur un chemin situé sous /home, l’agent indiquera que le répertoire n’existe pas. Ce comportement peut sembler déroutant jusqu’à ce que vous vous rappeliez cette ligne. ProtectSystem=full rend /usr, /boot et /etc accessibles en lecture seule, ce que le service n’a jamais besoin de modifier.

Il peut être tentant d’aller plus loin, mais c’est généralement une mauvaise idée. ProtectSystem=strict rend l’ensemble du système de fichiers accessible en lecture seule, à l’exception des pseudo-systèmes de fichiers du kernel. Le premier appel d’outil qui écrit dans 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 se détache jamais. Cela permet d’obtenir un véritable message d’erreur, contrairement au comportement par défaut. Avec Type=simple, systemd considère le démarrage comme réussi dès que le processus s’est détaché, 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 restent bloquées. Type=forking indique à systemd d’attendre la fin d’un processus parent. Or dsh ne se termine jamais. Le démarrage reste donc bloqué jusqu’à l’expiration de TimeoutStartSec, qui vaut 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. La comparaison complète des types de services systemd présente les autres cas, notamment les situations où il est utile de configurer notify.

Règles de redémarrage qui signalent clairement les échecs

Restart=on-failure redémarre après une sortie avec un code différent de zéro ou après un signal fatal, et laisse l’unité arrêtée après une sortie normale. C’est le comportement adapté à une build de prévisualisation. Si dsh se termine un jour avec le code 0 après avoir lu une configuration qu’il n’accepte pas, 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 le même événement en boucle de redémarrage qui semble saine de loin.

La limitation du débit est souvent omise. Les valeurs par défaut de systemd autorisent cinq démarrages en dix secondes et, avec RestartSec=5s, vous n’atteignez jamais cinq démarrages dans 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.. Réinitialisez 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 lors du boot en cours. Une commande systemctl start seule ne survit pas au redémarrage suivant, et les mises à jour du kernel nécessitent des redémarrages.

systemctl status dsh doit afficher Active: active (running), une ligne 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 réellement un service.

sudo reboot

Reconnectez-vous et exécutez systemctl is-active dsh. La commande affiche active.

Lecture des 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 par priorité. SyslogIdentifier=dsh dans l’unité explique pourquoi ces lignes sont marquées dsh plutôt que node. Cela compte la première fois que vous consultez la sortie du journal sans filtrer par unité.

Vérifiez que le journal survit aux redémarrages avant d’en avoir besoin :

journalctl -u dsh -b -1

Si cette 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 (user interface) sur 127.0.0.1:3080 et refuse de la servir ailleurs. Demandez --host 0.0.0.0 et il s’arrête avec ce message :

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 (application programming interface) pilote l’agent, et l’agent exécute des commandes shell. Un port accessible donne donc un shell sur votre VPS à quiconque le découvre. Les mainteneurs indiquent que l’absence d’authentification distante est la raison pour laquelle le bind est fixé sur loopback. Lisez Ce que signifie réellement la ligne 127.0.0.1:3080 dans la sortie de démarrage avant d’essayer de la modifier. Faites plutôt suivre 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, tel qu’il est résolu sur le VPS. -N signifie qu’aucune commande distante ne doit être exécutée. La session sert donc uniquement à maintenir le tunnel ouvert. Laissez-la fonctionner et ouvrez http://127.0.0.1:3080/ dans votre navigateur. C’est là que vous saisissez la clé d’API DeepSeek, sous Settings puis Models, et que vous choisissez le répertoire de travail. Définissez le workspace sur /var/lib/dsh/workspace, le répertoire appartenant à l’utilisateur du service. Sinon, les outils 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 accédez à http://127.0.0.1:3081/. Enregistrez la 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 l’élément qui le protège : utilisez uniquement des clés, désactivez l’authentification par mot de passe, et appliquez le reste du durcissement de SSH sur votre VPS avec une rigueur accrue. Si ce VPS devient un petit réseau privé, avec une base de données ou un serveur de staging derrière lui, annoncez ces adresses à votre tailnet avec un subnet router pour éviter un forward par service. Le bind loopback de dsh signifie toutefois que l’interface doit toujours être atteinte via un tunnel.

La clé ne doit pas figurer dans le fichier d’unité. Les valeurs Environment= sont affichées par systemctl show dsh -p Environment, que tout utilisateur de la machine peut exécuter. Si un plugin que vous installez nécessite 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. Le fichier sur disque dans lequel chaque paramètre est réellement enregistré, ainsi que ce qui quitte votre machine lorsque vous configurez dsh pour utiliser un endpoint Ollama local au lieu de l’API DeepSeek, font l’objet de la configuration des clés, des modèles et des endpoints de dsh.

Ce que coûte l’exécution

L’inférence s’effectue via l’API DeepSeek, et non sur votre VPS. Votre serveur prend en charge le processus Node, l’interface qu’il sert et chaque commande que l’agent décide d’exécuter. Les deux premiers éléments consomment peu de ressources et de manière stable. Le troisième n’est limité par rien dans ce fichier d’unité.

Mesurez la valeur minimale sur votre propre serveur au lieu de vous fier à un chiffre obtenu 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 se retrouvent donc dans le 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 largement 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 tue, 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 à l’origine du problème.

La solution consiste à définir volontairement une limite. MemoryMax= et CPUQuota= dans la section [Service] confinent les dégâts à l’unité. Une compilation qui s’emballe est ainsi tuée au lieu de bloquer tout le serveur. La section Limiter la mémoire et le CPU avec systemd présente 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 cherchez 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 comme unité lorsque vous voulez qu’il soit toujours actif 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 Failed to locate executable /usr/local/bin/dsh: No such file or directory. Le chemin indiqué dans ExecStart= ne correspond pas à celui affiché par command -v dsh. Il s’agit de l’échec signalé par Type=exec à l’heure systemctl start, au lieu de le masquer.

status=217/USER. Le compte indiqué dans User= n’existe pas. Vérifiez-le 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 le processus npx lancé dans un autre terminal est toujours actif. 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’un premier lancement a été effectué en tant que 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 des démarrages et 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 test 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 opération que vous déclenchez, et non un événement que vous subissez. Lisez d’abord les notes de version, car l’avertissement de l’éditeur concernant les changements incompatibles 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 en arrière consiste à appliquer la 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 où une mise à niveau réécrit 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 rattaché à votre session de connexion. Il est donc arrêté quand la session se termine. Une unité systemd est rattaché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 ce boot.

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 que execve() réussisse 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 au bout de 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 refuse --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 faire exécuter des commandes shell à l’agent 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 aussi 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 rencontrez 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 lorsque vous configurez le service, installée avec npm install -g @deepseek-ai/dsh@<that version> et consignée à un endroit où vous la retrouverez. 0.1.0-rc.7 était la version actuelle le 18 août 2026. Le nombre n’est pas le plus important. Le point important est que npx sans version résout le paquet au moment du démarrage. Un redémarrage unattended peut donc vous faire passer silencieusement à une build utilisant un autre format de configuration.