Ejecutar dsh sin terminal en un VPS con systemd
Configura dsh como servicio systemd con usuario dedicado, versión fijada, reinicio automático, logs en journalctl y túnel 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 específico que sea su propietario. dsh es el lanzador 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. Un harness es el programa que rodea al modelo, no el modelo en sí. Por tanto, lo que se ejecuta bajo systemd es el bucle, las herramientas y los permisos, no la inferencia de DeepSeek. 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, se ejecuta la versión que seleccionó. Esto es más importante de lo habitual porque el proyecto indica lo siguiente en mayúsculas:
DeepSeek Harness está actualmente en versión preliminar para desarrolladores y evoluciona rápidamente. HABRÁ CAMBIOS QUE ROMPAN LA COMPATIBILIDAD.
Esta guía presupone que dsh ya funciona 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), que es antiguo 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 en tiempo de ejecución, como un error de sintaxis o una funcionalidad integrada inexistente. Es un lugar 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 está ahí porque canalizar un script remoto directamente 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 webDeje ese proceso en ejecución. Desde una segunda sesión SSH:
curl -fsS http://127.0.0.1:3080/ -o /dev/null && echo upup indica que el perfil web está escuchando en la interfaz de loopback, que es donde se enlaza de forma predeterminada. curl: (7) Failed to connect to 127.0.0.1 port 3080: Connection refused indica que no lo está, y el primer terminal muestra 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 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 no es la herramienta adecuada 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 acceder al registro de npm durante el arranque. Si el registro responde con lentitud, una máquina que funcionaba puede terminar con una unidad fallida. Instálelo una sola vez y con una versión registrada:
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 en el archivo de unidad la ruta que realmente haya mostrado. npm ls -g muestra la versión exacta. Esa es la información que necesitará dentro de seis semanas si cambia el comportamiento y no recuerda qué instaló. Si la instalación falla, si command -v dsh no muestra nada después o si la versión obtenida no es la que solicitó, siga los procedimientos habituales para resolver fallos de instalación y versión de dsh antes de escribir el archivo de unidad.
Un usuario propietario del servicio y de nada más
El agente ejecuta comandos de shell. Esa es su función. Si lo ejecuta como root, cada llamada a una herramienta se ejecuta como 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 una pila con nombre de paquetes de plugins y una capa de parches propia encima. Los perfiles web y headless se generan a partir de plantillas incluidas la primera vez que se inician. Todo lo que añada después a esa pila se ejecutará como este usuario, con el acceso del propio agente a los archivos y al shell. Por eso, validar un plugin antes de instalarlo forma parte de la misma tarea que crear la cuenta. El primer inicio escribe archivos y puede descargar paquetes. 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 ella, porque que sudo reescriba HOME para un comando que no sea de inicio de sesión depende del ajuste set_home en /etc/sudoers. Si lo configura mal, la primera ejecución crea directorios de caché en su directorio personal, propiedad de dsh, y el servicio no puede encontrar después su propio estado. Deténgalo con Ctrl+C cuando la comprobación 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= recibe 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 PATH de su shell. Por tanto, una ruta absoluta evita la ambigüedad.
WorkingDirectory= es la ruta donde se resuelven las rutas relativas y el directorio inicial de una llamada a una herramienta que ejecuta ls sin argumentos. Asígnele el espacio de trabajo que entregará 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 se ejecute dsh.
ProtectHome=true oculta /home y /root al proceso. Aquí es seguro porque todo lo que utiliza el servicio está dentro de /var/lib/dsh. Si asigna el espacio de trabajo a una ruta situada bajo /home, el agente indicará 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.
Aumentar el aislamiento puede parecer conveniente, pero normalmente es un error. ProtectSystem=strict hace que todo el sistema de archivos sea de sólo lectura, excepto 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 de aislamiento, 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 proporciona un mensaje de error real, a diferencia del comportamiento predeterminado. Con Type=simple, systemd considera correcto el inicio en cuanto el proceso ha creado un proceso hijo, antes de saber siquiera si existe el binario. Por eso, systemctl start dsh termina correctamente y el fallo sólo aparece en el journal. Con Type=exec, systemd espera a que execve() se complete correctamente. Así, un error tipográfico en ExecStart= hace que falle el comando que acaba de ejecutar y puede verlo directamente.
Las otras dos opciones incorrectas se quedan bloqueadas. Type=forking indica a systemd que espere a que termine un proceso padre, pero dsh nunca termina. Por tanto, el inicio se bloquea hasta que vence TimeoutStartSec, que es de 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 manera. 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 sale alguna vez con 0 porque ha leído una configuración que no acepta, la unidad se detiene y permanece detenida, y systemctl status dsh muestra inactive (dead), donde puede verlo. Restart=always convierte el 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 para siempre y sólo el journal lo registra. StartLimitIntervalSec=300 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 cuando haya corregido la causa. Ambas opciones deben estar en [Unit], no en [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 cumple dos funciones. enable es lo que vuelve a iniciar el servicio después de un reinicio, y --now lo inicia durante este arranque. Un systemctl start sin más deja de estar activo tras el 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.
Leer 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 explica por qué esas líneas aparecen etiquetadas como dsh en lugar de node. Esto es importante la primera vez que se 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 (interfaz de usuario) en 127.0.0.1:3080 y se niega a servirla en cualquier otro lugar. Si solicita --host 0.0.0.0, se detiene con este mensaje:
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 insteadNo es una limitación que deba eludir. La API web (interfaz de programación de aplicaciones) controla el agente, y el agente ejecuta comandos de shell. Por tanto, un puerto accesible equivale a un shell en su VPS para 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. Vale la pena leer Qué significa realmente la línea 127.0.0.1:3080 en la salida de arranque antes de intentar cambiarla. Redirija 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éjelo ejecutándose y abra http://127.0.0.1:3080/ en el navegador. Allí debe 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 acceda después a http://127.0.0.1:3081/ en el navegador. Guarde el 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, ssh -N dsh-vps es el comando completo. Este túnel es ahora la única puerta de acceso al 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. Si ese VPS se convierte en una pequeña red privada, con una base de datos o un equipo de staging detrás, anunciar esas direcciones a su tailnet mediante un router de subred evita tener que crear una redirección por servicio, aunque la escucha de dsh en loopback significa que la interfaz seguirá accediéndose mediante un túnel.
La clave no debe estar en el archivo de unidad. Los valores de Environment= los muestra systemctl show dsh -p Environment, que cualquier usuario del equipo puede ejecutar. Si un plugin que instala necesita una clave en el entorno, colóquela en /etc/dsh.env con el modo 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. El archivo del disco en el que termina realmente cada ajuste y qué datos salen del equipo cuando configura dsh para usar un endpoint local de Ollama en lugar de la API de DeepSeek se explica en configurar las claves, los modelos y los endpoints de dsh.
Qué cuesta ejecutar el servicio
La inferencia se ejecuta en la API de DeepSeek, no en tu VPS. El servidor consume recursos para el proceso de Node, la interfaz que sirve y cada comando que el agente decide ejecutar. Los dos primeros consumos son constantes y reducidos. El tercero no está limitado por nada en este archivo de unidad.
Mide el consumo mínimo en tu 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. Obsérvalo mientras el agente trabaja, no cuando está inactivo.
Las llamadas a herramientas son procesos secundarios del servicio. Por tanto, se incluyen en el mismo grupo de control y cuentan para los mismos límites. Un agente que ejecuta npm install o una suite de pruebas dentro del espacio de trabajo puede usar mucha más memoria que el propio entorno de ejecución. En un VPS de 1 GB es donde suelen producirse los fallos: el kernel elige un proceso y lo termina, 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 es 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 termina en lugar de bloquear todo el servidor. Limitar la memoria y la CPU con systemd explica los valores y el comportamiento en caso de fallo. El uso de disco también crece por el historial de sesiones en DSH_HOME y por todo lo que el agente escribe en el espacio de trabajo. Por tanto, incluye du -sh /var/lib/dsh en la herramienta que ya utilices para supervisar el disco.
Si necesitas un agente interactivo al que puedas conectarte y desconectarte, un servicio no tiene la estructura adecuada. En ese caso, ejecutar un agente en una sesión persistente de tmux es una opción más apropiada. Ejecuta dsh como unidad cuando quieras que esté siempre activo y 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 el momento 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. Algo ya está usando el puerto. Normalmente, una ejecución de npx quedó abierta en otro terminal. sudo ss -lntp | grep 3080 muestra el proceso.
EACCES: permission denied seguido de una ruta. La propiedad dentro de /var/lib/dsh es incorrecta. Normalmente, la primera ejecución se hizo como root o con el 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 cambios incompatibles es precisamente el motivo para fijar la versión. Haga una copia de seguridad del directorio de estado y cambie después 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-pagerRevertir es lo mismo npm install -g con la versión anterior y restaurar ese archivo tar. Esto sólo funciona si lo guardó. Un entorno de ejecución 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 previo aviso.
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 de inicio, por eso sigue ejecutándose después de desconectarse y vuelve a iniciarse tras un reinicio. sudo systemctl enable --now dsh es el conjunto 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 hijo, por lo que ambas opciones funcionan, pero Type=exec hace que systemd espere a que execve() se complete correctamente antes de considerar correcto el inicio. Una ruta incorrecta en ExecStart= hace que systemctl start falle 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 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íe el puerto mediante SSH: ssh -N -L 3080:127.0.0.1:3080 you@your-vps; después, abra http://127.0.0.1:3080/ en el navegador. No intente 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 existe para ejecutar comandos y escribir archivos, por lo que los privilegios del servicio son también los privilegios del agente. Cree una cuenta de sistema con useradd --system --shell /usr/sbin/nologin dsh, asígnele la propiedad de /var/lib/dsh y añada NoNewPrivileges=true a la unidad. Si después aparece EACCES: permission denied, la causa habitual es que una ejecución anterior como root haya dejado archivos propiedad de root; sudo chown -R dsh:dsh /var/lib/dsh lo corrige.
¿Qué versión de dsh debo fijar en la unidad?
La que npm view @deepseek-ai/dsh version muestre al configurar el servicio, instalada con npm install -g @deepseek-ai/dsh@<that version> y registrada en un lugar localizable. 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 cambiarlo silenciosamente a una compilación con un formato de configuración diferente.