PM2 + Nginx Reverse Proxy: Node.js-App mit Port 80 verbinden

TestumgebungDebian-FamilieUbuntu 24.04.5 LTSnormales Benutzerkonto
Betrieb der Node.js-App und Reverse Proxy geprüft

Auf einem Ubuntu 24.04 Server haben wir einen Node.js PM2 Nginx Reverse Proxy eingerichtet: PM2 führt die App aus, und Nginx leitet Anfragen, die auf Port 80 ankommen, an die App weiter. Außerdem haben wir geprüft, dass PM2 die App wieder startet, auch wenn der App-Prozess beendet oder der Server neu gestartet wird.

Die Ergebnisboxen unten zeigen die Ausgabe von Befehlen, die mit dem normalen Benutzer deploy ausgeführt wurden. Als Domain steht der Beispielwert example.com, und die Anfragen wurden innerhalb des Servers mit einem Host-Header gesendet.

🎯 Kernpunkte
🗂️ Ablauf der Anfrage
📂 NginxReverse Proxy
📄 CertbotHTTPS-Zertifikat
📂 PM2App ausführen und Autostart
📄 Node.js-App
App mit PM2 ausführen und beim Booten starten
Nginx leitet Anfragen auf Port 80 an die App weiter

1️⃣ Node.js installieren

Node.js wird installiert, indem das NodeSource-Repository in apt eingetragen wird, und als Version haben wir Node.js 26 gewählt, die LTS-Version mit Stand November 2026.

🔲 NodeSource setup_26.x

export DEBIAN_FRONTEND=noninteractive

Wenn du diesen Wert zuerst setzt, erscheint während der Paketinstallation kein Fenster, das nach Einstellungen fragt. Die Option -E des sudo-Befehls unten gibt diesen Wert auch an den Befehl weiter, der mit root-Rechten läuft.

curl -fsSL https://deb.nodesource.com/setup_26.x | sudo -E bash -
2026-11-07 16:08:16 - Repository configured successfully.
2026-11-07 16:08:16 - To install Node.js, run: apt install nodejs -y

Sobald das Skript das Repository eingetragen hat, kannst du Node.js wie ein Ubuntu-Paket mit apt installieren.

🔲 apt-get install nodejs

sudo -E apt-get install -y nodejs
Setting up libatomic1:amd64 (14.2.0-4ubuntu2~24.04.1) ...
Setting up nodejs (26.13.0-1nodesource1) ...
Processing triggers for libc-bin (2.39-0ubuntu8.9) ...
Processing triggers for man-db (2.12.0-4build2) ...

🔲 node -v, npm -v

node -v
v26.13.0
npm -v
11.20.0

npm ist im Paket nodejs enthalten, daher installierst du es nicht separat.

2️⃣ Node.js-Test-App

Um zu sehen, ob PM2 und Nginx richtig verbunden sind, erstellst du eine Test-App, die bei einer Anfrage eine Zeile Text zurückgibt.

🔲 app.js

mkdir -p ~/hello

Erstelle unter deinem Home-Ordner den Ordner hello und lege darin die Datei app.js an.

cat > ~/hello/app.js <<'APP'
const http = require('http');
http.createServer((req, res) => {
  console.log(req.method, req.url);
  res.end('hello from node\n');
}).listen(3000, '127.0.0.1');
APP

Die App nimmt Anfragen nur auf Port 3000 von 127.0.0.1 an, daher lässt sie sich von außerhalb des Servers nicht direkt öffnen. Anfragen von außen nimmt Nginx entgegen und leitet sie an diesen Port weiter.

3️⃣ App mit PM2 ausführen und Autostart

PM2 ist ein Prozessmanager, der Node.js-Apps im Hintergrund ausführt und eine App wieder startet, wenn ihr Prozess endet. In diesem Abschnitt richten wir auch den Autostart ein, der die App beim Booten des Servers wieder hochfährt.

🔲 npm install -g pm2

sudo npm install -g pm2
added 45 packages in 5s
pm2 -v
[PM2] Spawning PM2 daemon with pm2_home=/home/deploy/.pm2
[PM2] PM2 Successfully daemonized
7.1.2

Beim ersten pm2-Befehl startet auch der PM2-Daemon, und die installierte PM2-Version ist 7.1.2.

🔲 pm2 start

cd ~/hello

Wechsle in den Ordner mit app.js und führe die App dann mit PM2 aus.

