Deploy Node.js to a VPS with PM2 and Nginx from A to Z
A practical Node.js deployment guide for Ubuntu using PM2 and Nginx, including builds, environment variables, HTTPS, logs, health checks, and rollback.
Article by Phạm Minh Thiện
Front-end developer working directly with Next.js, NestJS, Docker, Nginx, and Ubuntu VPS deployments.
Reviewed on 7/21/2026.
Table of contents
PM2 keeps a Node.js process alive and Nginx handles the public HTTP layer. Together they make a simple production setup for an API, server-rendered application, or Node.js backend.
This guide uses /var/www/myapp, port 3000, and example.com as examples.
1. Prepare the server
sudo apt update && sudo apt upgrade -y
sudo apt install -y nginx git curl
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
Install the Node.js version used by your team and keep it aligned with package.json and CI.
2. Install and build the application
sudo mkdir -p /var/www/myapp
sudo chown -R "$USER":"$USER" /var/www/myapp
cd /var/www/myapp
git clone https://github.com/your-org/your-app.git .
npm ci
npm run build
Use npm ci when a lockfile exists so production dependencies are reproducible.
3. Keep runtime configuration outside Git
Create a protected .env.production file:
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://app_user:change-me@127.0.0.1:5432/app
chmod 600 .env.production
Confirm which variables are required at build time and which are read only when the process starts.
4. Start Node.js with PM2
For an entry point such as dist/server.js:
npm install --global pm2
pm2 start dist/server.js --name myapp --time
pm2 save
pm2 startup systemd
Run the command printed by pm2 startup, then verify:
pm2 status
pm2 logs myapp --lines 100
curl http://127.0.0.1:3000/health
For an npm start script, use pm2 start npm --name myapp -- start and document the exact command.
5. Configure Nginx as a reverse proxy
Create /etc/nginx/sites-available/myapp:
server {
listen 80;
listen [::]:80;
server_name example.com www.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;
}
}
Enable and validate it before reloading:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/myapp
sudo nginx -t
sudo systemctl reload nginx
6. Add HTTPS
After DNS points to the VPS and HTTP works, issue a Let’s Encrypt certificate:
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
sudo certbot renew --dry-run
7. Make deployments repeatable
A safe release fetches a known revision, installs locked dependencies, builds outside the running process, runs migrations deliberately, reloads PM2, calls /health, and rolls back when the health check fails.
git fetch --tags origin
git checkout v1.2.3
npm ci
npm run build
pm2 reload myapp --update-env
curl --fail https://example.com/health
Do not make an irreversible database migration part of a release until its rollback impact is understood.
Troubleshooting checklist
502 Bad Gateway: inspectpm2 status,pm2 logs, andcurl http://127.0.0.1:3000.EADDRINUSE: another process owns port 3000; inspect it withss -ltnp.- The wrong site is served: verify
server_name, DNS, and enabled Nginx sites. - The app dies after reboot: run
pm2 saveand completepm2 startup. - Environment values are missing: check the working directory and the framework’s env loading rules.
Keep the operational commands in the project README so another person can deploy, diagnose, and recover the service without guesswork.
Continue reading
Related articles
Production VPS Monitoring: Logs, CPU, RAM, Disk, and Health Checks
Monitor a production VPS with journald, Nginx logs, PM2, Docker health checks, CPU, RAM, disk, uptime, and actionable alerts.
Read article →Point a Domain to a VPS with DNS, Cloudflare, and Nginx
Point a domain to an Ubuntu VPS with A, AAAA, and CNAME records, verify DNS propagation, configure Cloudflare and Nginx, and fix www issues.
Read article →GitHub Actions CI/CD: Automatically Deploy Node.js to a VPS
Create a secure GitHub Actions pipeline that builds, tests, and deploys Node.js to a VPS over SSH with secrets, health checks, concurrency, and rollback.
Read article →