SSD Nodes Learn Hosting plans →
Guías Matt ConnorPor Matt Connor

Cómo mantener un comando tras desconectar SSH

Si SSH se desconecta, el kernel envía SIGHUP y el proceso termina. Compara nohup, disown, tmux y systemd-run para elegir la opción adecuada.

Por qué el comando termina cuando se desconecta SSH

Para mantener un comando en ejecución después de una desconexión de SSH, el comando debe terminar en un lugar al que no pueda llegar la señal de cierre. Cada método siguiente organiza esto de una forma distinta. Por eso, conviene empezar por el mecanismo.

La sesión de inicio de sesión se ejecuta en una pty (pseudo-terminal), un dispositivo de terminal virtual que sshd crea en el servidor para la sesión. Es el terminal de control de la shell y de todos los comandos que se inician desde ella. Para conocer el resto de este proceso, consulte qué configura SSH al iniciar sesión. Cuando se interrumpe la conexión TCP, sshd cierra su extremo y la pty se destruye. El kernel interpreta esto como el cierre del terminal. Por eso envía SIGHUP al grupo de procesos en primer plano de ese terminal y al líder de la sesión, que es la shell. La acción predeterminada de SIGHUP es finalizar el proceso. El comando estaba en el grupo de procesos en primer plano, por lo que termina.

Los trabajos en segundo plano tampoco son seguros. Un trabajo iniciado con & se ejecuta en su propio grupo de procesos, por lo que el kernel no le envía la señal directamente. Bash sí lo hace. Al recibir SIGHUP, un bash interactivo reenvía SIGHUP a todos los trabajos de su tabla antes de salir. El resultado es el mismo desde el punto de vista del usuario: el trabajo desaparece y el archivo de registro queda detenido a mitad de una línea.

Aquí hay una asimetría que suele causar confusión. Escribir exit no cierra los trabajos en segundo plano, porque bash sólo lo hace cuando la opción huponexit está establecida, y está desactivada de forma predeterminada. Una conexión interrumpida sí los cierra. El trabajo que sobrevivió al cierre ordenado del terminal puede terminar cuando se pierde la conexión wifi.

De aquí se derivan dos consecuencias, que constituyen todo el problema. Un proceso que ignora SIGHUP, o que no tiene ningún terminal de control, no recibirá el cierre. Además, un proceso cuya salida estándar todavía apunta a la pty destruida no tiene dónde escribir: la escritura falla con EIO (error de entrada/salida), y la mayoría de los programas terminan en ese momento. Hay que resolver ambas partes. Muchas recetas sólo resuelven la primera. Por eso se informa de que «nohup no funcionó».

Si la conexión se interrumpe varias veces al día, corrija también ese problema. ServerAliveInterval 60 en ~/.ssh/config evita que una sesión inactiva sea descartada por un tiempo de espera de NAT (traducción de direcciones de red) en algún punto de la ruta. Una sesión que nunca llega a abrirse es un fallo distinto, con causas diferentes. En ese caso resulta importante la diferencia entre conexión rechazada y conexión agotada por tiempo de espera.

¿Qué método mantiene un comando en ejecución después de desconectarse de SSH?

Hay cuatro opciones, ordenadas según la importancia del trabajo.

  • nohup o setsid: un trabajo puntual que inicia ahora y cuyo registro revisa después. Debe redirigir la salida manualmente.
  • disown: un trabajo que ya inició y que olvidó proteger. Recupera el proceso, pero no puede devolverle la salida.
  • tmux o screen: trabajo que necesita supervisar, interrumpir y retomar durante varios días.
  • systemd-run o un archivo de unidad real: cualquier trabajo que deba continuar después de cerrar la sesión, como un rsync de seis horas o una importación de base de datos durante la noche.

La regla que conviene recordar es la siguiente: si olvidar el trabajo sería un problema, debe pertenecer a systemd y no a tmux. Una ventana de tmux depende de que una persona la recuerde. Una unidad tiene un nombre, un estado, un registro y una política de reinicio que la siguiente persona puede encontrar sin que nadie tenga que indicárselo.

nohup y setsid: inícializar el proceso y desconectarse

nohup ./import.sh > ~/import.log 2>&1 &
echo $! > ~/import.pid