pm2 start app.js --name hello
[PM2] Starting /home/deploy/hello/app.js in fork_mode (1 instance)
[PM2] Done.
┌────┬──────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name     │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
├────┼──────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0  │ hello    │ default     │ N/A     │ fork    │ 17257    │ 0s     │ 0    │ online    │ 0%       │ 26.3mb   │ deploy   │ disabled │
└────┴──────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘

--name ist der App-Name, der in der PM2-Liste angezeigt wird, und die folgenden Befehle sprechen die App über diesen Namen an.

pm2 list
┌────┬──────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name     │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
├────┼──────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0  │ hello    │ default     │ N/A     │ fork    │ 17257    │ 2s     │ 0    │ online    │ 0%       │ 58.5mb   │ deploy   │ disabled │
└────┴──────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
host metrics | cpu: 0% | ram usage: 38.8% | disk: ⇓ 0mb/s ⇑ 0mb/s

online in der Spalte status bedeutet, dass die App läuft, und die Spalte ↺ zeigt, wie oft PM2 die App neu gestartet hat.

🔲 curl 127.0.0.1:3000

Wenn du innerhalb des Servers eine Anfrage sendest, kommt die Textzeile, die die App zurückgibt, unverändert an.

curl -sS http://127.0.0.1:3000/
hello from node

🔲 pm2 restart

Nachdem du den App-Code geändert hast, startest du die App neu, damit die Änderung greift.

pm2 restart hello
Use --update-env to update environment variables
[PM2] Applying action restartProcessId on app [hello](ids: [ 0 ])
[PM2] [hello](0) ✓
┌────┬──────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name     │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
├────┼──────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0  │ hello    │ default     │ N/A     │ fork    │ 17306    │ 0s     │ 1    │ online    │ 0%       │ 19.3mb   │ deploy   │ disabled │
└────┴──────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
host metrics | cpu: 0% | ram usage: 38.8% | disk: ⇓ 0mb/s ⇑ 0mb/s
pm2 list
┌────┬──────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name     │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
├────┼──────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0  │ hello    │ default     │ N/A     │ fork    │ 17306    │ 2s     │ 1    │ online    │ 0%       │ 58.3mb   │ deploy   │ disabled │
└────┴──────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
host metrics | cpu: 0% | ram usage: 38.8% | disk: ⇓ 0mb/s ⇑ 0mb/s

Nach dem Neustart wechselte die pid von 17257 zu 17306, und die Spalte ↺ stieg auf 1.

🔲 kill -9

Ein plötzliches Ende des App-Prozesses lässt sich mit kill -9 herbeiführen, und pm2 pid gibt die Prozessnummer der App aus.

kill -9 "$(pm2 pid hello)"

Prüfe die PM2-Liste nach 3 Sekunden noch einmal.

pm2 list
┌────┬──────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name     │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
├────┼──────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0  │ hello    │ default     │ N/A     │ fork    │ 17797    │ 3s     │ 2    │ online    │ 0%       │ 58.6mb   │ deploy   │ disabled │
└────┴──────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
host metrics | cpu: 51.2% | ram usage: 35.8% | eth0: ⇓ 1kb/s ⇑ 1kb/s | eth1: ⇓ 19kb/s ⇑ 0mb/s | disk: ⇓ 728kb/s ⇑ 3.1mb/s

PM2 hatte die App mit der neuen pid 17797 wieder gestartet, sie war online, und die Spalte ↺ war auf 2 gestiegen.

🔲 pm2 save

pm2 save
[PM2] Saving current process list...
[PM2] Successfully saved in /home/deploy/.pm2/dump.pm2

pm2 save speichert die Liste der gerade laufenden Apps in der Datei dump.pm2, und beim Booten startet PM2 die Apps nach dieser Liste wieder.

🔲 pm2 startup

Damit PM2 beim Booten läuft, registrierst du PM2 als systemd-Dienst. Der Befehl unten erstellt den Dienst mit dem Namen des aktuellen Kontos und dessen Home-Ordner.

sudo env PATH="$PATH" pm2 startup systemd -u "$(id -un)" --hp "$HOME"
ExecStart=/usr/lib/node_modules/pm2/bin/pm2 resurrect
ExecReload=/usr/lib/node_modules/pm2/bin/pm2 reload all
ExecStop=/usr/lib/node_modules/pm2/bin/pm2 kill

[Install]
WantedBy=multi-user.target

