Garder une commande active après une déconnexion SSH
Après l’erreur SSH, SIGHUP peut arrêter votre commande. Comparez nohup, disown, tmux et systemd-run pour choisir la méthode adaptée à votre tâche.
Pourquoi votre commande s’arrête lorsque la connexion SSH est interrompue
Pour qu’une commande continue de s’exécuter après une déconnexion SSH, elle doit se retrouver dans un environnement que le signal de déconnexion ne peut pas atteindre. Chaque méthode ci-dessous organise cela d’une manière différente. Commencez donc par comprendre le mécanisme.
Votre session s’exécute sur un pty (pseudo-terminal), un périphérique de terminal virtuel que sshd crée sur le serveur pour votre session. Il s’agit du terminal de contrôle de votre shell et de chaque commande lancée depuis ce shell. Pour comprendre le reste du fonctionnement, consultez ce que SSH configure lors de la connexion. Lorsque la connexion TCP est interrompue, sshd ferme son extrémité et le pty est détruit. Le noyau considère cela comme une fermeture du terminal. Il envoie donc SIGHUP au groupe de processus au premier plan de ce terminal ainsi qu’au leader de session, qui est votre shell. L’action par défaut de SIGHUP consiste à terminer le processus. Votre commande appartenait au groupe de processus au premier plan. Elle s’arrête donc.
Les tâches en arrière-plan ne sont pas protégées pour autant. Une tâche lancée avec & se trouve dans son propre groupe de processus. Le noyau ne lui envoie donc pas directement le signal. C’est Bash qui s’en charge. Lorsqu’un Bash interactif reçoit SIGHUP, il renvoie SIGHUP à chaque tâche de sa table avant de quitter. De votre point de vue, le résultat est identique : la tâche a disparu et le fichier journal s’arrête au milieu d’une ligne.
Cette asymétrie est source de confusion. La saisie de exit ne ferme pas vos tâches en arrière-plan, car Bash ne le fait que lorsque l’option huponexit est activée, ce qui n’est pas le cas par défaut. Une connexion interrompue les ferme, en revanche. Une tâche qui a survécu à la fermeture volontaire du terminal peut donc quand même s’arrêter lorsque le wifi se déconnecte.
Deux conséquences en découlent, et elles constituent l’essentiel du sujet. Un processus qui ignore SIGHUP, ou qui n’a aucun terminal de contrôle, ne sera pas interrompu par la fermeture du terminal. En revanche, un processus dont la sortie standard pointe encore vers le pty détruit n’a plus d’endroit où écrire : l’écriture échoue avec EIO (erreur d’entrée/sortie), et la plupart des programmes s’arrêtent à ce moment-là. Vous devez résoudre ces deux problèmes. De nombreuses recettes ne traitent que le premier. C’est pourquoi certains indiquent que « nohup n’a pas fonctionné ».
Si votre connexion est interrompue plusieurs fois par jour, corrigez également ce problème. ServerAliveInterval 60 dans ~/.ssh/config empêche une session inactive d’être supprimée à cause de l’expiration du délai d’un NAT (network address translation) quelque part sur le chemin. Une session qui ne s’ouvre jamais correspond à un autre problème, avec d’autres causes. C’est dans ce cas que la différence entre une connexion refusée et une connexion dont le délai d’attente est dépassé est utile.
Quelle méthode permet de maintenir une commande en cours après une déconnexion SSH ?
Quatre réponses, classées selon l’importance de la tâche.
nohupousetsid: une tâche ponctuelle que vous lancez maintenant et dont vous consulterez le journal ensuite. Vous redirigez vous-même la sortie.disown: une tâche déjà lancée que vous avez oublié de protéger. Il récupère le processus. Il ne peut pas restituer sa sortie.tmuxouscreen: une tâche que vous devez surveiller, interrompre et reprendre pendant plusieurs jours.systemd-runou un véritable fichier d’unité : tout ce qui doit continuer après la fermeture de votre session, comme unrsyncde six heures ou un import de base de données effectué pendant la nuit.
La règle à retenir : si oublier la tâche pose problème, elle doit être gérée par systemd, pas par tmux. Une fenêtre tmux dépend de la mémoire d’un utilisateur. Une unité possède un nom, un état, un journal et une stratégie de redémarrage que la personne suivante peut retrouver sans qu’on ait besoin de lui expliquer.
nohup et setsid : lancer le processus puis se déconnecter
nohup ./import.sh > ~/import.log 2>&1 &
echo $! > ~/import.pidnohup configure la disposition de SIGHUP pour ignorer le signal, puis exécute votre commande. Ainsi, lorsque le noyau envoie le signal de déconnexion, celui-ci n’a aucun effet. C’est à vous d’écrire la redirection. Si la sortie standard reste dirigée vers le terminal, nohup la redirige automatiquement vers nohup.out dans le répertoire courant, puis utilise $HOME/nohup.out en dernier recours, et affiche :
nohup: ignoring input and appending output to 'nohup.out'Il est facile de perdre la trace de ce fichier. Donnez-lui donc vous-même un nom. $! contient le PID (identifiant du processus) du dernier job exécuté en arrière-plan. Enregistrez cette valeur pour pouvoir vérifier l’état du job après vous être reconnecté.
setsid traite le même problème sous un autre angle. Il exécute la commande dans une nouvelle session sans terminal de contrôle. Aucun terminal ne peut donc provoquer sa déconnexion.
setsid --fork ./import.sh > ~/import.log 2>&1Utilisez --fork. Sans cette option, setsid appelle setsid() sur place lorsque le processus n’est pas déjà un process group leader. C’est ce qui se produit dans un shell script. Votre script reste alors bloqué. Avec --fork, le comportement est identique dans un script et à l’invite du shell.
Vérifiez ce qui s’est réellement produit :
ps -o pid,ppid,sid,tty,stat,cmd -p "$(cat ~/import.pid)"Une colonne TTY de ? signifie que le processus n’a pas de terminal de contrôle. Rien ne peut donc provoquer sa déconnexion. Avec nohup, la colonne TTY affiche encore quelque chose comme pts/0 tant que vous restez connecté, puis devient ? lorsque le pty est détruit. Les deux résultats sont corrects. Le job a survécu.
disown : récupérer un job déjà démarré
Vous avez lancé un job de deux heures au premier plan, puis vous vous êtes souvenu de ce problème. Ne le tuez pas pour le relancer.
# press Ctrl-Z to suspend the job first
bg
jobs -l
disown -h %1Ctrl-Z suspend le job, bg le reprend en arrière-plan et jobs -l affiche son numéro de job à côté de son PID. disown -h %1 marque ce job afin que bash ne lui envoie pas SIGHUP. La commande simple disown %1 supprime complètement le job de la table de bash. Elle a le même effet lors de la déconnexion, mais jobs ne l’affiche alors plus.
disown ne peut pas déplacer la sortie. Le processus utilise toujours le pty comme sortie standard. Lorsque le pty disparaît, l’écriture suivante renvoie EIO. disown permet donc de récupérer de manière fiable un job silencieux, par exemple une compilation qui écrit dans un fichier, mais échoue souvent avec un job très bavard. Le job reste alors en cours sans pouvoir afficher sa sortie, ou s’arrête à la prochaine ligne produite.
Il existe un outil de récupération pour les descripteurs de fichiers. reptyr rattache un processus en cours à votre terminal actuel : installez-le avec sudo apt install -y reptyr, puis exécutez reptyr <pid> depuis une fenêtre tmux. Il fonctionne via ptrace. Ubuntu fournit kernel.yama.ptrace_scope = 1, qui autorise le traçage uniquement de vos propres processus descendants. Un processus dont vous avez hérité nécessite donc sudo reptyr <pid>. Considérez cet outil comme une solution d’urgence. N’en faites pas une procédure courante.
tmux : travail à surveiller et à reprendre
tmux (multiplexeur de terminal) résout le problème à un autre niveau. Au lieu de protéger votre processus contre le pty, il lui fournit un pty qui n’appartient pas à votre session SSH. Le serveur tmux s’exécute en dehors de cette session et possède les terminaux de tout ce qu’elle contient. Votre connexion SSH n’est qu’un affichage qui y est attaché. Si la connexion est interrompue, le serveur ne le remarque pas.
sudo apt update && sudo apt install -y tmux
tmux new -s importLancez le travail dans cette fenêtre, puis appuyez sur Ctrl-b suivi de d pour vous en détacher. Reconnectez-vous plus tard et reprenez-le :
tmux ls
tmux attach -t importtmux ls doit afficher une ligne commençant par import: 1 windows. S’il affiche no server running on /tmp/tmux-1000/default, aucune session n’est disponible, soit parce qu’elle n’a jamais été créée, soit parce qu’un processus a arrêté le serveur.
screen effectue la même opération avec une autre combinaison de touches. screen -S import crée une session, puis Ctrl-a suivi de d vous en détache. screen -ls liste les sessions existantes et screen -r import permet d’en reprendre une. Les deux outils conviennent ici. La combinaison de touches utilisée pour se détacher est la partie que l’on oublie le plus souvent.
Un multiplexeur est également l’environnement adapté au travail interactif qui doit survivre à une coupure de connexion. C’est pourquoi exécuter Claude Code sur un VPS dans tmux est la configuration habituelle. C’est aussi ce qui permet d’administrer une session serveur depuis un téléphone sur un réseau mobile qui se reconnecte toutes les quelques minutes.
systemd-run : confier le travail au PID 1
Pour un travail qui ne doit absolument pas dépendre de vous, confiez-le au système d’initialisation.
sudo systemd-run --unit=bigsync --collect /usr/bin/rsync -aH --stats /srv/data/ /mnt/backup/Cette commande crée une unité de service temporaire nommée bigsync.service. Elle possède son propre cgroup, aucun terminal de contrôle et aucun lien avec votre session. La commande rend la main immédiatement et affiche Running as unit: bigsync.service. Surveillez-la avec l’une de ces commandes :
systemctl status bigsync
journalctl -u bigsync -f--collect indique à systemd de supprimer l’unité une fois terminée, même si elle a échoué. Sans cette option, une unité temporaire ayant échoué reste chargée et son nom reste occupé. L’exécution suivante échoue alors avec un message indiquant que l’unité existe déjà. La sortie est envoyée au journal, avec un horodatage sur chaque ligne. Les entrées du journal ne persistent après un redémarrage que si /var/log/journal existe. Exécutez donc sudo mkdir -p /var/log/journal, puis redémarrez systemd-journald si vous voulez conserver ces entrées.
En tant qu’utilisateur normal, appeler systemd-run sans sudo demande une autorisation à polkit et affiche ==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units ===. Utilisez sudo pour les unités système.
Vous pouvez également exécuter le travail avec votre propre gestionnaire utilisateur :
systemd-run --user --unit=bigsync --collect /usr/bin/rsync -aH /srv/data/ /mnt/backup/Cette méthode comporte un piège. Votre gestionnaire par utilisateur, user@1000.service, s’arrête normalement lorsque votre dernière session se termine, et il arrête toutes les unités utilisateur avec lui. Activez la persistance une seule fois :
loginctl enable-linger "$USER"
loginctl show-user "$USER" --property=LingerLa deuxième commande doit afficher Linger=yes. Lorsque la persistance est activée, votre gestionnaire utilisateur démarre au boot et continue de fonctionner, que vous soyez connecté ou non. Sans cette option, systemd-run --user ne vous apporte rien de plus que nohup.
systemd-run --scope est différent. Cette commande exécute le programme au premier plan, attaché à votre terminal. Elle ne convient donc pas ici.
Pour tout travail que vous exécuterez plus d’une fois, écrivez l’unité dans un fichier au lieu de créer une unité temporaire à chaque fois.
Une unité permanente pour un travail que vous exécuterez de nouveau
[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.shEnregistrez ce contenu dans /etc/systemd/system/nightly-sync.service, exécutez sudo systemctl daemon-reload, puis démarrez l’unité avec sudo systemctl start nightly-sync et affichez-la avec journalctl -u nightly-sync. Ajoutez un fichier .timer correspondant lorsque le travail doit s’exécuter selon un calendrier plutôt qu’à la demande.
Écrire une unité de service systemd et son timer présente en détail le format des fichiers et la syntaxe des calendriers.
Où va la sortie, et pourquoi elle disparaît
L’ordre des redirections est important. > file 2>&1 redirige la sortie standard vers le fichier, puis redirige la sortie d’erreur standard au même endroit. 2>&1 > file fait l’inverse : la sortie d’erreur standard continue d’aller vers le terminal, qui est précisément ce qui va disparaître. Bash accepte aussi &> file pour les deux flux à la fois.
La mise en tampon constitue une autre source de surprise. Lorsque la sortie standard est un terminal, la bibliothèque C vide le tampon à chaque ligne. Lorsqu’elle est un fichier, elle utilise un tampon par blocs de quelques kilo-octets. Ainsi, tail -f ~/import.log n’affiche rien pendant plusieurs minutes et le processus semble bloqué. Forcez la mise en tampon par ligne avec stdbuf -oL ./import.sh > ~/import.log 2>&1, ou utilisez l’option du programme, comme python3 -u ou grep --line-buffered.
Évitez ce schéma :
nohup ./import.sh 2>&1 | tee ~/import.log &nohup protège import.sh, et rien d’autre. tee est un processus distinct de la même pipeline et il se termine lui aussi lors de la déconnexion. import.sh écrit alors dans un tube sans lecteur, reçoit SIGPIPE, puis s’arrête. Placez toute la pipeline dans setsid bash -c '...', ou écrivez directement dans le fichier et exécutez tail -f dessus lorsque vous vous reconnectez.
Un dernier point concerne spécifiquement rsync. --info=progress2 écrit un flux de retours chariot qui s’affiche correctement sur un terminal, mais qui devient une seule ligne gigantesque dans un fichier journal ou dans le journal systemd. Pour une exécution non interactive, supprimez-le et utilisez plutôt --stats.
Pourquoi une tâche qui fonctionne dans votre shell échoue avec systemd ou cron
Votre shell interactif lit /etc/profile, ~/.profile et ~/.bashrc. Il dispose donc de votre PATH, des shims de votre gestionnaire de versions et de vos variables exportées. Une unité systemd ne lit aucun de ces fichiers. cron non plus : sous Debian et Ubuntu, cron exécute les tâches avec SHELL=/bin/sh et PATH=/usr/bin:/bin.
Avec systemd, le symptôme est généralement systemctl status qui signale (code=exited, status=203/EXEC). Cela signifie que systemd n’a pas pu exécuter le fichier, parce que le chemin est incorrect ou que le fichier n’est pas marqué comme exécutable. Avec cron, il s’agit généralement de command not found, envoyé par la messagerie locale, ou envoyé nulle part lorsqu’aucun système de messagerie n’est installé.
Vérifiez l’environnement réel avant de passer une heure à faire des suppositions :
sudo systemd-run --collect --wait --unit=envtest /usr/bin/env
journalctl -u envtest --no-pagerCette commande affiche l’environnement exact dans lequel votre tâche sera exécutée. Corrigez ensuite l’écart. Utilisez des chemins absolus pour vos propres fichiers et commandes, car systemd résout un rsync sans chemin dans une liste fixe de chemins système, jamais dans le PATH de votre shell. Transmettez les variables nécessaires avec -p Environment="KEY=value" sur la ligne de commande, ou avec EnvironmentFile=/etc/default/myjob dans un fichier d’unité. Lorsqu’une tâche a réellement besoin de votre environnement de connexion, exécutez-la avec /bin/bash -lc 'my-command' et acceptez qu’elle dépende alors de vos fichiers de configuration utilisateur.
Ce qui peut encore arrêter un job détaché
- Un redémarrage. Rien dans tmux ne persiste après un redémarrage, car le serveur est un processus ordinaire et les sessions sont stockées en mémoire. Les mises à jour du kernel nécessitent des redémarrages. Un job que vous ne pouvez pas relancer facilement doit donc appartenir à une unité que vous pouvez
systemctl enable. - Le tueur de processus lorsqu’il n’y a plus assez de mémoire.
dmesg -T | grep -i 'killed process'l’indique, avec notamment le nom du processus choisi. Un import volumineux sur un petit VPS est souvent concerné. - Le nettoyage effectué par logind. Si
/etc/systemd/logind.confdéfinitKillUserProcesses=yes, vos processus restants sont tués lorsque votre dernière session se termine, y compris le serveur tmux. Vérifiez le paramètre actuel avecloginctl show --property=KillUserProcesses, et excluez votre utilisateur avecloginctl enable-linger "$USER". - Un disque plein. Le job s’arrête parce que le fichier journal vers lequel vous avez redirigé la sortie a rempli le système de fichiers, pas parce que vous êtes parti. Exécutez
df -havant d’accuser le signal.
Lancer une tâche via SSH sans rester connecté
ssh vps 'sudo systemd-run --unit=import --collect /usr/local/bin/import.sh'systemd-run revient dès que l’unité a démarré. La commande ssh revient donc elle aussi, et la tâche n’est plus liée à la session qui l’a lancée. C’est la méthode propre.
La variante nohup demande davantage d’attention :
ssh vps 'nohup ./import.sh > ~/import.log 2>&1 < /dev/null &'Sans les redirections, la commande semble rester bloquée. sshd maintient le canal ouvert tant qu’un processus utilise encore la sortie standard ou l’erreur standard de la commande distante. Une tâche exécutée en arrière-plan hérite des deux. nohup ne suffit pas, car nohup ne redirige la sortie que lorsque celle-ci est un terminal. Ici, il s’agit d’un pipe vers votre client. Ajouter < /dev/null ferme également l’entrée standard. ssh -n produit le même effet côté client.
FAQ
Pourquoi ma commande s’arrête-t-elle lorsque la connexion SSH est interrompue ?
Le pty (pseudo-terminal) utilisé par votre session est détruit, et le kernel envoie SIGHUP au groupe de processus au premier plan sur ce terminal. L’action par défaut de SIGHUP consiste à arrêter le processus. Les tâches en arrière-plan s’arrêtent également, car bash renvoie SIGHUP à chaque tâche de sa table avant de quitter. Une commande qui ignore SIGHUP, par exemple une commande lancée avec nohup, ou une commande qui n’a jamais partagé votre session, comme une unité systemd, n’est pas affectée.
Pour un rsync de six heures, vaut-il mieux utiliser tmux ou systemd-run ?
systemd-run. Une session tmux dépend d’un processus serveur que vous avez démarré. Elle se termine donc au prochain redémarrage et reste invisible pour toute personne qui ne sait pas exécuter tmux ls. L’exécution de sudo systemd-run --unit=bigsync --collect /usr/bin/rsync -aH --stats /srv/data/ /mnt/backup/ vous fournit systemctl status bigsync pour l’état et journalctl -u bigsync pour la sortie. Le prochain administrateur les trouve ainsi sans explication supplémentaire. Utilisez tmux lorsque vous devez surveiller l’écran et saisir des commandes.
Comment afficher la sortie d’une tâche dont j’ai oublié de rediriger la sortie ?
En général, c’est impossible, car cette sortie a été envoyée vers un terminal qui n’existe plus. Tant que le processus s’exécute, vous pouvez examiner ses fichiers ouverts avec sudo ls -l /proc/<pid>/fd ou surveiller ses appels système avec sudo strace -p <pid>, mais le texte déjà écrit est perdu. reptyr <pid> peut déplacer le processus vers un nouveau terminal, et, sur Ubuntu, kernel.yama.ptrace_scope = 1 signifie qu’il faut sudo pour un processus qui n’est pas votre propre processus enfant. Pour éviter tous ces problèmes, redirigez la sortie vers un fichier dès le lancement, puis tail -f ce fichier.
Une session tmux détachée survit-elle à un redémarrage ?
Non. Le serveur tmux est un processus ordinaire, et les sessions sont conservées en mémoire par ce processus. Un redémarrage met donc fin aux deux. Le serveur s’arrête également lorsque /etc/systemd/logind.conf définit KillUserProcesses=yes et que vous vous déconnectez de votre dernière session, ce que loginctl enable-linger "$USER" empêche. Pour une tâche qui doit reprendre automatiquement après un redémarrage, écrivez une unité systemd et systemctl enable-la.
Pourquoi mon script fonctionne-t-il dans le shell, mais échoue-t-il comme unité systemd ?
Une unité ne lit pas /etc/profile ni ~/.bashrc. Elle ne dispose donc ni de vos ajouts à PATH ni de vos variables exportées. Si systemctl status affiche (code=exited, status=203/EXEC), cela signifie que systemd n’a pas pu exécuter le fichier du tout. Utilisez donc un chemin absolu et vérifiez le bit d’exécution. Exécutez sudo systemd-run --collect --wait --unit=envtest /usr/bin/env, relisez le résultat avec journalctl -u envtest et vous obtiendrez l’environnement exact de votre tâche. Fournissez les éléments manquants avec Environment= ou EnvironmentFile=.