nohup establece la disposición de SIGHUP para ignorarla y después ejecuta el comando. Así, la señal de cierre de la terminal llega al proceso y no produce ningún efecto. La redirección debe escribirla usted. Si deja la salida estándar apuntando a la terminal, nohup la redirige automáticamente a nohup.out en el directorio actual. Si no puede hacerlo, usa $HOME/nohup.out y muestra:

nohup: ignoring input and appending output to 'nohup.out'

Es fácil perder de vista ese archivo, así que asígnele un nombre. $! contiene el PID (identificador de proceso) del último trabajo en segundo plano. Guardarlo permite comprobar el estado del trabajo después de volver a iniciar sesión.

setsid aborda el mismo problema desde el lado contrario. Ejecuta el comando en una sesión nueva sin terminal de control. Por tanto, no existe ninguna terminal que pueda cerrarse y afectar al proceso.

setsid --fork ./import.sh > ~/import.log 2>&1

Use --fork. Sin esa opción, setsid ejecuta setsid() en el mismo proceso cuando el proceso aún no es un líder de grupo de procesos. Esto ocurre dentro de un script de shell y hace que el script quede bloqueado. Con --fork, el comportamiento es el mismo en un script y en el prompt.

Compruebe qué se inició realmente:

ps -o pid,ppid,sid,tty,stat,cmd -p "$(cat ~/import.pid)"

Una columna TTY de ? indica que el proceso no tiene una terminal de control. Por tanto, ninguna terminal puede afectarlo al cerrarse. Con nohup, la columna TTY sigue mostrando algo como pts/0 mientras mantiene la conexión y pasa a mostrar ? cuando se destruye el pty. Ambos resultados son correctos. El trabajo sobrevivió.

disown: recuperar un trabajo ya iniciado

Inició un trabajo de dos horas en primer plano y después recordó este problema. No lo finalice para iniciarlo de nuevo.

# press Ctrl-Z to suspend the job first
bg
jobs -l
disown -h %1

Ctrl-Z suspende el trabajo, bg lo reanuda en segundo plano y jobs -l muestra su número de trabajo junto a su PID. disown -h %1 marca ese trabajo para que bash no le envíe SIGHUP. Un disown %1 simple elimina el trabajo por completo de la tabla de bash. Esto tiene el mismo efecto cuando se cierra la sesión, pero entonces jobs ya no lo muestra.

Lo que disown no puede hacer es mover la salida. El proceso sigue teniendo la pty como salida estándar y, cuando la pty desaparece, la siguiente escritura devuelve EIO. Por tanto, disown guarda de forma fiable un trabajo silencioso, como una compilación que escribe en un archivo, pero a menudo pierde uno que genera mucha salida. El trabajo sobrevive sin un lugar donde escribir o termina en la siguiente línea de salida.

Existe una herramienta de recuperación para los descriptores de archivo. reptyr mueve un proceso en ejecución a su terminal actual: instálelo con sudo apt install -y reptyr y después ejecute reptyr <pid> desde una ventana de tmux. Funciona mediante ptrace, y Ubuntu incluye kernel.yama.ptrace_scope = 1, que permite realizar seguimientos sólo de sus propios procesos descendientes, por lo que un proceso heredado necesita sudo reptyr <pid>. Úselo como herramienta de emergencia. No base en ella un procedimiento habitual.

tmux: trabajo que debe seguir ejecutándose aunque se cierre la sesión

tmux (multiplexor de terminal) resuelve el problema en otro punto. En lugar de proteger el proceso frente al pty, proporciona al proceso un pty que no pertenece a la sesión SSH. El servidor tmux se ejecuta fuera de esa sesión y controla los terminales de todo lo que se ejecuta dentro de él. La conexión SSH sólo es un visor conectado a ese servidor. Si la conexión se interrumpe, el servidor no lo detecta.

sudo apt update && sudo apt install -y tmux
tmux new -s import

Inicie el trabajo en esa ventana y pulse Ctrl-b seguido de d para desconectarse de la sesión. Vuelva a iniciar sesión más tarde y retómelo:

tmux ls
tmux attach -t import

tmux ls debería mostrar una línea que empiece por import: 1 windows. Si muestra no server running on /tmp/tmux-1000/default, no existe ninguna sesión a la que conectarse, porque nunca se creó o porque algún proceso terminó el servidor.

screen hace lo mismo con otra combinación de teclas. screen -S import crea una sesión y Ctrl-a seguido de d se desconecta de ella. screen -ls muestra las sesiones existentes y screen -r import recupera una. Cualquiera de las dos herramientas sirve. La combinación para desconectarse es la parte que suele olvidarse.

