Eine im Terminal gestartete App endet, sobald Sie SSH schließen oder den VPS neu starten. Wir richten sie als systemd-Dienst ein: Sie läuft im Hintergrund, startet sich nach einem Absturz selbst neu, startet beim Booten des VPS, und ihre Ausgabe landet im Systemlog.
Was Sie brauchen
- Ein StormHost-VPS mit Ubuntu 24.04 oder Debian 13
- Eine SSH-Verbindung als root (Anleitung)
- Eine App, die Sie von der Befehlszeile aus starten können (zum Beispiel ein Discord-Bot in Node.js)
- Etwa 20 Minuten
Warum systemd#
systemd ist der Dienstmanager, der sowohl bei Ubuntu als auch bei Debian mitgeliefert wird. Er startet alle Systemdienste (SSH, Nginx, Datenbanken), es muss also nichts zusätzlich installiert werden.
| Methode | Neustart nach Absturz | Start nach Neustart des VPS | Logs | Hinweis |
|---|---|---|---|---|
Terminal, nohup, & | Nein | Nein | Nur in eine Datei | Nur für einen schnellen Test geeignet. |
screen, tmux | Nein | Nein | Nur im Fenster | Praktisch für manuelle Arbeit, nicht für den Produktivbetrieb. |
| pm2 | Ja | Ja (nach Konfiguration) | Eigene | Ein weiteres Programm, nur Node.js. |
| systemd | Ja | Ja | Journal | Teil des Systems, funktioniert für jede Sprache. |
Ein Benutzer für die App#
Die App sollte nicht als root laufen. Kompromittiert sie jemand, erhält er nur die Berechtigungen ihres Benutzers. Legen Sie einen Systembenutzer mit Home-Verzeichnis in /opt an:
useradd --system --create-home --home-dir /opt/mujbot --shell /usr/sbin/nologin mujbot--system– ein Systemkonto für einen Dienst (niedrigere Benutzer-ID, kein Passwort).--create-home --home-dir /opt/mujbot– legt das Verzeichnis/opt/mujbotan, das dem Benutzer gehört.--shell /usr/sbin/nologin– niemand kann sich als dieser Benutzer per SSH oder im Terminal anmelden.mujbot– der Benutzername (und der Name der zugehörigen Gruppe). Wählen Sie Ihren eigenen und ersetzen Sie ihn überall in dieser Anleitung.
Node.js LTS#
Das Repository von Ubuntu 24.04 liefert Node.js 18 und Debian 13 Node.js 20. Keine der beiden Versionen wird vom Node.js-Projekt noch unterstützt (18 seit dem 30. April 2025, 20 seit dem 30. April 2026). Installieren Sie das aktuelle LTS-Release aus den offiziellen Paketen auf nodejs.org (öffnet sich in einem neuen Fenster). Im Oktober 2026 ist das aktive LTS Node.js 24 (Ende Oktober 2026 wird Node.js 26 zum LTS).
Laden Sie zuerst das Archiv herunter und prüfen Sie es:
apt -y install curl xz-utils
cd /tmp
NODE_SOUBOR=$(curl -fsSL https://nodejs.org/dist/latest-v24.x/SHASUMS256.txt | grep -o 'node-v[0-9.]*-linux-x64.tar.xz' | head -n 1)
curl -fsSLO "https://nodejs.org/dist/latest-v24.x/$NODE_SOUBOR"
curl -fsSL https://nodejs.org/dist/latest-v24.x/SHASUMS256.txt | grep " $NODE_SOUBOR\$" | sha256sum -c -node-v24.21.0-linux-x64.tar.xz: OKFahren Sie nur fort, wenn Sie OK sehen. Erhalten Sie FAILED, löschen Sie die Datei (rm /tmp/node-v*.tar.xz) und laden Sie sie erneut herunter. Installieren Sie dann Node.js (im selben Terminalfenster, die Variable NODE_SOUBOR muss noch gesetzt sein):
mkdir -p /opt/nodejs
tar -xJf "$NODE_SOUBOR" -C /opt/nodejs --strip-components=1
ln -sf /opt/nodejs/bin/node /usr/local/bin/node
ln -sf /opt/nodejs/bin/npm /usr/local/bin/npm
ln -sf /opt/nodejs/bin/npx /usr/local/bin/npx| Befehl | Was er tut |
|---|---|
apt -y install curl xz-utils | Ein Download-Werkzeug und Unterstützung zum Entpacken von .tar.xz-Archiven. |
NODE_SOUBOR=$(… SHASUMS256.txt | grep -o … | head -n 1) | Liest die Prüfsummenliste des neuesten 24.x-Release und findet den Dateinamen für Linux x64, zum Beispiel node-v24.. |
curl -fsSLO "…/$NODE_SOUBOR" | Lädt das Archiv herunter; -O speichert es unter dem ursprünglichen Namen. |
… | grep " $NODE_SOUBOR\$" | sha256sum -c - | Prüft die Prüfsumme der heruntergeladenen Datei gegen die offizielle Liste. Schützt Sie vor einer beschädigten oder manipulierten Datei. |
mkdir -p /opt/nodejs | Ein Verzeichnis für Node.js. |
tar -xJf … -C /opt/nodejs --strip-components=1 | Entpackt das Archiv (J = xz-Format) nach /opt/nodejs ohne das oberste, nach der Version benannte Verzeichnis. |
ln -sf /opt/nodejs/bin/… /usr/local/bin/… | Erstellt Links auf node, npm und npx in einem Verzeichnis, in dem das System nach Programmen sucht. Der Pfad /usr/local/bin/node bleibt nach Updates gleich. |
Prüfung: node -v gibt v24.… aus, npm -v die npm-Version.
Für eine andere Release-Linie ersetzen Sie latest-v24.x in allen URLs zum Beispiel durch latest-v26.x. Node.js aktualisieren = Dienst stoppen, /opt/nodejs löschen und die Schritte wiederholen.
Warum nicht nvm
nvm (auf der Download-Seite von nodejs.org angeboten) installiert Node.js in das Home-Verzeichnis des Benutzers unter einem Pfad, der die Versionsnummer enthält. Ein systemd-Dienst braucht einen festen absoluten Pfad zum Programm, der sich nach einem Update nicht ändert. Deshalb verwenden wir das offizielle Archiv mit einem Link in /usr/local/bin.
Die App#
Laden Sie den Code der App nach
/opt/mujbot/apphoch (per SFTP, Anleitung, oder mitgit clone). Laden Sie weder die Datei.envmit dem Token noch das Verzeichnisnode_moduleshoch.Übergeben Sie die Dateien dem App-Benutzer:
bash chown -R mujbot:mujbot /opt/mujbotInstallieren Sie die Pakete als App-Benutzer:
bash cd /opt/mujbot/app runuser -u mujbot -- npm ci --omit=devrunuser -u mujbot --– führt den Befehl als Benutzermujbotaus (funktioniert auch ohne das Paket sudo).npm ci– installiert genau die Versionen auspackage-lock.json. Hat das Projekt keine, verwenden Sienpm install --omit=dev.--omit=dev– überspringt Pakete, die nur für die Entwicklung nötig sind.
Probieren Sie die App von Hand:
runuser -u mujbot -- node index.js. Startet sie ohne Fehler, stoppen Sie sie mit Strg+C. (Braucht sie einen Token, machen Sie den manuellen Test nach dem Abschnitt Geheimnisse.)
Geheimnisse in EnvironmentFile#
Tokens und Passwörter gehören weder in den Code noch in die Unit-Datei. Speichern Sie sie in einer Variablendatei, die nur root lesen kann, und systemd übergibt sie der App beim Start.
Erstellen Sie die Datei mit
nano /etc/mujbot.env:/etc/ mujbot.env iniDISCORD_TOKEN=<BOT_TOKEN> NODE_ENV=production- Jede Zeile ist
NAME=wert, ohneexportund ohne Leerzeichen um das=. - Zeilen, die mit
#beginnen, sind Kommentare. - In der App lesen Sie den Wert als
process..env. DISCORD_ TOKEN
- Jede Zeile ist
Beschränken Sie die Berechtigungen:
bash chown root:root /etc/mujbot.env chmod 600 /etc/mujbot.envchmod 600– nur root kann die Datei lesen und ändern. systemd liest sie, bevor es zum App-Benutzer wechselt, sodass die Variablen die App erreichen, aber niemand sonst die Datei lesen kann.
Die Unit-Datei des Dienstes#
Erstellen Sie die Datei mit nano /etc/:
[Unit]
Description=Mein Discord-Bot
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=mujbot
Group=mujbot
WorkingDirectory=/opt/mujbot/app
EnvironmentFile=/etc/mujbot.env
ExecStart=/usr/local/bin/node /opt/mujbot/app/index.js
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target| Direktive | Was sie tut | Empfohlener Wert | Wann ändern |
|---|---|---|---|
Description= | Beschreibung des Dienstes in Auflistungen. | App-Name | – |
After= | Der Dienst startet erst, wenn das Netzwerk aktiv ist. | network-online.target | – |
Wants= | Bittet das System, auf das Netzwerk zu warten (After allein reicht nicht). | network-online.target | – |
Type=simple | Der Prozess aus ExecStart ist der Hauptprozess des Dienstes und läuft im Vordergrund. | simple | Nur wenn sich die App selbst in den Hintergrund forkt (forking). |
User=, Group= | Als welcher Benutzer die App läuft. | der App-Benutzer | Nie root. |
WorkingDirectory= | Arbeitsverzeichnis; relative Pfade in der App werden von hier aufgelöst. | das App-Verzeichnis | Wenn Sie die App verschieben. |
EnvironmentFile= | Datei mit Umgebungsvariablen. Existiert sie nicht, startet der Dienst nicht (ein Bindestrich vor dem Pfad, -/etc/…, macht sie optional). | /etc/mujbot.env | – |
ExecStart= | Der Startbefehl. Das Programm muss als absoluter Pfad angegeben werden. | /usr/local/bin/node /opt/…/index.js | Je nach Hauptdatei der App. |
Restart=on-failure | Startet neu, wenn der Prozess mit einem Code ungleich null endet oder durch ein Signal beendet wird (außer SIGTERM, SIGINT, SIGHUP und SIGPIPE, die systemd als sauberes Beenden wertet). Beenden mit Code 0 löst keinen Neustart aus. | on-failure | always, wenn sie auch nach einem sauberen Ende erneut starten soll. |
RestartSec=5 | Wartet 5 Sekunden vor dem Neustart (der Standard ist 100 ms). | 5 | Länger, wenn die abstürzende App eine fremde API belastet. |
NoNewPrivileges=true | Weder die App noch ihre Kindprozesse können höhere Rechte erlangen (zum Beispiel über setuid-Programme). | true | – |
WantedBy= | Nach enable startet der Dienst beim Booten des VPS. | multi-user.target | – |
Startratenbegrenzung
Standardmäßig erlaubt systemd höchstens 5 Starts eines Dienstes innerhalb von 10 Sekunden. Mit RestartSec=5 wird dieses Limit nie erreicht, sodass ein abstürzender Dienst endlos alle 5 Sekunden einen Startversuch unternimmt. Soll er nach einer Serie von Abstürzen aufgeben, fügen Sie im Abschnitt [Unit] zum Beispiel StartLimitIntervalSec= und StartLimitBurst=10 hinzu (höchstens 10 Starts in 5 Minuten).
Dienst starten#
systemctl daemon-reload
systemctl enable --now mujbot
systemctl status mujbotsystemctl daemon-reload– lädt neue und geänderte Unit-Dateien. Führen Sie es nach jeder Änderung an einer.service-Datei aus.systemctl enable --now mujbot– aktiviert den Start beim Booten des VPS und startet den Dienst sofort.systemctl status mujbot– der Dienststatus und die letzten Logzeilen.
Achten Sie in der Ausgabe von status auf:
● mujbot.service - Mein Discord-Bot
Loaded: loaded (/etc/systemd/system/mujbot.service; enabled; preset: enabled)
Active: active (running) since …
Main PID: 12345 (node)Loaded: … enabled– der Dienst startet nach einem Neustart des VPS.Active: active (running)– er läuft.failedbedeutet, dass er abgestürzt ist;activating (auto-restart), dass er auf den Neustart wartet.Main PID– die Prozess-ID der App.
Weitere Befehle: systemctl restart mujbot (Neustart), systemctl stop mujbot (Stopp), systemctl disable mujbot (nicht beim Booten starten).
Logs#
Alles, was die App ausgibt (console.log, print, Fehler), speichert systemd im Journal.
| Befehl | Was er zeigt |
|---|---|
journalctl -u mujbot -f | Live-Ausgabe wie eine Konsole; beenden mit Strg+C. |
journalctl -u mujbot -n 100 | Die letzten 100 Zeilen. |
journalctl -u mujbot --since "1 hour ago" | Ausgabe der letzten Stunde. |
journalctl -u mujbot -b | Ausgabe seit dem letzten Booten des VPS. |
journalctl --disk-usage | Wie viel Platz das Journal belegt. |
Das Journal räumt sich selbst auf: Standardmäßig nutzt es höchstens 10 % der Dateisystemgröße, bis zu 4 GB.
Python und Java#
Die Schritte sind dieselben; nur die Installation der Laufzeitumgebung und die Zeile ExecStart unterscheiden sich.
Python
Python 3 ist bereits auf dem System. Legen Sie für die App eine virtuelle Umgebung an, damit sich ihre Pakete nicht mit den Systempaketen mischen:
apt -y install python3-venv
runuser -u mujbot -- python3 -m venv /opt/mujbot/venv
runuser -u mujbot -- /opt/mujbot/venv/bin/pip install -r /opt/mujbot/app/requirements.txtpython3 -m venv /opt/mujbot/venv– legt eine separate Python-Umgebung im Verzeichnisvenvan./opt/mujbot/venv/bin/pip install -r …– installiert die Pakete ausrequirements.txtin diese Umgebung.
Verwenden Sie in der Unit-Datei das Python aus der Umgebung und schalten Sie die Ausgabepufferung aus (sonst erscheinen Logs im Journal verzögert):
Environment=PYTHONUNBUFFERED=1
ExecStart=/opt/mujbot/venv/bin/python /opt/mujbot/app/bot.pyJava
Installieren Sie die Java-Laufzeitumgebung in der Version, die Ihre App verlangt. Für Java 21:
apt -y install openjdk-21-jre-headlessFür Java 25 installieren Sie stattdessen das Paket openjdk-25-jre-headless. Beide sind in den Repositories von Ubuntu 24.04 und Debian 13. Prüfen Sie die Version mit java -version.
In der Unit-Datei:
ExecStart=/usr/bin/java -Xms512M -Xmx1G -jar /opt/mujbot/app/aplikace.jar
SuccessExitStatus=143-Xms512M -Xmx1G– Anfangs- und Maximalspeicher von Java. Setzen Sie das Maximum deutlich unter den freien RAM des VPS.SuccessExitStatus=143– bei einem sauberen Stopp (Signal SIGTERM) endet Java mit Code 143. Ohne diese Zeile würde systemd den Dienst nach jedemsystemctl stopalsfailedmelden.
App aktualisieren#
systemctl stop mujbot
cd /opt/mujbot/app
runuser -u mujbot -- git pull
runuser -u mujbot -- npm ci --omit=dev
systemctl start mujbotgit pull– holt die neue Version des Codes (wenn Ihre App in Git liegt; andernfalls laden Sie die Dateien per SFTP hoch und führenchown -R mujbot:mujbot /opt/mujbotaus).npm ci --omit=dev– installiert geänderte Pakete.
Haben sich nur Codedateien geändert, reicht systemctl restart mujbot.
Überprüfung#
systemctl is-enabled mujbotgibtenabledaus,systemctl is-active mujbotgibtactiveaus.- Nach einem Neustart des VPS (
reboot) läuft die App, ohne dass Sie etwas tun. - Test des automatischen Neustarts:
systemctl kill -s SIGKILL mujbot(beendet den Prozess gewaltsam, als wäre er abgestürzt). Innerhalb von 5 Sekunden zeigtsystemctl status mujbotwiederactive (running)mit einer anderenMain PID. Ein bloßessystemctl killohne-s SIGKILLsendet SIGTERM, das systemd als sauberes Beenden wertet, sodass der Dienst mitRestart=on-failurenicht erneut startet.
Fehlerbehebung#
status=203/EXEC#
systemd konnte das Programm aus ExecStart nicht starten. Der Programmpfad existiert nicht, ist nicht absolut, oder die Datei ist nicht ausführbar. Prüfen Sie den Pfad (ls -l /usr/local/bin/node) und bei Skripten die erste Zeile (#!/usr/bin/env …) und die Ausführungsberechtigung.
status=217/USER#
Der Benutzer aus User= existiert nicht. Prüfen Sie den Namen (id mujbot) oder legen Sie den Benutzer wie unter Ein Benutzer für die App beschrieben an.
status=200/CHDIR#
Das Verzeichnis aus WorkingDirectory= existiert nicht, oder der Benutzer kann nicht darauf zugreifen. Prüfen Sie Pfad und Besitzer (ls -ld /opt/mujbot/app).
Failed to load environment files: No such file or directory#
Die Datei aus EnvironmentFile= existiert nicht. Prüfen Sie den Pfad oder setzen Sie - davor, wenn die Datei optional ist.
Start request repeated too quickly. (start-limit-hit)#
Der Dienst ist zu oft hintereinander abgestürzt, und systemd hat aufgehört, ihn zu starten. Die Ursache finden Sie im Log (journalctl -u mujbot -n 50). Ist sie behoben, setzen Sie den Zähler zurück und starten Sie den Dienst:
systemctl reset-failed mujbot
systemctl start mujbotWarning: The unit file … changed on disk. Run 'systemctl daemon-reload' to reload units.#
Sie haben die Unit-Datei bearbeitet und systemctl daemon-reload nicht ausgeführt. Führen Sie es aus und starten Sie den Dienst neu.
Die App meldet EACCES oder permission denied#
Der App-Benutzer kann die Dateien nicht lesen oder schreiben. Das passiert typischerweise nach dem Hochladen von Dateien als root. Korrigieren Sie den Besitzer: chown -R mujbot:mujbot /opt/mujbot. Ein normaler Benutzer kann keinen Port unter 1024 öffnen (zum Beispiel 80); lassen Sie die App auf einem höheren Port lauschen und setzen Sie Nginx davor (Anleitung).
Die App sieht eine Variable aus .env nicht#
systemd lädt die Datei .env im App-Verzeichnis nicht von selbst. Legen Sie die Variablen in die Datei aus EnvironmentFile= oder laden Sie sie in der App mit einer Bibliothek (zum Beispiel dotenv). Starten Sie den Dienst nach einer Änderung der Variablendatei neu.
Zusammenfassung
- Die App läuft als eigener Benutzer, nicht als root.
- Die Unit-Datei in
/etc/systemd/system/legt Benutzer, Verzeichnis, den Befehl mit absolutem Pfad und den Neustart nach einem Absturz fest. - Geheimnisse liegen in der Datei aus
EnvironmentFile=mit Berechtigungen600. - Nach dem Bearbeiten der Unit-Datei:
systemctl daemon-reload; aktivieren und starten:systemctl enable --now. - Logs:
journalctl -u <DIENSTNAME> -f.
Weiterführende Links#
- systemd.service (man) (öffnet sich in einem neuen Fenster)
- systemd.exec (man) (öffnet sich in einem neuen Fenster)
- systemd.unit (man) (öffnet sich in einem neuen Fenster)
- journalctl (man) (öffnet sich in einem neuen Fenster)
- Node.js: Downloads und Releases (öffnet sich in einem neuen Fenster)
- Node.js: Release-Zeitplan (öffnet sich in einem neuen Fenster)