Zum Inhalt springen
Aktueller Gutschein:10 % RabattIn der Bestellung einlösen

App rund um die Uhr auf einem VPS: systemd-Dienst (Node.js, Python, Java)

Betreiben Sie eine App oder einen Bot auf Ihrem VPS als systemd-Dienst: Sie läuft im Hintergrund, startet nach einem Absturz und nach einem Neustart des VPS neu, loggt ins Journal und hält Geheimnisse aus dem Code.

  • Fortgeschrittene
  • ~20 Min.
  • Aktualisiert am 9.10.2026

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.

MethodeNeustart nach AbsturzStart nach Neustart des VPSLogsHinweis
Terminal, nohup, &NeinNeinNur in eine DateiNur für einen schnellen Test geeignet.
screen, tmuxNeinNeinNur im FensterPraktisch für manuelle Arbeit, nicht für den Produktivbetrieb.
pm2JaJa (nach Konfiguration)EigeneEin weiteres Programm, nur Node.js.
systemdJaJaJournalTeil 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:

bash
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/mujbot an, 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:

bash
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 -
Ausgabetext
node-v24.21.0-linux-x64.tar.xz: OK

Fahren 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):

bash
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
BefehlWas er tut
apt -y install curl xz-utilsEin 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.21.0-linux-x64.tar.xz.
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/nodejsEin Verzeichnis für Node.js.
tar -xJf … -C /opt/nodejs --strip-components=1Entpackt 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#

  1. Laden Sie den Code der App nach /opt/mujbot/app hoch (per SFTP, Anleitung, oder mit git clone). Laden Sie weder die Datei .env mit dem Token noch das Verzeichnis node_modules hoch.

  2. Übergeben Sie die Dateien dem App-Benutzer:

    bash
    chown -R mujbot:mujbot /opt/mujbot
  3. Installieren Sie die Pakete als App-Benutzer:

    bash
    cd /opt/mujbot/app
    runuser -u mujbot -- npm ci --omit=dev
    • runuser -u mujbot -- – führt den Befehl als Benutzer mujbot aus (funktioniert auch ohne das Paket sudo).
    • npm ci – installiert genau die Versionen aus package-lock.json. Hat das Projekt keine, verwenden Sie npm install --omit=dev.
    • --omit=dev – überspringt Pakete, die nur für die Entwicklung nötig sind.
  4. 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.

  1. Erstellen Sie die Datei mit nano /etc/mujbot.env:

    /etc/mujbot.envini
    DISCORD_TOKEN=<BOT_TOKEN>
    NODE_ENV=production
    • Jede Zeile ist NAME=wert, ohne export und ohne Leerzeichen um das =.
    • Zeilen, die mit # beginnen, sind Kommentare.
    • In der App lesen Sie den Wert als process.env.DISCORD_TOKEN.
  2. Beschränken Sie die Berechtigungen:

    bash
    chown root:root /etc/mujbot.env
    chmod 600 /etc/mujbot.env
    • chmod 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/systemd/system/mujbot.service:

/etc/systemd/system/mujbot.serviceini
[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
DirektiveWas sie tutEmpfohlener WertWann ändern
Description=Beschreibung des Dienstes in Auflistungen.App-Name–
After=network-online.targetDer Dienst startet erst, wenn das Netzwerk aktiv ist.network-online.target–
Wants=network-online.targetBittet das System, auf das Netzwerk zu warten (After allein reicht nicht).network-online.target–
Type=simpleDer Prozess aus ExecStart ist der Hauptprozess des Dienstes und läuft im Vordergrund.simpleNur wenn sich die App selbst in den Hintergrund forkt (forking).
User=, Group=Als welcher Benutzer die App läuft.der App-BenutzerNie root.
WorkingDirectory=Arbeitsverzeichnis; relative Pfade in der App werden von hier aufgelöst.das App-VerzeichnisWenn 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.jsJe nach Hauptdatei der App.
Restart=on-failureStartet 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-failurealways, wenn sie auch nach einem sauberen Ende erneut starten soll.
RestartSec=5Wartet 5 Sekunden vor dem Neustart (der Standard ist 100 ms).5Länger, wenn die abstürzende App eine fremde API belastet.
NoNewPrivileges=trueWeder die App noch ihre Kindprozesse können höhere Rechte erlangen (zum Beispiel über setuid-Programme).true–
WantedBy=multi-user.targetNach 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=300 und StartLimitBurst=10 hinzu (höchstens 10 Starts in 5 Minuten).

Dienst starten#

bash
systemctl daemon-reload
systemctl enable --now mujbot
systemctl status mujbot
  • systemctl 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:

Ausgabetext
● 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. failed bedeutet, 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.

BefehlWas er zeigt
journalctl -u mujbot -fLive-Ausgabe wie eine Konsole; beenden mit Strg+C.
journalctl -u mujbot -n 100Die letzten 100 Zeilen.
journalctl -u mujbot --since "1 hour ago"Ausgabe der letzten Stunde.
journalctl -u mujbot -bAusgabe seit dem letzten Booten des VPS.
journalctl --disk-usageWie 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:

bash
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.txt
  • python3 -m venv /opt/mujbot/venv – legt eine separate Python-Umgebung im Verzeichnis venv an.
  • /opt/mujbot/venv/bin/pip install -r … – installiert die Pakete aus requirements.txt in 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):

ini
Environment=PYTHONUNBUFFERED=1
ExecStart=/opt/mujbot/venv/bin/python /opt/mujbot/app/bot.py

Java

Installieren Sie die Java-Laufzeitumgebung in der Version, die Ihre App verlangt. Für Java 21:

bash
apt -y install openjdk-21-jre-headless

Fü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:

ini
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 jedem systemctl stop als failed melden.

App aktualisieren#

bash
systemctl stop mujbot
cd /opt/mujbot/app
runuser -u mujbot -- git pull
runuser -u mujbot -- npm ci --omit=dev
systemctl start mujbot
  • git pull – holt die neue Version des Codes (wenn Ihre App in Git liegt; andernfalls laden Sie die Dateien per SFTP hoch und führen chown -R mujbot:mujbot /opt/mujbot aus).
  • npm ci --omit=dev – installiert geänderte Pakete.

Haben sich nur Codedateien geändert, reicht systemctl restart mujbot.

Überprüfung#

  • systemctl is-enabled mujbot gibt enabled aus, systemctl is-active mujbot gibt active aus.
  • 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 zeigt systemctl status mujbot wieder active (running) mit einer anderen Main PID. Ein bloßes systemctl kill ohne -s SIGKILL sendet SIGTERM, das systemd als sauberes Beenden wertet, sodass der Dienst mit Restart=on-failure nicht 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:

bash
systemctl reset-failed mujbot
systemctl start mujbot

Warning: 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 Berechtigungen 600.
  • Nach dem Bearbeiten der Unit-Datei: systemctl daemon-reload; aktivieren und starten: systemctl enable --now.
  • Logs: journalctl -u <DIENSTNAME> -f.

Anleitungen durchsuchen