Un multiplexor también es el entorno adecuado para el trabajo interactivo que debe sobrevivir a una interrupción de la conexión. Por eso ejecutar Claude Code en un VPS dentro de tmux es la configuración habitual y permite controlar una sesión del servidor desde un teléfono mediante una red móvil que se reconecta cada pocos minutos.

systemd-run: delegar el trabajo al PID 1

Si un trabajo no debe depender de usted en absoluto, entréguelo al sistema init.

sudo systemd-run --unit=bigsync --collect /usr/bin/rsync -aH --stats /srv/data/ /mnt/backup/

Esto crea una unidad de servicio transitoria llamada bigsync.service. Tiene su propio cgroup, ningún terminal de control y ninguna relación con su sesión de inicio de sesión. El comando devuelve el control de inmediato y muestra Running as unit: bigsync.service. Supervíselo con cualquiera de estas opciones:

systemctl status bigsync
journalctl -u bigsync -f

--collect indica a systemd que elimine la unidad cuando termine, incluso si falla. Sin esta opción, una unidad transitoria fallida permanece cargada y su nombre sigue ocupado. Por eso, la siguiente ejecución falla con un mensaje que indica que la unidad ya existe. La salida se escribe en el journal, con marcas de tiempo en cada línea. Las entradas del journal sólo sobreviven a un reinicio cuando existe /var/log/journal. Ejecute sudo mkdir -p /var/log/journal y reinicie systemd-journald si necesita conservarlas.

Como usuario normal, ejecutar systemd-run sin sudo solicita autorización a polkit y muestra ==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units ===. Use sudo para las unidades del sistema.

También puede ejecutar el trabajo mediante su propio gestor de usuario:

systemd-run --user --unit=bigsync --collect /usr/bin/rsync -aH /srv/data/ /mnt/backup/

Esto tiene un inconveniente. El gestor por usuario, user@1000.service, normalmente se detiene cuando termina la última sesión y también detiene todas las unidades de usuario. Active el modo lingering una sola vez:

loginctl enable-linger "$USER"
loginctl show-user "$USER" --property=Linger

El segundo comando debe mostrar Linger=yes. Con el modo lingering activado, el gestor de usuario se inicia durante el arranque y sigue ejecutándose aunque no haya iniciado sesión. Sin esta opción, systemd-run --user no le aporta nada frente a nohup.

systemd-run --scope es otra cosa. Ejecuta el comando en primer plano y conectado a su terminal, por lo que por sí solo no sirve para este caso.

Para cualquier trabajo que vaya a ejecutar más de una vez, escriba la unidad en un archivo en lugar de crear una unidad transitoria cada vez.

Una unidad permanente para un trabajo que volverá a ejecutar

User=deploy es el usuario con el que se ejecuta el trabajo. Escriba ahí su propio nombre de inicio de sesión o cree un usuario de servicio para el trabajo:

sudo useradd --system --shell /usr/sbin/nologin deploy
[Unit]
Description=Nightly data sync
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
User=deploy
WorkingDirectory=/srv/data
ExecStart=/usr/local/bin/nightly-sync.sh

Guárdelo como /etc/systemd/system/nightly-sync.service, ejecute sudo systemctl daemon-reload, inícielo con sudo systemctl start nightly-sync y consulte su contenido con journalctl -u nightly-sync. Añada un archivo .timer equivalente cuando deba ejecutarse según una programación en lugar de bajo demanda.

Escribir una unidad de servicio systemd y su temporizador explica en detalle el formato del archivo y la sintaxis de la programación.

Adónde va la salida y por qué desaparece

El orden de las redirecciones importa. > file 2>&1 dirige la salida estándar al archivo y después dirige el error estándar al mismo destino. 2>&1 > file lo hace al revés: el error estándar sigue yendo al terminal, y el terminal es precisamente lo que está a punto de desaparecer. Bash también acepta &> file para ambas secuencias a la vez.

