Eine Node-Anwendung kann HTTP selbst ausliefern. Sie sollte es nicht: Sie müsste TLS terminieren, statische Dateien ausliefern, einen Absturz überleben und als root an Port 443 binden. Nginx davor macht alle vier, und die Anwendung muss dann nur noch eine Anwendung sein.

Der Proxy

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name app.example.com;

    ssl_certificate     /etc/letsencrypt/live/app.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        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;
    }
}

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

Das Paar Upgrade und Connection ist es, was WebSockets funktionieren lässt. Ohne sie wird die Verbindung weitergereicht und dann fallengelassen, was wie eine kaputte Anwendung aussieht.

Node nur an localhost binden

app.listen(3000, '127.0.0.1');
An 0.0.0.0 zu binden lässt Port 3000 aus dem Internet erreichbar, vorbei an jeder Regel, die Sie in Nginx geschrieben haben. Das ist der häufigste Fehler in diesem Aufbau.

Mit systemd am Laufen halten, nicht mit einem Terminal

# /etc/systemd/system/app.service
[Unit]
Description=Node app
After=network.target

[Service]
Type=simple
User=app
WorkingDirectory=/var/www/app
EnvironmentFile=/etc/app.env
ExecStart=/usr/bin/node server.js
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo systemctl enable --now app
journalctl -u app -f

Dem Proxy vertrauen - in der Anwendung

Hinter Nginx sieht Ihre App bei jeder Anfrage die Adresse 127.0.0.1. Sagen Sie dem Framework, den weitergereichten Header zu lesen - in Express app.set('trust proxy', 1) -, sonst sind jedes Log und jede Ratenbegrenzung sinnlos.

Liefern Sie statische Dateien mit einem location-Block aus Nginx aus statt durch Node. Das ist schneller und hält die Event-Loop frei für die Arbeit, die nur Ihr Code tun kann.