Ejecutar dsh sin terminal en un VPS con systemd
Configure dsh como servicio systemd en un VPS con usuario dedicado, version fijada, reinicio automatico, logs con journalctl y tunel SSH para la interfaz.
Ejecutar dsh sin terminal en un VPS
Ejecutar dsh sin terminal en un VPS requiere un archivo de unidad de systemd y un usuario dedicado que sea su propietario. dsh es el iniciador de línea de comandos de DeepSeek Harness, el entorno de ejecución de agentes de DeepSeek, publicado con licencia MIT en versión preliminar para desarrolladores en agosto de 2026. La guía de inicio rápido indica que debe escribir npx @deepseek-ai/dsh web. Esto es correcto, pero el proceso también termina en cuanto cierra la sesión SSH (secure shell).
Un archivo de unidad resuelve cuatro aspectos a la vez. El servicio vuelve a iniciarse después de un reinicio. Su salida se envía al journal en lugar de desplazarse por la terminal. Se ejecuta con una cuenta que no es root. Además, ejecuta la versión que usted seleccionó. Esto es más importante de lo habitual porque el proyecto original lo indica en mayúsculas:
DeepSeek Harness está actualmente en versión preliminar para desarrolladores y evoluciona rápidamente. HABRÁ CAMBIOS QUE ROMPERÁN LA COMPATIBILIDAD.
Esta guía supone que dsh ya funciona correctamente cuando lo ejecuta manualmente. Si no es así, empiece por instalar DeepSeek Harness en un VPS y vuelva cuando npx @deepseek-ai/dsh web sirva una página.
Primero Node, porque npm no emitirá advertencias
node -vEl paquete propio de Ubuntu 24.04 es Node 18 (18.19.1 en agosto de 2026), una versión antigua para un paquete publicado este año. @deepseek-ai/dsh no publica ningún campo engines, por lo que npm no muestra ninguna advertencia EBADENGINE cuando la versión de Node es demasiado antigua. El fallo aparece durante la ejecución, como un error de sintaxis o un componente integrado ausente. Ese es un momento mucho peor para detectarlo. Instale una versión actual de soporte a largo plazo (LTS) desde 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 ahora debería mostrar una versión v22. La línea less existe porque canalizar directamente un script remoto a bash ejecuta código que no ha leído.
Compruebe que se ejecuta antes de escribir una unidad
npx @deepseek-ai/dsh@0.1.0-rc.7 webDéjelo en ejecución. Desde una segunda sesión SSH:
curl -fsS http://127.0.0.1:3080/ -o /dev/null && echo upup significa que el perfil web está escuchando en loopback, que es donde se enlaza de forma predeterminada. curl: (7) Failed to connect to 127.0.0.1 port 3080: Connection refused significa que no lo está haciendo, y el primer terminal indica el motivo. Detenga la ejecución manual con Ctrl+C antes de continuar: una unidad que intenta enlazarse a un puerto que ya está ocupado por otro proceso falla con Error: listen EADDRINUSE: address already in use 127.0.0.1:3080.
0.1.0-rc.7 era la versión publicada el 18 August 2026. Compruebe cuál es la versión actual con npm view @deepseek-ai/dsh version y, después, fije la versión que decida ejecutar.
Instale globalmente la versión fijada
npx es la herramienta incorrecta dentro de un archivo de unidad. Resuelve la versión del paquete cuando se inicia el proceso. Por tanto, un reinicio dentro de tres meses puede iniciar una compilación diferente de un agente en fase preliminar sin que usted haya cambiado nada. Además, necesita que el registro de npm esté accesible durante el arranque. Si el registro funciona con lentitud, una máquina operativa se convierte en una unidad fallida. Instálelo una vez, con una versión que haya anotado:
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 muestra /usr/bin/dsh cuando npm procede de NodeSource y /usr/local/bin/dsh cuando procede del paquete propio de Ubuntu. Use la ruta que haya mostrado realmente en el archivo de unidad. npm ls -g muestra la versión exacta. Esa es la respuesta que necesitará dentro de seis semanas, cuando cambie el comportamiento y no recuerde qué instaló.
Un usuario propietario del servicio y nada más
El agente ejecuta comandos de shell. Esa es su función. Si se ejecuta como root, cada llamada a una herramienta se ejecuta con privilegios de root. Por eso, asígnele su propia cuenta sin shell de inicio de sesión.
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 se convierte en DSH_HOME, el directorio donde dsh guarda los perfiles. Un perfil es un conjunto con nombre de paquetes de plugins y una capa de parches propia encima. Los perfiles web y headless se crean a partir de las plantillas incluidas la primera vez que se inician. Ese primer inicio escribe archivos y puede descargar paquetes. Por eso, ejecútelo manualmente para poder supervisarlo.
sudo -u dsh env HOME=/var/lib/dsh DSH_HOME=/var/lib/dsh/harness /usr/bin/dsh --profile webEstablezca HOME explícitamente en lugar de confiar en lo que sudo haga con él, porque que sudo reescriba HOME para un comando que no sea de inicio de sesión depende del ajuste set_home de /etc/sudoers. Si lo configura mal, la primera ejecución creará directorios de caché en su directorio personal, propiedad de dsh. Después, el servicio no podrá encontrar su propio estado. Deténgalo con Ctrl+C cuando la comprobación de curl devuelva up.
El archivo de unidad
Escriba /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= toma la ruta absoluta obtenida de command -v dsh. systemd busca los nombres de comandos sin ruta en una lista de rutas fija, pero esa lista no es el valor de PATH de su shell. Por tanto, una ruta absoluta evita la incertidumbre.
WorkingDirectory= es el directorio donde se resuelven las rutas relativas y donde comienza una llamada a una herramienta que ejecuta ls sin argumentos. Indique aquí el workspace que entrega al agente. Si el directorio no existe o el usuario del servicio no puede acceder a él, la unidad falla con status=200/CHDIR antes de que dsh se ejecute.
ProtectHome=true oculta /home y /root al proceso. Esto es seguro porque todo lo que usa el servicio se encuentra bajo /var/lib/dsh. Si indica el workspace con una ruta bajo /home, el agente informará de que el directorio no existe. Esto resulta confuso hasta recordar esta línea. ProtectSystem=full hace que /usr, /boot y /etc sean de sólo lectura, algo que el servicio nunca necesita modificar.
Ir más allá resulta tentador y normalmente es incorrecto. ProtectSystem=strict hace que todo el sistema de archivos sea de sólo lectura, salvo los pseud sistemas de archivos del kernel. Por tanto, la primera llamada a una herramienta que escriba un archivo falla con EROFS: read-only file system. Si necesita ese nivel, añada ReadWritePaths=/var/lib/dsh en la misma edición.
Qué valor de Type= corresponde aquí
Type=exec, porque dsh permanece en primer plano y nunca crea un proceso hijo. Esto ofrece un mensaje de error real en lugar del comportamiento predeterminado. Con Type=simple, systemd considera que el inicio se ha completado correctamente en cuanto crea el proceso hijo, antes de saber siquiera si el binario existe, por lo que systemctl start dsh termina correctamente y el fallo sólo aparece en el journal. Con Type=exec, systemd espera a que execve() termine correctamente, de modo que un error tipográfico en ExecStart= hace que falle el comando que acaba de escribir, y podrá verlo ahí.
Las dos respuestas incorrectas se quedan bloqueadas. Type=forking indica a systemd que espere a que termine un proceso padre, pero dsh nunca termina, por lo que el inicio se bloquea hasta que vence TimeoutStartSec (90 segundos de forma predeterminada) y después informa de Job for dsh.service failed because a timeout was exceeded. Type=notify espera un mensaje READY=1 a través de sd_notify, y un proceso de Node que nunca envía ese mensaje se bloquea de la misma forma. La comparación completa de los tipos de servicio de systemd explica el resto, incluido cuándo merece la pena configurar notify.
Reglas de reinicio que fallan de forma visible
Restart=on-failure reinicia tras una salida distinta de cero o una señal fatal, y deja la unidad detenida después de una salida correcta. Ese es el comportamiento que necesita una compilación preliminar. Si dsh termina alguna vez con el código 0 porque ha leído una configuración que no admite, la unidad se detiene y permanece detenida, y systemctl status dsh muestra inactive (dead), donde puede verlo. Restart=always convierte ese mismo evento en un bucle de reinicio que parece saludable a distancia.
El límite de frecuencia es la parte que suele omitirse. Los valores predeterminados de systemd son cinco arranques en diez segundos y, con RestartSec=5s, nunca se alcanzan cinco arranques dentro de una ventana de diez segundos. Por tanto, una unidad que falla al arrancar se reinicia indefinidamente y sólo el journal lo registra. StartLimitIntervalSec=300 junto con StartLimitBurst=5 significa que cinco fallos en cinco minutos son suficientes: systemd se rinde y deja la unidad en failed, registrando Start request repeated too quickly.. Borre ese estado con sudo systemctl reset-failed dsh después de corregir la causa. Ambas opciones pertenecen a [Unit], no a [Service], y systemd las ignora silenciosamente si se encuentran en la sección incorrecta.
Inícialo y compruébalo
sudo systemctl daemon-reload
sudo systemctl enable --now dsh
systemctl status dshenable --now realiza dos tareas. enable hace que el servicio vuelva a iniciarse después de un reinicio y --now lo inicia durante este arranque. Un systemctl start independiente deja de estar activo después del siguiente reinicio, y las actualizaciones del kernel requieren reinicios.
systemctl status dsh debería mostrar Active: active (running), una Main PID y una línea Memory:. Después, confirma dónde está escuchando:
sudo ss -lntp | grep 3080Debe aparecer 127.0.0.1:3080. Si aparece 0.0.0.0:3080, algo ha modificado la dirección de enlace y tu agente está expuesto a Internet pública. El nombre del proceso en esa salida es node, no dsh, porque el binario dsh es un script de Node, por lo que pgrep -x dsh no encuentra nada. Usa systemctl show -p MainPID dsh en su lugar.
Después, reinicia una vez. Un servicio que nunca ha sobrevivido a un reinicio todavía no es un servicio.
sudo rebootVuelve a conectarte y ejecuta systemctl is-active dsh. Muestra active.
Lectura de los registros con journalctl
Todo lo que dsh escribe en stdout y stderr termina en el journal con el nombre de la unidad.
journalctl -u dsh -f
journalctl -u dsh -n 200 --no-pager
journalctl -u dsh --since "10 min ago" -p err-f sigue las líneas nuevas, -n muestra las últimas N y -p err filtra por prioridad. SyslogIdentifier=dsh en la unidad es el motivo por el que esas líneas tienen la etiqueta dsh en lugar de node. Esto es importante la primera vez que lee la salida del journal sin filtrar por unidad.
Compruebe que el journal sobreviva a los reinicios antes de necesitarlo:
journalctl -u dsh -b -1Si muestra Specifying boot ID or boot offset has no effect, no persistent journal was found, el journal se almacena en /run y se elimina en cada reinicio. Cree el directorio y reinicie el daemon:
sudo mkdir -p /var/log/journal
sudo systemctl restart systemd-journaldAcceda a la interfaz mediante un túnel SSH, no mediante un puerto público
dsh sirve la interfaz web en 127.0.0.1:3080 y se niega a servirla en cualquier otro lugar. Si solicita --host 0.0.0.0, se detiene y muestra lo siguiente:
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 insteadEsto no es una limitación que deba sortear. La API web controla el agente, y el agente ejecuta comandos de shell. Por tanto, un puerto accesible equivale a ofrecer un shell en su VPS a cualquiera que lo encuentre. Los mantenedores indican que la autenticación remota aún no está implementada como motivo para fijar la escucha a loopback. Reenvíe el puerto desde su propio equipo:
ssh -N -L 3080:127.0.0.1:3080 you@203.0.113.10-L 3080:127.0.0.1:3080 abre el puerto 3080 en su portátil y envía todo lo que llegue allí a 127.0.0.1:3080, resuelto en el VPS. -N indica que no se ejecute ningún comando remoto, por lo que la sesión sólo mantiene abierto el túnel. Déjela en ejecución y abra http://127.0.0.1:3080/ en el navegador. Allí introducirá la clave de la API de DeepSeek, en Settings y después Models, y seleccionará el directorio del espacio de trabajo. Configure el espacio de trabajo con /var/lib/dsh/workspace, el directorio propiedad del usuario del servicio, o las herramientas de archivos del agente fallarán con EACCES: permission denied.
Si el puerto 3080 está ocupado en su portátil, ssh lo indica:
bind [127.0.0.1]:3080: Address already in use
channel_setup_fwd_listener_tcpip: cannot listen to port: 3080Elija otro puerto local con ssh -N -L 3081:127.0.0.1:3080 you@203.0.113.10 y después acceda a http://127.0.0.1:3081/ en el navegador. Guarde este comando en ~/.ssh/config en su propio equipo:
Host dsh-vps
HostName 203.0.113.10
User you
LocalForward 3080 127.0.0.1:3080Después de eso, ssh -N dsh-vps es el comando completo. Este túnel es ahora la única puerta de acceso a su agente, por lo que el daemon SSH es el elemento que lo protege: use sólo claves, desactive la autenticación mediante contraseña y aplique el resto de medidas para reforzar SSH en su VPS con más rigor de lo habitual.
La clave no debe estar en el archivo de unidad. Los valores de Environment= se muestran mediante systemctl show dsh -p Environment, que cualquier usuario del sistema puede ejecutar. Si un plugin que instala necesita una clave en el entorno, colóquela en /etc/dsh.env con permisos 600 y propiedad de root, y haga referencia a ella con EnvironmentFile=/etc/dsh.env. systemd lee ese archivo como root en el momento de ejecutar el proceso, y systemctl show no muestra su contenido.
Coste de ejecución
La inferencia se realiza en la API de DeepSeek, no en su VPS. Su servidor ejecuta el proceso de Node, la interfaz que sirve y cada comando que el agente decide ejecutar. Los dos primeros consumen recursos de forma estable y reducida. El tercero no está limitado por nada en este archivo de unidad.
Mida el consumo mínimo en su propio servidor en lugar de confiar en una cifra obtenida de otro:
systemctl show dsh -p MemoryCurrent
systemd-cgtop -1 --depth 2MemoryCurrent se expresa en bytes. Supervise este valor mientras el agente trabaja, no cuando está inactivo.
Las llamadas a herramientas son procesos secundarios del servicio. Por tanto, se ejecutan en el mismo grupo de control y están sujetas a los mismos límites. Un agente que ejecuta npm install o una suite de pruebas dentro del workspace puede usar mucha más memoria que el propio arnés. En un VPS de 1 GB, ahí es donde aparecen los fallos: el kernel selecciona un proceso y lo finaliza, y journalctl -k | grep -i "out of memory" muestra la línea Out of memory: Killed process con el nombre del proceso elegido. A menudo, ese proceso no es el que causó el problema.
La solución consiste en establecer un límite de forma intencionada. MemoryMax= y CPUQuota= en la sección [Service] mantienen el impacto dentro de la unidad. Así, una compilación descontrolada se finaliza en lugar de bloquear todo el servidor. Limitar la memoria y la CPU con systemd explica los valores y el comportamiento ante fallos. El uso de disco también aumenta, debido al historial de sesiones en DSH_HOME y a todo lo que el agente escriba en el workspace. Por tanto, incluya du -sh /var/lib/dsh en la herramienta que ya utilice para supervisar el disco.
Si necesita un agente interactivo al que pueda conectarse y del que pueda desconectarse, un servicio no es la opción adecuada. Ejecutar un agente en una sesión persistente de tmux se adapta mejor. Ejecute dsh como unidad cuando necesite que esté siempre activo y sea accesible mediante un túnel.
Modos de fallo y mensajes que verá
status=203/EXEC. systemd no pudo ejecutar el archivo y los registros Failed to locate executable /usr/local/bin/dsh: No such file or directory. La ruta de ExecStart= no coincide con lo que mostró command -v dsh. Este es el fallo que Type=exec informa en systemctl start en lugar de ocultarlo.
status=217/USER. La cuenta de User= no existe. Confírmelo con id dsh.
status=200/CHDIR. Falta WorkingDirectory= o el usuario del servicio no puede acceder a él. sudo -u dsh ls /var/lib/dsh/workspace lo reproduce directamente.
Error: listen EADDRINUSE: address already in use 127.0.0.1:3080. Otro proceso ya ocupa el puerto. Normalmente, una ejecución de npx quedó abierta en otro terminal. sudo ss -lntp | grep 3080 identifica el proceso.
EACCES: permission denied seguido de una ruta. La propiedad dentro de /var/lib/dsh es incorrecta. Normalmente, esto ocurre porque la primera ejecución se hizo como root o con un HOME incorrecto. sudo chown -R dsh:dsh /var/lib/dsh lo corrige.
Start request repeated too quickly. La unidad alcanzó el límite de frecuencia de arranque y dejó de intentarlo. El error real aparece en las líneas anteriores. Ejecute sudo systemctl reset-failed dsh antes de volver a intentarlo.
La unidad está en active (running), pero el navegador no muestra nada. Ejecute la comprobación en el VPS: si allí curl -fsS http://127.0.0.1:3080/ -o /dev/null && echo up muestra up, el servicio funciona correctamente y el problema está en el reenvío de puertos.
Actualizar de forma controlada
Fijar una versión significa que la actualización es una acción planificada, no un cambio inesperado. Lea primero las notas de la versión, porque la advertencia del propio proyecto sobre los cambios incompatibles es precisamente el motivo para fijar la versión. Haga una copia de seguridad del directorio de estado y, después, cambie la versión:
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-pagerLa reversión es el mismo npm install -g con la versión anterior, además de restaurar ese archivo tar. Esto sólo funciona si lo guardó. Un runtime de agente en fase preliminar es precisamente el tipo de software en el que una actualización puede reescribir el formato de configuración sin avisar.
FAQ
¿Por qué dsh se detiene cuando cierro mi sesión SSH?
Porque npx @deepseek-ai/dsh web es un proceso en primer plano propiedad de la sesión de inicio de sesión, por lo que se termina cuando finaliza la sesión. Una unidad de systemd pertenece al sistema init, por eso sigue ejecutándose después de desconectarse y vuelve a iniciarse tras un reinicio. sudo systemctl enable --now dsh es el par de pasos que proporciona ambas cosas: enable para el reinicio y --now para este arranque.
¿Debo usar Type=simple o Type=exec para dsh?
Type=exec. dsh se ejecuta en primer plano y nunca crea un proceso bifurcado, por lo que ambas opciones funcionan, pero Type=exec hace que systemd espere a que execve() termine correctamente antes de considerar correcto el inicio. Una ruta incorrecta en ExecStart= hace que systemctl start falle de inmediato, con status=203/EXEC visible. Con Type=simple, el mismo error devuelve un estado correcto y queda oculto en el journal. Type=forking y Type=notify también son incorrectos en este caso, y ambos esperan hasta que TimeoutStartSec vence después de 90 segundos.
¿Cómo abro la interfaz web de dsh desde mi portátil?
Reenvía el puerto mediante SSH: ssh -N -L 3080:127.0.0.1:3080 you@your-vps y abre http://127.0.0.1:3080/ en el navegador. No intentes enlazar el servicio a una dirección pública. dsh rechaza --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 porque la API web puede hacer que el agente ejecute comandos de shell y no hay autenticación remota delante de ella.
¿Puedo ejecutar dsh como root para simplificar los permisos?
No. El arnés sirve para ejecutar comandos y escribir archivos, por lo que los privilegios del servicio son también los privilegios del agente. Crea una cuenta de sistema con useradd --system --shell /usr/sbin/nologin dsh, asígnale la propiedad de /var/lib/dsh y añade NoNewPrivileges=true a la unidad. Si después aparece EACCES: permission denied, la causa habitual es que una ejecución anterior como root dejó archivos propiedad de root, y sudo chown -R dsh:dsh /var/lib/dsh lo corrige.
¿Qué versión de dsh debo fijar en la unidad?
La que indique npm view @deepseek-ai/dsh version al configurar el servicio, instalada con npm install -g @deepseek-ai/dsh@<that version> y registrada en un lugar donde puedas encontrarla. 0.1.0-rc.7 era la versión actual el 18 de agosto de 2026. El número no es lo importante. Lo importante es que npx sin una versión resuelve el paquete en el momento del inicio, por lo que un reinicio desatendido puede cambiarte silenciosamente a una compilación con un formato de configuración diferente.