La segunda sorpresa es el almacenamiento en búfer. Cuando la salida estándar es un terminal, la biblioteca C vacía el búfer después de cada línea. Cuando la salida estándar es un archivo, cambia a un búfer por bloques de unos pocos kilobytes, por lo que tail -f ~/import.log no muestra nada durante minutos y el trabajo parece bloqueado. Fuerce el almacenamiento por líneas con stdbuf -oL ./import.sh > ~/import.log 2>&1. Esto funciona con los programas que escriben mediante los flujos estándar de la biblioteca C y no modifican su propio almacenamiento en búfer, pero no con programas como dd o tee. En los demás casos, use la opción propia del programa, como python3 -u o grep --line-buffered.

Evite este patrón:

nohup ./import.sh 2>&1 | tee ~/import.log &

nohup protege import.sh y nada más. tee es un proceso independiente de la misma canalización y también termina al recibir la señal de desconexión. import.sh intenta entonces escribir en una tubería sin lector, por lo que recibe SIGPIPE y se detiene. Introduzca toda la canalización dentro de setsid bash -c '...' o escriba directamente en el archivo y ejecute tail -f sobre él cuando vuelva a conectarse.

Hay otro detalle específico de rsync. --info=progress2 escribe un flujo de retornos de carro que se muestra correctamente en un terminal, pero se convierte en una única línea enorme en el archivo de registro. El journal sólo separa los registros en los saltos de línea, por lo que allí la misma salida llega como una secuencia de fragmentos de 48K (el valor predeterminado de LineMax=). Para una ejecución no interactiva, elimínelo y use --stats en su lugar.

Por qué un trabajo que funciona en su shell falla con systemd o cron

El shell de inicio de sesión de SSH lee /etc/profile y, después, el primero de ~/.bash_profile, ~/.bash_login y ~/.profile que encuentra. Un shell interactivo que no es de inicio de sesión lee ~/.bashrc. Por eso, su shell tiene su PATH, los shims de su gestor de versiones y sus variables exportadas. Una unidad de systemd no lee ninguno de esos archivos. cron tampoco los lee: en Debian y Ubuntu, cron ejecuta los trabajos con SHELL=/bin/sh y PATH=/usr/bin:/bin.

Con systemd, el síntoma suele ser que systemctl status informa de (code=exited, status=203/EXEC). Esto significa que systemd no pudo ejecutar el archivo, porque la ruta era incorrecta o porque el archivo no tiene permiso de ejecución. Con cron, normalmente aparece command not found, que se entrega mediante el correo local, o no se entrega en absoluto si no hay ningún sistema de correo instalado.

Compruebe cuál es el entorno antes de pasar una hora haciendo suposiciones:

sudo systemd-run --collect --wait --unit=envtest /usr/bin/env
journalctl -u envtest --no-pager

Esto muestra el entorno exacto con el que se ejecutará el trabajo. Después, corrija la diferencia. Use rutas absolutas para todo lo que sea suyo, porque systemd resuelve un rsync sin ruta contra una lista fija de rutas del sistema y nunca contra el PATH de su shell. Pase las variables que necesite con -p Environment="KEY=value" en la línea de comandos o con EnvironmentFile=/etc/default/myjob en un archivo de unidad. Si un trabajo realmente necesita su entorno de inicio de sesión, ejecútelo como /bin/bash -lc 'my-command' y acepte que el trabajo dependerá de sus archivos de configuración personales.

Qué puede detener un trabajo desconectado

  • Un reinicio. Nada de tmux persiste después de un reinicio, porque el servidor es un proceso normal y las sesiones son su estado en memoria. Las actualizaciones del kernel implican reinicios, por lo que un trabajo que no pueda reiniciar fácilmente debe pertenecer a una unidad que pueda systemctl enable.
  • El terminador de procesos por falta de memoria. dmesg -T | grep -i 'killed process' muestra esta situación, incluido el nombre del proceso elegido. Una importación grande en un VPS pequeño es un objetivo frecuente.
  • La limpieza de logind. Si /etc/systemd/logind.conf establece KillUserProcesses=yes, los procesos que quedan en el ámbito de una sesión de inicio de sesión se terminan cuando se cierra esa sesión, incluido un servidor tmux iniciado desde ella. Compruebe la configuración actual con loginctl show-session --property=KillUserProcesses. El modo persistente por sí solo no le exime. Inicie tmux fuera del ámbito de la sesión con systemd-run --scope --user tmux new -s import y mantenga activado el modo persistente con loginctl enable-linger "$USER", o pida a un administrador que añada su usuario a KillExcludeUsers=.
  • Un disco lleno. El trabajo se detiene porque el registro que redirigió llenó el sistema de archivos, no porque usted se desconectara. Ejecute df -h antes de atribuirlo a la señal.

