A Node application can serve HTTP by itself. It should not: it would have to terminate TLS, serve static files, survive a crash and bind to port 443 as root. Nginx in front does all four, and the application then only has to be an application.

The proxy

server {\n    listen 443 ssl;\n    listen [::]:443 ssl;\n    server_name app.example.com;\n\n    ssl_certificate     /etc/letsencrypt/live/app.example.com/fullchain.pem;\n    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;\n\n    location / {\n        proxy_pass http://127.0.0.1:3000;\n        proxy_http_version 1.1;\n        proxy_set_header Upgrade $http_upgrade;\n        proxy_set_header Connection $connection_upgrade;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n    }\n}\n\nmap $http_upgrade $connection_upgrade {\n    default upgrade;\n    ''      close;\n}

The Upgrade and Connection pair is what makes WebSockets work. Without them the connection is proxied and then dropped, which looks like a broken application.

Bind Node to localhost only

app.listen(3000, '127.0.0.1');
Binding to 0.0.0.0 leaves port 3000 reachable from the internet, past every rule you wrote in Nginx. It is the most common mistake in this setup.

Keep it running with systemd, not with a terminal

# /etc/systemd/system/app.service\n[Unit]\nDescription=Node app\nAfter=network.target\n\n[Service]\nType=simple\nUser=app\nWorkingDirectory=/var/www/app\nEnvironmentFile=/etc/app.env\nExecStart=/usr/bin/node server.js\nRestart=always\nRestartSec=5\n\n[Install]\nWantedBy=multi-user.target
sudo systemctl enable --now app\njournalctl -u app -f

Trust the proxy, in the application

Behind Nginx, the address your app sees is 127.0.0.1 on every request. Tell the framework to read the forwarded header - in Express, app.set('trust proxy', 1) - or every log and every rate limit is meaningless.

Serve static files from Nginx with a location block rather than through Node. It is faster, and it leaves the event loop free for the work only your code can do.