Ein node app.js in einem Terminal reicht, um eine Anwendung für die Dauer einer Demo am Laufen zu halten. Im Produktivbetrieb lässt dieser Befehl den Dienst unbeaufsichtigt: Der kleinste Absturz stoppt ihn, ein Neustart des Servers vergisst ihn, und nichts kümmert sich um Verschlüsselung oder Last. Der Weg von einem von Hand gestarteten Prozess zu einem verlässlichen Dienst führt über zwei bewährte Bausteine, einen System-Supervisor und einen Reverse Proxy. Hier ein minimales Fundament ohne exotische Abhängigkeiten, um eine Node.js-Anwendung sauber online zu stellen.
Warum Node nicht direkt starten
Ein über die Tastatur gestarteter Prozess erbt die Sitzung, die ihn gestartet hat. Er übersteht keine SSH-Trennung, startet nach einem Absturz nicht neu, kommt nach einem Neustart der Maschine nicht wieder und lauscht oft auf einem hohen Port, der so wie er ist offenliegt. Die Frage ist nicht, ob der Prozess umfällt, sondern wann, und was danach geschieht.
| Ansatz | Automatischer Neustart | Übersteht Boot | Abhängigkeit |
|---|---|---|---|
node app.js | Nein | Nein | Keine |
| Anwendungs-Manager (PM2) | Ja | Per Skript | Globales npm-Paket |
| systemd-Dienst | Ja | Nativ | Bereits in der Distribution |
systemd ist auf den meisten Server-Distributionen standardmäßig installiert. Es kann einen Prozess überwachen, ihn nach einer definierten Richtlinie neu starten, ihn an den Systemstart binden und seine Protokolle zentralisieren. Besser darauf setzen, als ein zusätzliches Werkzeug zu stapeln, wie die Hinweise zum kontrollierten Selbsthosten bereits festhielten.
Ein systemd-Dienst zur Überwachung des Prozesses
Erste Regel: Die Anwendung läuft nie als root. Ein eigener Systembenutzer ohne Login-Shell begrenzt die Angriffsfläche im Fall einer Kompromittierung.
sudo useradd --system --home /srv/monapp --shell /usr/sbin/nologin monapp
sudo chown -R monapp:monapp /srv/monappDer Dienst wird in einer Unit-Datei unter /etc/systemd/system/ beschrieben. Sensible Variablen bleiben außerhalb des Repositoriums und werden aus einer Umgebungsdatei mit eingeschränkten Rechten geladen.
[Unit]
Description=Node-Anwendung monapp
After=network.target
[Service]
Type=simple
User=monapp
Group=monapp
WorkingDirectory=/srv/monapp
EnvironmentFile=/srv/monapp/.env
Environment=NODE_ENV=production
ExecStart=/usr/bin/node /srv/monapp/server.js
Restart=on-failure
RestartSec=2
# Haertung
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/srv/monapp/tmp
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now monapp
sudo systemctl status monapp
journalctl -u monapp -fDie Protokolle laufen in journalctl, mit Zeitstempel und vom System rotiert, ohne manuelle Umleitung in eine Datei. Die Richtlinie Restart=on-failure startet den Dienst bei einem abnormalen Ende neu, und RestartSec verhindert eine zu aggressive Neustartschleife.
Nginx als Reverse Proxy
Die Anwendung lauscht lokal auf 127.0.0.1:3000 und wird nie direkt dem Internet ausgesetzt. Nginx stellt sich davor: Es terminiert TLS, liefert statische Dateien aus, wendet Komprimierung an und leitet den Rest an Node weiter.
server {
listen 80;
server_name monapp.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket-Unterstuetzung
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
gzip on;
gzip_types text/plain text/css application/javascript application/json;
}Das Zertifikat wird mit Certbot bezogen und erneuert, das die Konfiguration automatisch auf HTTPS umstellt.
sudo certbot --nginx -d monapp.example.comSobald der Proxy steht, bleibt nur Port 443 öffentlich; der Anwendungsport verlässt die lokale Loopback-Schnittstelle nicht. Diese Abschottung geht Hand in Hand mit regelmäßigen Backups, deren Prinzip im Leitfaden zu automatisierten Backups eines Webservers erläutert wird.
Aktualisierung ohne Ausfall
Ein bloßes systemctl restart legt den Dienst für einen Moment lahm. Um dieses Ausfallfenster zu vermeiden, laufen zwei Instanzen parallel hinter einem Upstream, und der Neustart erfolgt nacheinander.
upstream monapp {
server 127.0.0.1:3000 max_fails=1 fail_timeout=5s;
server 127.0.0.1:3001 max_fails=1 fail_timeout=5s;
}Dennoch muss sich die Anwendung sauber beenden: Sie darf keine neuen Verbindungen mehr annehmen, muss die laufenden Anfragen abschließen und dann beenden. Das ist die Aufgabe eines Shutdown-Handlers auf SIGTERM, dem Signal, das systemd sendet.
const server = app.listen(process.env.PORT || 3000);
process.on("SIGTERM", () => {
server.close(() => {
// Verbindungen geleert, Ressourcen werden freigegeben
process.exit(0);
});
// Sicherheitsnetz, falls ein Socket haengen bleibt
setTimeout(() => process.exit(1), 10000).unref();
});Die beiden Instanzen stammen aus einer Vorlagen-Unit: Eine Datei [email protected] übernimmt die vorherige Unit, ersetzt aber die Umgebungszeile durch Environment=PORT=%i, wobei %i die nach dem At-Zeichen übergebene Zahl ist. Der rollende Neustart startet dann jede Instanz nacheinander neu und überlässt es Nginx, den Verkehr auf die jeweils verfügbare zu leiten.
sudo systemctl enable --now monapp@3000 monapp@3001
# Aktualisierung ohne Ausfall
sudo systemctl restart monapp@3000
sleep 5
sudo systemctl restart monapp@3001Im Produktivbetrieb ist die eigentliche Frage nicht, ob der Prozess umfällt, sondern was in der Sekunde geschieht, in der er umfällt.
Punkt zur Vorsicht. Drei Fehler kehren immer wieder: die Anwendung als root laufen lassen, NODE_ENV=production vergessen und Geheimnisse ins Repositorium einchecken. Der erste öffnet die Tür zu einer Rechteausweitung, der zweite schaltet Optimierungen ab und lässt Fehlermeldungen zu gesprächig, der dritte legt Schlüssel schon beim ersten git push offen. Eine Umgebungsdatei mit den Rechten 600, im Besitz des Anwendungsbenutzers, löst den letzten Punkt.
Prüfpunkte vor dem Livegang
| Prüfung | Erwartet |
|---|---|
| Ausführender Benutzer | Eigenes Systemkonto, nie root |
| Umgebungsvariable | NODE_ENV=production |
| Neustartrichtlinie | Restart=on-failure aktiv |
| Verschlüsselung | TLS via Certbot, automatische Erneuerung |
| Anwendungsport | An 127.0.0.1 gebunden, nie öffentlich |
| Health-Endpunkt | Eine Route /health für die Überwachung |
| Firewall | Nur 80 und 443 offen |
Was bleibt
Ein robustes Node.js-Deployment braucht kein schweres Werkzeug: einen systemd-Dienst für die Überwachung, einen Nginx-Reverse-Proxy für TLS und Routing, einen eigenen Benutzer für die Abschottung und ein sauberes Beenden auf SIGTERM für Aktualisierungen ohne Ausfall. Dieses Fundament läuft auf jedem aktuellen Server und bleibt sechs Monate später lesbar, wenn es jemand wieder aufgreifen muss. Es taugt für eine API ebenso wie für eine serverseitig gerenderte Anwendung, wie sie auf den aktuellen Node.js-Versionen gebaut werden.
Ich habe lange PM2 benutzt, aus Gewohnheit, bevor ich auf meinen eigenen Servern zu systemd zurückgekehrt bin. Der Wechsel hat mir eine zu pflegende Abhängigkeit erspart und Protokolle beschert, die endlich mit dem Rest der Maschine vereinheitlicht sind. Mein einziger systematischer Zusatz ist heute eine triviale /health-Route, die den Datenbankzugriff prüft: Sie ist es, die mich vor einem Vorfall warnt, bevor der Kunde es tut. — Simon Janvier
Weiterführend
Referenz der Dienstdirektiven an der Primärquelle: systemd.service-Dokumentation.