Target path
/etc/systemd/system/pm2-deploy.service
Command list
[ 'systemctl enable pm2-deploy' ]
[PM2] Writing init configuration in /etc/systemd/system/pm2-deploy.service
[PM2] Making script booting at startup...
[PM2] [-] Executing: systemctl enable pm2-deploy...
Created symlink /etc/systemd/system/multi-user.target.wants/pm2-deploy.service → /etc/systemd/system/pm2-deploy.service.
[PM2] [v] Command successfully executed.

env PATH sorgt dafür, dass pm2, das mit root-Rechten läuft, den node-Pfad des aktuellen Kontos findet, und -u und --hp sind das Konto und der Home-Ordner, mit denen der Dienst läuft. Die Zeile ExecStart in der Dienstdatei ist pm2 resurrect, das beim Booten die gespeicherte App-Liste wiederherstellt.

systemctl status pm2-deploy --no-pager | head -n 5
○ pm2-deploy.service - PM2 process manager
     Loaded: loaded (/etc/systemd/system/pm2-deploy.service; enabled; preset: enabled)
     Active: inactive (dead)
       Docs: https://pm2.keymetrics.io/

enabled bedeutet, dass der Dienst beim Booten ausgeführt wird. pm2 startup registriert den Dienst nur und startet ihn jetzt nicht, daher steht dort inactive (dead), und der Dienst läuft ab dem nächsten Booten.

🔲 pm2 list nach dem Neustart

sudo systemctl reboot

Verbinde dich nach etwa 2 Minuten erneut als deploy und prüfe den Zustand der App.

uptime -p
up 2 minutes
pm2 list
┌────┬──────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name     │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
├────┼──────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0  │ hello    │ default     │ N/A     │ fork    │ 976      │ 108s   │ 0    │ online    │ 0%       │ 63.0mb   │ deploy   │ disabled │
└────┴──────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
host metrics | cpu: 0.3% | ram usage: 35.1% | disk: ⇓ 4kb/s ⇑ 4kb/s

Zwei Minuten nach dem Hochfahren des Servers war die App bereits seit 108 Sekunden online, PM2 hatte sie also mit dem Booten wieder gestartet.

4️⃣ Nginx Reverse Proxy einrichten

Da die App Anfragen nur auf 127.0.0.1:3000 annimmt, nimmt Nginx die Anfragen auf Port 80 entgegen und leitet sie an die App weiter. Einen solchen Aufbau, bei dem ein Server vorne Anfragen annimmt und an einen Server dahinter weiterleitet, nennt man Reverse Proxy.

🔲 apt-get install nginx

sudo -E apt-get install -y nginx
Setting up nginx-common (1.24.0-2ubuntu7.19) ...
Created symlink /etc/systemd/system/multi-user.target.wants/nginx.service → /usr/lib/systemd/system/nginx.service.
Setting up nginx (1.24.0-2ubuntu7.19) ...
nginx -v
nginx version: nginx/1.24.0 (Ubuntu)

Das Nginx im Ubuntu 24.04-Repository ist Version 1.24.0 und wird bei der Installation gleich für den Start beim Booten eingetragen.

🔲 /etc/nginx/sites-available/hello

Erstelle die Datei /etc/nginx/sites-available/hello und füge den folgenden Inhalt ein.

server {
    listen 80;
    server_name 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;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

proxy_pass ist die App-Adresse, an die Anfragen weitergegeben werden. Die Header Host, X-Real-IP, X-Forwarded-For und X-Forwarded-Proto sind Werte, die mitgegeben werden, damit die App die Domain der ursprünglichen Anfrage, die IP des Besuchers und http oder https erkennen kann.

proxy_http_version 1.1 und die Header Upgrade und Connection braucht man, um WebSocket-Verbindungen bis zur App durchzureichen. Die Header Upgrade und Connection werden nach einem Proxy nicht mehr an die App weitergegeben, daher setzt man sie direkt in der Konfiguration.

sudo ln -sf /etc/nginx/sites-available/hello /etc/nginx/sites-enabled/hello

Nginx liest nur Konfigurationen, für die im Ordner sites-enabled ein Link angelegt ist.

🔲 nginx -t

sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
sudo systemctl reload nginx

Prüfe zuerst mit nginx -t die Syntax der Konfiguration, und wenn sie passt, übernimmst du die neue Konfiguration mit reload, ohne Nginx anzuhalten.

🔲 curl-Anfrage mit Host-Header

curl -sS -i -H 'Host: example.com' http://127.0.0.1/ | head -n 4
HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)
Date: Sat, 07 Nov 2026 08:09:38 GMT
Content-Length: 16

