PM2 + Nginx reverse proxy: een Node.js-app aan poort 80 koppelen
Op een Ubuntu 24.04-server hebben we een Node.js PM2 Nginx reverse proxy opgezet: PM2 voert de app uit en Nginx geeft verzoeken die op poort 80 binnenkomen door aan de app. We hebben ook gecontroleerd dat PM2 de app opnieuw start, zelfs als het app-proces wordt beëindigd of de server opnieuw opstart.
De resultaatvakken hieronder tonen de uitvoer van opdrachten die zijn uitgevoerd met de gewone gebruiker deploy. Op de plek van het domein staat de voorbeeldwaarde example.com, en de verzoeken zijn binnen de server verstuurd met een Host-header.
1️⃣ Node.js installeren
Node.js wordt geïnstalleerd door de NodeSource-repository aan apt toe te voegen, en we kozen Node.js 26, de LTS-versie per november 2026.
🔲 NodeSource setup_26.x
export DEBIAN_FRONTEND=noninteractiveAls je deze waarde eerst instelt, verschijnt er tijdens de installatie van pakketten geen venster dat om instellingen vraagt. De optie -E van het sudo-commando hieronder geeft deze waarde ook door aan het commando dat met root-rechten draait.
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 -yZodra het script de repository heeft toegevoegd, kun je Node.js net als een Ubuntu-pakket met apt installeren.
🔲 apt-get install nodejs
sudo -E apt-get install -y nodejsSetting 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 -vv26.13.0npm -v11.20.0npm zit in het pakket nodejs, dus je hoeft het niet apart te installeren.
2️⃣ Node.js-test-app
Om te zien of PM2 en Nginx goed gekoppeld zijn, maak je een test-app die één regel tekst teruggeeft wanneer er een verzoek binnenkomt.
🔲 app.js
mkdir -p ~/helloMaak onder je thuismap de map hello aan en schrijf daarin het bestand app.js.
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');
APPDe app accepteert alleen verzoeken op poort 3000 van 127.0.0.1, dus van buiten de server is hij niet direct te openen. Nginx stellen we zo in dat het verzoeken van buiten ontvangt en aan deze poort doorgeeft.
3️⃣ App uitvoeren met PM2 en automatisch starten
PM2 is een procesbeheerder die Node.js-apps op de achtergrond uitvoert en een app opnieuw start wanneer het proces stopt. In dit deel stellen we ook het automatisch starten in, dat de app weer opstart wanneer de server opstart.
🔲 npm install -g pm2
sudo npm install -g pm2added 45 packages in 5spm2 -v[PM2] Spawning PM2 daemon with pm2_home=/home/deploy/.pm2
[PM2] PM2 Successfully daemonized
7.1.2Bij het eerste pm2-commando start ook de PM2-daemon, en de geïnstalleerde PM2 is versie 7.1.2.
🔲 pm2 start
cd ~/helloGa naar de map met app.js en voer de app daarna uit met PM2.
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 is de app-naam die in de PM2-lijst verschijnt, en latere commando's wijzen de app met deze naam aan.
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/sonline in de kolom status betekent dat de app draait, en de kolom ↺ is het aantal keren dat PM2 de app opnieuw heeft gestart.
🔲 curl 127.0.0.1:3000
Als je binnen de server een verzoek verstuurt, komt de regel tekst die de app teruggeeft ongewijzigd terug.
curl -sS http://127.0.0.1:3000/hello from node🔲 pm2 restart
Nadat je de app-code hebt aangepast, herstart je de app om de wijziging toe te passen.
pm2 restart helloUse --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/spm2 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/sNa de herstart veranderde de pid van 17257 naar 17306 en ging de kolom ↺ naar 1.
🔲 kill -9
Een plotselinge stop van het app-proces kun je nabootsen met kill -9, en pm2 pid toont het procesnummer van de app.
kill -9 "$(pm2 pid hello)"Controleer de PM2-lijst na 3 seconden opnieuw.
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/sPM2 had de app opnieuw gestart met de nieuwe pid 17797 en hij was online, en de kolom ↺ was naar 2 gegaan.
🔲 pm2 save
pm2 save[PM2] Saving current process list...
[PM2] Successfully saved in /home/deploy/.pm2/dump.pm2pm2 save slaat de lijst met draaiende apps op in het bestand dump.pm2, en bij het opstarten start PM2 de apps opnieuw volgens deze lijst.
🔲 pm2 startup
Om PM2 bij het opstarten te laten draaien, registreer je PM2 als systemd-service. Het commando hieronder maakt de service aan met de naam van het huidige account en de thuismap.
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 is het deel waarmee pm2, uitgevoerd met root-rechten, het node-pad van het huidige account vindt, en -u en --hp zijn het account en de thuismap waarmee de service draait. De regel ExecStart in het servicebestand is pm2 resurrect, dat bij het opstarten de opgeslagen app-lijst terugzet.
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 betekent dat de service bij het opstarten draait. pm2 startup registreert de service alleen en start hem nu niet, daarom staat er inactive (dead), en de service draait vanaf de volgende keer opstarten.
🔲 pm2 list na herstart
sudo systemctl rebootMaak na ongeveer 2 minuten opnieuw verbinding als deploy en controleer de status van de app.
uptime -pup 2 minutespm2 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/sTwee minuten nadat de server was opgestart, was de app al 108 seconden online: PM2 had de app samen met het opstarten weer gestart.
4️⃣ Nginx reverse proxy instellen
Omdat de app alleen verzoeken op 127.0.0.1:3000 accepteert, ontvangt Nginx de verzoeken die op poort 80 binnenkomen en geeft ze door aan de app. Zo'n opzet, waarbij een server vooraan verzoeken ontvangt en doorstuurt naar een server erachter, heet een reverse proxy.
🔲 apt-get install nginx
sudo -E apt-get install -y nginxSetting 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 -vnginx version: nginx/1.24.0 (Ubuntu)De Nginx uit de Ubuntu 24.04-repository is versie 1.24.0, en bij de installatie wordt hij meteen ingesteld om bij het opstarten te draaien.
🔲 /etc/nginx/sites-available/hello
Maak het bestand /etc/nginx/sites-available/hello aan en zet de onderstaande inhoud erin.
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 is het app-adres waaraan verzoeken worden doorgegeven. De headers Host, X-Real-IP, X-Forwarded-For en X-Forwarded-Proto zijn waarden die worden meegegeven zodat de app het domein van het oorspronkelijke verzoek, het IP van de bezoeker en http of https kan kennen.
proxy_http_version 1.1 en de headers Upgrade en Connection zijn nodig om WebSocket-verbindingen tot aan de app door te geven. De headers Upgrade en Connection gaan na een proxy niet meer naar de app, daarom zet je ze direct in de configuratie.
sudo ln -sf /etc/nginx/sites-available/hello /etc/nginx/sites-enabled/helloNginx leest alleen configuraties waarvoor een link in de map sites-enabled staat.
🔲 nginx -t
sudo nginx -tnginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successfulsudo systemctl reload nginxControleer eerst de syntaxis van de configuratie met nginx -t, en pas daarna bij succes de nieuwe configuratie toe met reload zonder Nginx te stoppen.
🔲 curl-verzoek met Host-header
curl -sS -i -H 'Host: example.com' http://127.0.0.1/ | head -n 4HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)
Date: Sat, 07 Nov 2026 08:09:38 GMT
Content-Length: 16Nadat de configuratie opnieuw was ingelezen, kreeg het verzoek met Host-header het antwoord van de app terug. De regel Server in de antwoordheaders is nginx, en Content-Length 16 is even groot als de regel hello from node die de app teruggeeft (16 bytes inclusief regeleinde). Om van buiten de server verbinding te maken, moet poort 80 open staan in de firewall.
5️⃣ HTTPS toepassen met Certbot
Met de Nginx-plug-in van Certbot kun je het certificaat aanvragen en de Nginx-configuratie in één keer aanpassen. Voer het commando hieronder uit wanneer het domein al naar het IP van de server wijst.
sudo certbot --nginxDe installatie van Certbot en de test van automatisch vernieuwen staan in het artikel hieronder; het aanvraagcommando in dat artikel is voor Apache, dus vervang alleen de aanvraagstap door het commando hierboven.
🔢 FAQ & aanbevolen content
Servicelogs
journalctl-commando: gebruik, logs per service en de journaalgrootte opschonen
Artikel over hoe je de logs van systemd-services zoals nginx en pm2-deploy per service leest.
ℹ️ Affiliatemelding
De content van deze site bevat affiliatelinks. Koopt een bezoeker via een van deze links een product of dienst, dan ontvangt deze site een commissie van de verkoper. Het bedrag dat de koper betaalt (bij acties gaat het omlaag ↓) wordt daardoor niet hoger. De vermelde prijzen, kortingen en voorraad gelden op het moment van schrijven en kunnen afwijken; controleer ze daarom vóór je aankoop bij de verkoper. Producten worden gekozen en beoordeeld volgens onze eigen criteria, en commissies hebben geen invloed op de volgorde of de inhoud van de beoordelingen.