Befehl nach SSH-Trennung weiterlaufen lassen
Bei SSH-Abbruch beendet SIGHUP Ihren Prozess. Vergleichen Sie nohup, disown, tmux und systemd-run, und wählen Sie die passende Methode für Ihren Job.
Warum Ihr Befehl beendet wird, wenn die SSH-Verbindung getrennt wird
Damit ein Befehl nach einer getrennten SSH-Verbindung weiterläuft, muss er sich an einem Ort befinden, den das Hangup-Signal nicht erreichen kann. Jede der folgenden Methoden setzt dies anders um. Beginnen wir daher mit dem Mechanismus.
Ihre Anmeldung läuft auf einem pty (Pseudo-Terminal), einem virtuellen Terminalgerät, das sshd auf dem Server für Ihre Sitzung erstellt. Es ist das steuernde Terminal Ihrer Shell und jedes Befehls, den Sie aus dieser Shell starten. Wenn Sie den weiteren Ablauf nachvollziehen möchten, erklärt was SSH beim Anmelden einrichtet die Details. Wenn die TCP-Verbindung abbricht, schließt sshd seine Seite, und das pty wird zerstört. Der Kernel behandelt dies als Trennung des Terminals. Deshalb sendet er SIGHUP an die Vordergrund-Prozessgruppe dieses Terminals und an den Sitzungsleiter, also Ihre Shell. Die Standardaktion für SIGHUP besteht darin, den Prozess zu beenden. Ihr Befehl gehörte zur Vordergrund-Prozessgruppe. Daher wird Ihr Befehl beendet.
Auch Hintergrundjobs sind nicht automatisch sicher. Ein mit & gestarteter Job befindet sich in einer eigenen Prozessgruppe. Der Kernel signalisiert ihn daher nicht direkt. Das übernimmt Bash. Beim Empfang von SIGHUP sendet eine interaktive Bash-Shell SIGHUP erneut an jeden Job in ihrer Jobtabelle, bevor sie beendet wird. Das Ergebnis ist aus Ihrer Sicht identisch: Der Job ist verschwunden, und die Logdatei endet mitten in einer Zeile.
Hier gibt es eine Asymmetrie, die häufig zu Verwirrung führt. Die Eingabe von exit trennt Ihre Hintergrundjobs nicht, weil Bash dies nur ausführt, wenn die Option huponexit gesetzt ist. Standardmäßig ist sie deaktiviert. Eine abgebrochene Verbindung beendet die Jobs dagegen. Der Job, der das manuelle Schließen des Terminals ordnungsgemäß überlebt hat, kann dennoch beendet werden, wenn die WLAN-Verbindung abbricht.
Daraus ergeben sich zwei Konsequenzen, die den gesamten Sachverhalt erklären. Ein Prozess, der SIGHUP ignoriert oder überhaupt kein steuerndes Terminal besitzt, wird nicht durch ein Hangup beendet. Wenn seine Standardausgabe weiterhin auf das zerstörte pty zeigt, kann der Prozess jedoch nirgendwohin schreiben. Der Schreibvorgang schlägt mit EIO (Ein-/Ausgabefehler) fehl, und die meisten Programme werden an diesem Punkt beendet. Sie müssen daher beide Probleme lösen. Viele Anleitungen lösen nur das erste. Deshalb berichten manche Benutzer, dass „nohup nicht funktioniert hat“.
Wenn Ihre Verbindung mehrmals täglich abbricht, sollten Sie auch dieses Problem beheben. ServerAliveInterval 60 in ~/.ssh/config verhindert, dass eine inaktive Sitzung aufgrund eines NAT-Timeouts (Network Address Translation) irgendwo auf dem Übertragungsweg verworfen wird. Eine Sitzung, die überhaupt nicht geöffnet werden kann, weist auf ein anderes Problem mit anderen Ursachen hin. In diesem Fall ist der Unterschied zwischen „connection refused“ und „connection timed out“ relevant.
Welche Methode hält einen Befehl nach der Trennung der SSH-Verbindung am Laufen?
Vier Antworten, geordnet nach der Kritikalität des Jobs.
nohupodersetsid: ein einmaliger Vorgang, den Sie jetzt starten und dessen Log Sie anschließend lesen. Die Ausgabe leiten Sie selbst um.disown: der Job, den Sie bereits gestartet und nicht geschützt haben. Damit retten Sie den Prozess. Die Ausgabe lässt sich damit nicht nachträglich wiederherstellen.tmuxoderscreen: Arbeiten, die Sie über mehrere Tage beobachten, unterbrechen und später fortsetzen müssen.systemd-runoder eine echte Unit-Datei: alles, was Ihre Anmeldung überdauern muss, beispielsweise ein sechsstündigerrsyncoder ein Datenbankimport über Nacht.
Die wichtigste Regel lautet: Wenn es problematisch wäre, den Job zu vergessen, gehört er zu systemd und nicht zu tmux. Ein tmux-Fenster ist etwas, an das sich ein Mensch erinnern muss. Eine Unit hat einen Namen, einen Status, ein Log und eine Neustartregel, die die nächste Person ohne zusätzliche Hinweise finden kann.
nohup und setsid: Starten und die Sitzung verlassen
nohup ./import.sh > ~/import.log 2>&1 &
echo $! > ~/import.pidnohup setzt die Behandlung von SIGHUP auf Ignorieren und führt anschließend Ihren Befehl aus. Dadurch erreicht das Hangup-Signal des Kernels den Prozess, ohne eine Wirkung zu haben. Die Umleitung müssen Sie selbst angeben. Wenn die Standardausgabe weiterhin auf das Terminal zeigt, leitet nohup sie automatisch nach nohup.out im aktuellen Verzeichnis um. Falls das nicht möglich ist, verwendet es $HOME/nohup.out und gibt Folgendes aus:
nohup: ignoring input and appending output to 'nohup.out'Diese Datei ist leicht aus den Augen zu verlieren. Geben Sie ihr deshalb selbst einen Namen. $! enthält die PID (Prozess-ID) des letzten Hintergrundprozesses. Wenn Sie sie speichern, können Sie den Prozess nach der nächsten Anmeldung überprüfen.
setsid löst dasselbe Problem von der anderen Seite. Der Befehl wird in einer neuen Sitzung ohne steuerndes Terminal ausgeführt. Es gibt dann kein Terminal, das den Prozess beenden könnte.
setsid --fork ./import.sh > ~/import.log 2>&1Verwenden Sie --fork. Ohne diese Option ruft setsid setsid() direkt auf, sobald der Prozess noch kein Prozessgruppenleiter ist. Das ist in einem Shell-Skript der Fall. Ihr Skript bleibt dann blockiert. Mit --fork verhält sich der Befehl in einem Skript und an der Eingabeaufforderung gleich.
Prüfen Sie, was tatsächlich gestartet wurde:
ps -o pid,ppid,sid,tty,stat,cmd -p "$(cat ~/import.pid)"Ein TTY-Eintrag von ? bedeutet, dass der Prozess kein steuerndes Terminal besitzt. Nichts kann ihn daher beenden. Unter nohup zeigt die Spalte TTY weiterhin einen Wert wie pts/0, solange Sie verbunden bleiben. Nach dem Zerstören des PTY wird dort ? angezeigt. Beide Ergebnisse sind korrekt. Der Prozess wurde nicht beendet.
disown: Einen bereits gestarteten Job retten
Sie haben einen zweistündigen Job im Vordergrund gestartet und sich dann an dieses Problem erinnert. Beenden Sie ihn nicht, um ihn anschließend erneut zu starten.
# press Ctrl-Z to suspend the job first
bg
jobs -l
disown -h %1Ctrl-Z hält den Job an, bg setzt ihn im Hintergrund fort, und jobs -l gibt seine Jobnummer neben seiner PID aus. disown -h %1 markiert diesen Job, damit bash ihm SIGHUP nicht sendet. Das einfache disown %1 entfernt den Job vollständig aus der Jobtabelle von bash. Beim Hangup hat das denselben Effekt. jobs führt den Job dann jedoch nicht mehr auf.
disown kann die Ausgabe nicht umleiten. Der Prozess verwendet das pty weiterhin als Standardausgabe. Wenn das pty verschwindet, liefert der nächste Schreibvorgang EIO zurück. Deshalb rettet disown zuverlässig einen stillen Job, beispielsweise eine Kompilierung, die in eine Datei schreibt. Bei einem ausgabeintensiven Job scheitert es dagegen häufig. Der Job läuft entweder ohne Ausgabemöglichkeit weiter oder beendet sich bei der nächsten Ausgabezeile.
Für die Dateideskriptoren gibt es ein Rettungswerkzeug. reptyr verschiebt einen laufenden Prozess auf Ihr aktuelles Terminal. Installieren Sie es mit sudo apt install -y reptyr und führen Sie anschließend reptyr <pid> in einem tmux-Fenster aus. Es arbeitet über ptrace. Ubuntu liefert kernel.yama.ptrace_scope = 1 mit, das die Ablaufverfolgung auf Ihre eigenen Nachkommen beschränkt. Für einen geerbten Prozess benötigen Sie daher sudo reptyr <pid>. Verwenden Sie es nur als Notfallwerkzeug. Bauen Sie keinen regulären Ablauf darauf auf.
tmux: Arbeit, die Sie überwachen und später fortsetzen müssen
tmux (Terminal-Multiplexer) löst das Problem an einer anderen Stelle. Statt Ihren Prozess vor dem pty zu schützen, stellt tmux ihm ein pty bereit, das nicht zu Ihrer SSH-Sitzung gehört. Der tmux-Server läuft außerhalb dieser Sitzung und verwaltet die Terminals aller darin ausgeführten Prozesse. Ihre SSH-Verbindung ist lediglich ein daran angeschlossenes Anzeigefenster. Wird die Verbindung getrennt, bemerkt der Server dies nicht.
sudo apt update && sudo apt install -y tmux
tmux new -s importStarten Sie den Auftrag in diesem Fenster. Drücken Sie anschließend Ctrl-b und danach d, um die Verbindung zu trennen. Melden Sie sich später erneut an und setzen Sie die Arbeit fort:
tmux ls
tmux attach -t importtmux ls sollte eine Zeile ausgeben, die mit import: 1 windows beginnt. Wenn no server running on /tmp/tmux-1000/default ausgegeben wird, gibt es keine Sitzung, an die Sie sich anhängen können. Entweder wurde sie nie erstellt oder ein Prozess hat den Server beendet.
screen erledigt dieselbe Aufgabe mit einer anderen Tastenkombination. screen -S import erstellt eine Sitzung, und Ctrl-a trennt sie anschließend mit d. screen -ls listet vorhandene Sitzungen auf, und screen -r import stellt eine davon wieder her. Beide Werkzeuge sind hierfür geeignet. Die Tastenkombination zum Trennen wird häufig vergessen.
Ein Multiplexer ist auch der richtige Ort für interaktive Arbeiten, die einen Verbindungsabbruch überstehen müssen. Deshalb ist Claude Code auf einem VPS innerhalb von tmux auszuführen die übliche Einrichtung. Dadurch lässt sich außerdem eine Serversitzung von einem Telefon aus zu steuern in einem Mobilfunknetz verwenden, das sich alle paar Minuten neu verbindet.
systemd-run: Den Auftrag an PID 1 übergeben
Wenn ein Auftrag vollständig unabhängig von Ihnen laufen muss, übergeben Sie ihn dem Init-System.
sudo systemd-run --unit=bigsync --collect /usr/bin/rsync -aH --stats /srv/data/ /mnt/backup/Dadurch wird eine transiente Service-Unit mit dem Namen bigsync.service erstellt. Sie erhält eine eigene cgroup, kein steuerndes Terminal und keine Verbindung zu Ihrer Anmeldung. Der Befehl kehrt sofort zurück und gibt Running as unit: bigsync.service aus. Überwachen Sie den Auftrag mit einem der folgenden Befehle:
systemctl status bigsync
journalctl -u bigsync -f--collect weist systemd an, die Unit nach ihrem Ende zu entfernen, auch wenn sie fehlgeschlagen ist. Ohne diese Option bleibt eine fehlgeschlagene transiente Unit geladen und ihr Name bleibt belegt. Der nächste Start schlägt dann mit der Meldung fehl, dass die Unit bereits existiert. Die Ausgabe wird mit Zeitstempeln in jeder Zeile in das Journal geschrieben. Journal-Einträge überdauern einen Reboot nur, wenn /var/log/journal vorhanden ist. Führen Sie in diesem Fall sudo mkdir -p /var/log/journal aus und starten Sie systemd-journald neu.
Wenn Sie systemd-run als normaler Benutzer ohne sudo aufrufen, fordert polkit eine Autorisierung an und gibt ==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units === aus. Verwenden Sie sudo für System-Units.
Sie können den Auftrag auch unter Ihrem eigenen Benutzer-Manager ausführen:
systemd-run --user --unit=bigsync --collect /usr/bin/rsync -aH /srv/data/ /mnt/backup/Dabei gibt es eine wichtige Einschränkung. Ihr benutzerspezifischer Manager, user@1000.service, wird normalerweise beendet, sobald Ihre letzte Sitzung endet. Dadurch werden auch alle Benutzer-Units beendet. Aktivieren Sie Lingering einmalig:
loginctl enable-linger "$USER"
loginctl show-user "$USER" --property=LingerDer zweite Befehl sollte Linger=yes ausgeben. Bei aktiviertem Lingering startet Ihr Benutzer-Manager beim Booten und läuft unabhängig davon weiter, ob Sie angemeldet sind. Ohne diese Einstellung bietet systemd-run --user gegenüber nohup keinen Vorteil.
systemd-run --scope ist etwas anderes. Der Befehl läuft im Vordergrund und ist an Ihr Terminal gebunden. Daher ist er für diesen Zweck nicht geeignet.
Wenn Sie einen Auftrag mehr als einmal ausführen, legen Sie die Unit als Datei an, statt jedes Mal eine transiente Unit einzugeben.
Eine dauerhafte Unit für einen Auftrag, den Sie erneut ausführen
[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.shSpeichern Sie die Datei als /etc/systemd/system/nightly-sync.service, führen Sie sudo systemctl daemon-reload aus, starten Sie sie anschließend mit sudo systemctl start nightly-sync und lesen Sie sie mit journalctl -u nightly-sync ein. Fügen Sie eine passende .timer-Datei hinzu, wenn der Auftrag nach einem Zeitplan statt manuell ausgeführt werden soll.
Eine systemd-Service-Unit und ihren Timer schreiben behandelt das Dateiformat und die Syntax für Zeitpläne vollständig.
Wohin die Ausgabe geht und warum sie verschwindet
Die Reihenfolge der Umleitungen ist wichtig. > file 2>&1 leitet die Standardausgabe in die Datei um und leitet anschließend die Standardfehlerausgabe an dieselbe Stelle um. 2>&1 > file macht es umgekehrt: Die Standardfehlerausgabe wird weiterhin an das Terminal ausgegeben, und genau dieses Terminal verschwindet gleich. Bash akzeptiert außerdem &> file für beide Datenströme gleichzeitig.
Die zweite Überraschung ist die Pufferung. Wenn die Standardausgabe ein Terminal ist, leert die C-Bibliothek den Puffer nach jeder Zeile. Wenn die Standardausgabe eine Datei ist, wechselt sie zu einem Blockpuffer von einigen Kilobytes. Deshalb zeigt tail -f ~/import.log minutenlang nichts an, und der Auftrag wirkt beendet. Erzwingen Sie die zeilenweise Pufferung mit stdbuf -oL ./import.sh > ~/import.log 2>&1 oder verwenden Sie den entsprechenden Schalter des Programms, beispielsweise python3 -u oder grep --line-buffered.
Vermeiden Sie dieses Muster:
nohup ./import.sh 2>&1 | tee ~/import.log &nohup schützt import.sh und nichts anderes. tee ist ein separater Prozess in derselben Pipeline und beendet sich bei einem Hangup ebenfalls. import.sh schreibt anschließend in eine Pipe ohne Leser, erhält deshalb SIGPIPE und stoppt. Führen Sie die gesamte Pipeline innerhalb von setsid bash -c '...' aus oder schreiben Sie direkt in die Datei und führen Sie bei der nächsten Verbindung tail -f darauf aus.
Ein weiteres Detail gilt speziell für rsync. --info=progress2 schreibt einen Datenstrom aus Wagenrückläufen, der auf einem Terminal korrekt aussieht, in einer Logdatei oder im Journal jedoch zu einer einzigen sehr langen Zeile wird. Lassen Sie es bei unbeaufsichtigten Ausführungen weg und verwenden Sie stattdessen --stats.
Warum ein Job in Ihrer Shell funktioniert, aber unter systemd oder cron fehlschlägt
Ihre interaktive Shell liest /etc/profile, ~/.profile und ~/.bashrc ein. Daher kennt sie Ihr PATH, die Shims Ihres Versionsmanagers und Ihre exportierten Variablen. Eine systemd-Unit liest keine dieser Dateien ein. cron liest ebenfalls keine davon ein: Unter Debian und Ubuntu führt cron Jobs mit SHELL=/bin/sh und PATH=/usr/bin:/bin aus.
Unter systemd zeigt sich das Problem meist durch systemctl status mit der Meldung (code=exited, status=203/EXEC). Das bedeutet, dass systemd die Datei überhaupt nicht ausführen konnte, weil der Pfad falsch war oder die Datei nicht als ausführbar markiert ist. Unter cron ist das Ergebnis normalerweise command not found. Die Ausgabe wird per lokaler E-Mail zugestellt oder überhaupt nicht, wenn kein E-Mail-System installiert ist.
Ermitteln Sie zuerst die tatsächliche Umgebung, bevor Sie eine Stunde lang nach der Ursache suchen:
sudo systemd-run --collect --wait --unit=envtest /usr/bin/env
journalctl -u envtest --no-pagerDamit wird die exakte Umgebung ausgegeben, mit der Ihr Job ausgeführt wird. Beheben Sie anschließend die Abweichung. Verwenden Sie für eigene Dateien absolute Pfade. systemd löst ein einfaches rsync anhand einer festen Liste von Systempfaden auf und verwendet niemals das PATH Ihrer Shell. Übergeben Sie die benötigten Variablen in der Befehlszeile mit -p Environment="KEY=value" oder in einer Unit-Datei mit EnvironmentFile=/etc/default/myjob. Wenn ein Job tatsächlich Ihre Anmeldeumgebung benötigt, führen Sie ihn mit /bin/bash -lc 'my-command' aus. Dann hängt der Job von Ihren Dotfiles ab.
Was einen abgekoppelten Job weiterhin beendet
- Ein Reboot. tmux bleibt dabei nicht bestehen, weil der Server ein gewöhnlicher Prozess ist und die Sitzungen in seinem Arbeitsspeicherzustand hält. Kernel-Updates erfordern Reboots. Ein Job, den Sie nicht einfach neu starten können, gehört daher in eine Unit, die Sie
systemctl enablekönnen. - Der Out-of-Memory-Killer.
dmesg -T | grep -i 'killed process'zeigt ihn einschließlich des Namens des ausgewählten Prozesses an. Ein großer Import auf einer kleinen VPS ist ein häufiges Ziel. - Die Bereinigung durch logind. Wenn
/etc/systemd/logind.confKillUserProcesses=yessetzt, werden Ihre verbleibenden Prozesse beendet, sobald Ihre letzte Sitzung endet. Das betrifft auch den tmux-Server. Prüfen Sie die aktuelle Einstellung mitloginctl show --property=KillUserProcessesund nehmen Sie Ihren Benutzer mitloginctl enable-linger "$USER"davon aus. - Ein vollständig belegtes Dateisystem. Der Job stoppt, weil das umgeleitete Log das Dateisystem gefüllt hat, nicht weil Sie die Sitzung verlassen haben. Führen Sie
df -haus, bevor Sie das Signal verantwortlich machen.
Einen Job über SSH starten, ohne verbunden zu bleiben
ssh vps 'sudo systemd-run --unit=import --collect /usr/local/bin/import.sh'systemd-run kehrt zurück, sobald die Unit gestartet wurde. Dadurch kehrt auch der Befehl ssh zurück, und der Job ist nicht mit der Sitzung verbunden, die ihn gestartet hat. Das ist die saubere Variante.
Die Variante mit nohup erfordert mehr Sorgfalt:
ssh vps 'nohup ./import.sh > ~/import.log 2>&1 < /dev/null &'Ohne die Umleitungen scheint der Befehl zu hängen. sshd hält den Kanal offen, solange noch ein Prozess die Standardausgabe oder den Standardfehlerausgabekanal des Remote-Befehls geöffnet hat. Ein im Hintergrund gestarteter Job übernimmt beide Dateideskriptoren. nohup allein behebt das Problem nicht, weil nohup die Ausgabe nur dann umleitet, wenn sie an ein Terminal geht. In diesem Fall ist sie jedoch eine Pipe zurück zu Ihrem Client. Mit < /dev/null wird auch die Eingabeseite geschlossen. ssh -n erfüllt dieselbe Aufgabe auf der Client-Seite.
FAQ
Warum wird mein Befehl beendet, wenn die SSH-Verbindung abbricht?
Das pty (Pseudo-Terminal), das Ihre Sitzung verwendet hat, wird zerstört. Der Kernel sendet SIGHUP an die Vordergrund-Prozessgruppe dieses Terminals. Die Standardaktion für SIGHUP besteht darin, den Prozess zu beenden. Auch Hintergrundprozesse werden beendet, weil bash SIGHUP vor dem Beenden an jeden Job in seiner Tabelle erneut sendet. Ein Befehl, der SIGHUP ignoriert, beispielsweise ein mit nohup gestarteter Befehl, oder ein Prozess, der nie zu Ihrer Sitzung gehört hat, beispielsweise eine systemd-Unit, ist davon nicht betroffen.
Ist tmux oder systemd-run für einen sechs Stunden dauernden rsync besser?
systemd-run. Eine tmux-Sitzung hängt von einem von Ihnen gestarteten Serverprozess ab. Sie endet daher beim nächsten Reboot. Außerdem ist sie für Personen unsichtbar, die nicht wissen, dass sie tmux ls ausführen müssen. Wenn Sie sudo systemd-run --unit=bigsync --collect /usr/bin/rsync -aH --stats /srv/data/ /mnt/backup/ ausführen, erhalten Sie systemctl status bigsync für den Status und journalctl -u bigsync für die Ausgabe. Beides findet der nächste Administrator, ohne dass Sie ihn darauf hinweisen müssen. Verwenden Sie tmux für Arbeiten, bei denen Sie den Bildschirm beobachten und Eingaben machen müssen.
Wie kann ich die Ausgabe eines Jobs anzeigen, für den ich die Umleitung vergessen habe?
Normalerweise gar nicht, weil diese Ausgabe an ein Terminal gesendet wurde, das nicht mehr existiert. Solange der Prozess noch läuft, können Sie seine geöffneten Dateien mit sudo ls -l /proc/<pid>/fd untersuchen oder seine Systemaufrufe mit sudo strace -p <pid> überwachen. Bereits geschriebener Text ist jedoch verloren. Mit reptyr <pid> können Sie den Prozess auf ein neues Terminal verschieben. Unter Ubuntu bedeutet kernel.yama.ptrace_scope = 1, dass Sie für einen Prozess, der nicht Ihr eigener Kindprozess ist, sudo benötigen. Die Gewohnheit, die all dies vermeidet, besteht darin, die Ausgabe von Anfang an in eine Datei umzuleiten und diese Datei mit tail -f zu überwachen.
Bleibt eine getrennte tmux-Sitzung nach einem Reboot bestehen?
Nein. Der tmux-Server ist ein gewöhnlicher Prozess, und die Sitzungen sind sein im Speicher befindlicher Zustand. Ein Reboot beendet daher beides. Der Server wird außerdem beendet, wenn /etc/systemd/logind.conf KillUserProcesses=yes setzt und Sie sich aus Ihrer letzten Sitzung abmelden. Das verhindert loginctl enable-linger "$USER". Wenn eine Arbeit nach einem Reboot selbstständig fortgesetzt werden muss, schreiben Sie eine systemd-Unit und systemctl enable sie.
Warum läuft mein Skript in der Shell, schlägt aber als systemd-Unit fehl?
Eine Unit liest weder /etc/profile noch ~/.bashrc. Daher enthält sie weder Ihre Ergänzungen von PATH noch Ihre exportierten Variablen. Wenn systemctl status (code=exited, status=203/EXEC) anzeigt, konnte systemd die Datei überhaupt nicht ausführen. Verwenden Sie daher einen absoluten Pfad und prüfen Sie das Ausführungsbit. Führen Sie sudo systemd-run --collect --wait --unit=envtest /usr/bin/env aus und lesen Sie die Ausgabe mit journalctl -u envtest wieder ein. Damit erhalten Sie exakt die Umgebung, die Ihr Job verwendet. Setzen Sie fehlende Werte mit Environment= oder EnvironmentFile=.