Nach dem Neuladen der Konfiguration kam auf die Anfrage mit Host-Header die Antwort der App zurück. Die Zeile Server in den Antwort-Headern ist nginx, und Content-Length 16 entspricht der Größe der Zeile hello from node, die die App zurückgibt (16 Byte mit Zeilenumbruch). Für Zugriffe von außerhalb des Servers muss Port 80 in der Firewall offen sein.

🧭 Anfragen an die Node.js-App und Neustart
Erklär-Diagramm
🧭 Situation
Ablauf der Anfrage
🌐BrowserAnfrage an example.com
➜
🧱NginxPort 80, proxy_pass
➜
🟢Node.js-App127.0.0.1:3000
Prozessverwaltung
⚙️systemdDienst pm2-deploy
➜
🔁PM2App-Liste dump.pm2
➜
🟢Node.js-Apphello
Nginx auf Port 80 nimmt die Anfrage des Browsers an, leitet sie an 127.0.0.1:3000 aus proxy_pass weiter und gibt die Antwort der App unverändert zurück.
Endet der App-Prozess, startet PM2 dieselbe App sofort wieder. Nach dem Beenden mit kill -9 zeigte pm2 list 3 Sekunden später die App mit neuer pid als online.
Beim Booten des Servers führt systemd den Dienst pm2-deploy aus, und PM2 startet die Apps nach der mit pm2 save gespeicherten Liste wieder.

5️⃣ HTTPS mit Certbot einrichten

Mit dem Nginx-Plugin von Certbot kannst du das Zertifikat ausstellen und die Nginx-Konfiguration in einem Schritt ändern. Führe den Befehl unten aus, wenn die Domain bereits auf die Server-IP zeigt.

sudo certbot --nginx

Die Installation von Certbot und den Test der automatischen Erneuerung findest du im Beitrag unten; der Ausstellungsbefehl dort ist für Apache, ersetze also nur den Schritt der Ausstellung durch den Befehl oben.

🔢 FAQ & empfohlene Inhalte

Kann Nginx die in proxy_pass eingetragene App-Adresse nicht erreichen, gibt es 502 Bad Gateway zurück. Nachdem die App mit pm2 stop hello angehalten war, ergab dieselbe Anfrage 502, und in /var/log/nginx/error.log stand die Zeile [connect() failed (111: Connection refused) while connecting to upstream] mit der Adresse, zu der verbunden werden sollte, http://127.0.0.1:3000/. Prüfe zuerst in pm2 list, ob die App online ist und ob der Port, auf dem die App Anfragen annimmt, zur Adresse in proxy_pass passt. Nach dem Neustart der App kam dieselbe Anfrage mit 200 zurück.

PM2 speichert die Ausgabe der App als Dateien pro App-Name im Ordner .pm2/logs unter dem Home-Ordner des Kontos, das sie gestartet hat. pm2 logs hello --nostream --lines 5 zeigte die letzten Zeilen der beiden Dateien /home/deploy/.pm2/logs/hello-out.log und hello-error.log, und in der out-Datei stand die von der App protokollierte Anfrage GET /. Ohne --nostream werden neu eingehende Logs fortlaufend ausgegeben.

Den Autostart-Dienst entfernst du mit pm2 unstartup systemd. Nachdem du Apps hinzugefügt oder entfernt hast, führst du pm2 save erneut aus, damit beim Booten die geänderte Liste gilt. Auch beim Ausführen von pm2 startup wurden diese beiden Befehle am Ende der Ausgabe genannt.

ℹ️ Affiliate-Hinweis
Die Inhalte dieser Website enthalten Affiliate-Links. Kauft ein Besucher über einen dieser Links ein Produkt oder eine Dienstleistung, erhält die Website vom Verkäufer eine Provision. Der Betrag, den der Käufer zahlt(bei Aktionsrabatten sinkt er ↓), steigt dadurch nicht. Angegebene Preise, Rabatte und Lagerbestände entsprechen dem Stand bei Erstellung und können abweichen; prüfe sie daher vor dem Kauf beim Verkäufer. Produkte werden nach eigenen Kriterien ausgewählt und bewertet, und Provisionen beeinflussen weder die Reihenfolge noch den Inhalt der Bewertungen.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Beweisen Sie, dass Sie ein Mensch sind: 4   +   6   =