Iniciar un trabajo mediante SSH sin mantener la conexión

ssh vps 'sudo systemd-run --unit=import --collect /usr/local/bin/import.sh'

systemd-run devuelve el control en cuanto se inicia la unidad. Por tanto, el comando ssh también termina y el trabajo no queda asociado a la sesión que lo inició. Esta es la opción limpia.

La variante de nohup requiere más cuidado:

ssh vps 'nohup ./import.sh > ~/import.log 2>&1 < /dev/null &'

Sin las redirecciones, parece que el comando se bloquea. sshd mantiene abierto el canal mientras cualquier proceso conserve la salida estándar o el error estándar del comando remoto. Un trabajo ejecutado en segundo plano hereda ambos descriptores. nohup por sí solo no lo corrige, porque nohup sólo redirige la salida cuando esa salida es una terminal. En este caso, es una tubería hacia el cliente. Añadir < /dev/null también cierra la entrada estándar. ssh -n realiza la misma función desde el extremo del cliente.

FAQ

¿Por qué se detiene mi comando cuando se interrumpe la conexión SSH?

El pty (pseudo-terminal) que usaba la sesión se destruye y el kernel envía SIGHUP al grupo de procesos en primer plano de ese terminal. La acción predeterminada para SIGHUP es finalizar el proceso. Los trabajos en segundo plano también terminan porque bash reenvía SIGHUP a todos los trabajos de su tabla antes de salir. Un comando que ignore SIGHUP, como uno iniciado con nohup, o uno que nunca haya compartido la sesión, como una unidad de systemd, no se ve afectado.

¿Es mejor tmux o systemd-run para un rsync de seis horas?

systemd-run. Una sesión de tmux depende de un proceso de servidor que usted inició, por lo que termina en el siguiente reinicio y es invisible para quien no sepa ejecutar tmux ls. Ejecutar sudo systemd-run --unit=bigsync --collect /usr/bin/rsync -aH --stats /srv/data/ /mnt/backup/ proporciona systemctl status bigsync para el estado y journalctl -u bigsync para la salida. El siguiente administrador puede encontrar ambos sin que nadie se lo indique. Use tmux para trabajos en los que necesite observar la pantalla y escribir en ella.

¿Cómo puedo ver la salida de un trabajo cuya redirección olvidé?

Normalmente no puede, porque esa salida se envió a un terminal que ya no existe. Mientras el proceso siga ejecutándose, puede inspeccionar sus archivos abiertos con sudo ls -l /proc/<pid>/fd o observar sus llamadas al sistema con sudo strace -p <pid>, pero el texto que ya se escribió se ha perdido. reptyr <pid> puede mover el proceso a un terminal nuevo, y kernel.yama.ptrace_scope = 1 de Ubuntu significa que necesita sudo para un proceso que no es hijo suyo. El hábito que evita todo esto consiste en redirigir la salida a un archivo desde el principio y tail -f ese archivo.

¿Sobrevive un reinicio una sesión de tmux separada?

No. El servidor de tmux es un proceso normal y las sesiones son su estado en memoria, por lo que un reinicio termina ambos. También termina cuando /etc/systemd/logind.conf establece KillUserProcesses=yes y usted cierra la sesión que lo inició. El lingering por sí solo no lo evita: inicie tmux fuera del ámbito de la sesión con systemd-run --scope --user tmux new -s import mientras el lingering esté activo, o incluya a su usuario en KillExcludeUsers=. Para trabajos que deban reanudarse automáticamente después de un reinicio, escriba una unidad de systemd y systemctl enable.

¿Por qué mi script funciona en el shell, pero falla como unidad de systemd?

Una unidad no lee /etc/profile ni ~/.bashrc, por lo que no dispone de sus adiciones de PATH ni de sus variables exportadas. Que systemctl status muestre (code=exited, status=203/EXEC) significa que systemd no pudo ejecutar el archivo en absoluto. Use una ruta absoluta y compruebe el bit de ejecución. Ejecute sudo systemd-run --collect --wait --unit=envtest /usr/bin/env, vuelva a leerlo con journalctl -u envtest y tendrá el entorno exacto que recibe el trabajo. Proporcione lo que falte con Environment= o EnvironmentFile=.

#ssh#tmux#nohup#systemd#long-running-jobs