Skip to content
KhaiziNam Blog KhaiziNam Blog
Go back
Đọc bằng tiếng Việt

How to Deploy Node.js and React Apps on Linux Hosting with Nginx Reverse Proxy (PM2 + SSL Guide 2025)

You built a Node.js API or React app and now you’re staring at a blank Linux VPS wondering how to actually get it live. Uploading files to shared hosting won’t work here — Node.js needs a process manager, a reverse proxy, and proper server configuration to run reliably in production. This guide walks you through the complete deployment stack: PM2 to keep your app alive, Nginx as a reverse proxy with proxy_pass, SSL via Let’s Encrypt, and a clean setup for React static builds — all from scratch on Ubuntu.

Table of Contents

Install curl if not present

sudo apt install -y curl git build-essential

Add NodeSource repository for Node.js 20 LTS

curl -fsSL https://deb.nodesource.com/setup\_20.x | sudo -E bash -

Install Node.js and npm

sudo apt install -y nodejs

Verify installation

node -v # should show v20.x.x npm -v # should show 10.x.x

If you manage multiple Node.js projects requiring different versions, consider installing NVM (Node Version Manager) instead. It lets you switch between Node versions per project without reinstalling:

# Install NVM curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash source ~/.bashrc

Install and use Node.js 20

nvm install 20 nvm use 20 nvm alias default 20

Step 2 — Set Up PM2 Process Manager

PM2 is the industry standard process manager for Node.js in production. Without it, your app dies the moment your SSH session ends or the server reboots. Install it globally:

sudo npm install -g pm2

Clone your application and start it with PM2:

# Clone your repository cd /var/www git clone https://github.com/yourusername/your-node-app.git cd your-node-app

Install dependencies

npm install —production

Start app with PM2 (replace app.js with your entry file)

pm2 start app.js —name “my-node-app”

Or for an Express app with specific port

PORT=3000 pm2 start app.js —name “my-node-app”

Save PM2 process list

pm2 save

Configure PM2 to start on system boot

pm2 startup

Copy and run the command PM2 outputs (it looks like:)

sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u youruser —hp /home/youruser

Essential PM2 commands you’ll use daily:

pm2 list # List all running processes pm2 logs my-node-app # Tail logs in real time pm2 restart my-node-app # Restart after code changes pm2 stop my-node-app # Stop the process pm2 delete my-node-app # Remove from PM2 list pm2 monit # Real-time CPU/memory dashboard

For production apps, use PM2 cluster mode to utilize all CPU cores:

# Start with cluster mode — spawns one instance per CPU core pm2 start app.js —name “my-node-app” -i max

Or specify exact number of instances

pm2 start app.js —name “my-node-app” -i 4

Step 3 — Install and Configure Nginx

# Install Nginx sudo apt install -y nginx

Enable Nginx to start on boot

sudo systemctl enable nginx

Start Nginx

sudo systemctl start nginx

Verify it’s running

sudo systemctl status nginx

Configure firewall — allow HTTP, HTTPS, and SSH

sudo ufw allow ssh sudo ufw allow ‘Nginx Full’ sudo ufw enable sudo ufw status

Nginx configuration lives in two directories on Ubuntu:

This separation lets you draft and test configurations without activating them.

Step 4 — Configure Nginx proxy_pass for Node.js API

Create a new Nginx config file for your Node.js application. Replace yourdomain.com with your actual domain and 3000 with the port your app runs on:

sudo nano /etc/nginx/sites-available/yourdomain.com

Paste the following configuration:

server { listen 80; server_name yourdomain.com www.yourdomain.com;

# Logging
access\_log /var/log/nginx/yourdomain.access.log;
error\_log  /var/log/nginx/yourdomain.error.log;

location / {
    proxy\_pass         http://127.0.0.1:3000;
    proxy\_http\_version 1.1;

    # Required for WebSocket support
    proxy\_set\_header Upgrade    $http\_upgrade;
    proxy\_set\_header Connection 'upgrade';

    # Pass real client info to Node.js app
    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\_cache\_bypass $http\_upgrade;
    proxy\_redirect     off;

    # Timeouts — increase for long-running requests
    proxy\_read\_timeout  240s;
    proxy\_send\_timeout  240s;
    proxy\_connect\_timeout 75s;
}

}

Enable the site and reload Nginx:

# Create symlink to enable the site sudo ln -s /etc/nginx/sites-available/yourdomain.com /etc/nginx/sites-enabled/

Test configuration syntax — always do this before reloading

sudo nginx -t

If test passes, reload Nginx

sudo systemctl reload nginx

Your Node.js app is now accessible at http://yourdomain.com. The proxy_pass http://127.0.0.1:3000 line is the core of the reverse proxy — all traffic to port 80 is forwarded to your Node.js process on port 3000 internally.

Why use 127.0.0.1 instead of localhost in proxy_pass?

Using 127.0.0.1 forces IPv4 resolution and avoids a subtle bug where systems with IPv6 enabled resolve localhost to ::1 (IPv6 loopback) while your Node.js app only listens on IPv4 — resulting in a connection refused error even though both Nginx and Node.js are running correctly.

Step 5 — Deploy React Build as Static Files with Nginx

React (and other SPA frameworks like Vue, Next.js static export) should be served as pre-built static files — not proxied through Node.js. This is significantly faster and puts zero load on your application server.

Build your React app locally or on the server:

# On your server or in your CI pipeline cd /var/www/your-react-app npm install npm run build

Output is in /var/www/your-react-app/build (CRA) or /dist (Vite)

Create an Nginx config to serve the static build and optionally proxy API calls to your Node.js backend:

sudo nano /etc/nginx/sites-available/app.yourdomain.com

server { listen 80; server_name app.yourdomain.com;

# Root directory — point to your React build output
root /var/www/your-react-app/build;
index index.html;

access\_log /var/log/nginx/react-app.access.log;
error\_log  /var/log/nginx/react-app.error.log;

# Serve React app — handle client-side routing
# Without this, refreshing on /dashboard returns 404
location / {
    try\_files $uri $uri/ /index.html;
}

# Proxy /api requests to Node.js backend
# React app calls /api/users -> forwarded to Node.js on port 3000
location /api {
    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\_cache\_bypass $http\_upgrade;
}

# Cache static assets aggressively
location ~\* \\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
    expires    1y;
    add\_header Cache-Control "public, immutable";
}

}

The try_files $uri $uri/ /index.html directive is critical for React Router — without it, navigating directly to /dashboard returns a 404 because Nginx looks for a file called dashboard on disk and finds nothing. This line tells Nginx: if the file doesn’t exist, serve index.html and let React Router handle the routing client-side.

Step 6 — Enable HTTPS with Let’s Encrypt (Certbot)

Never run production apps over plain HTTP. Let’s Encrypt provides free SSL certificates via Certbot, which also auto-configures Nginx:

# Install Certbot and Nginx plugin sudo apt install -y certbot python3-certbot-nginx

Allow HTTPS through firewall

sudo ufw allow ‘Nginx Full’

Issue certificate and auto-configure Nginx

Replace with your actual domain(s)

sudo certbot —nginx -d yourdomain.com -d www.yourdomain.com

Follow the prompts:

- Enter email for renewal notifications

- Agree to terms of service

- Choose redirect option (2) to force all HTTP to HTTPS

Certbot automatically modifies your Nginx config to add HTTPS and set up HTTP-to-HTTPS redirect. Your config will gain a new server block listening on port 443 with the SSL certificate paths.

Certificates are valid for 90 days. Certbot installs a systemd timer that auto-renews before expiry. Test auto-renewal with:

sudo certbot renew —dry-run

Step 7 — Host Multiple Node.js Apps on One Server

One of Nginx’s biggest advantages: run many apps on the same server, each on a different domain or subdomain, each on a different internal port. Create a separate config file per app:

# App 1 — API on port 3000 sudo nano /etc/nginx/sites-available/api.yourdomain.com

App 2 — Admin panel on port 4000

sudo nano /etc/nginx/sites-available/admin.yourdomain.com

App 3 — React frontend (static)

sudo nano /etc/nginx/sites-available/app.yourdomain.com

Example for running two separate Node.js APIs:

# /etc/nginx/sites-available/api.yourdomain.com server { listen 80; server_name api.yourdomain.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_cache_bypass $http_upgrade; } }

/etc/nginx/sites-available/admin.yourdomain.com

server { listen 80; server_name admin.yourdomain.com; location / { proxy_pass http://127.0.0.1:4000; 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_cache_bypass $http_upgrade; } }

Enable both and reload:

sudo ln -s /etc/nginx/sites-available/api.yourdomain.com /etc/nginx/sites-enabled/ sudo ln -s /etc/nginx/sites-available/admin.yourdomain.com /etc/nginx/sites-enabled/ sudo nginx -t && sudo systemctl reload nginx

Common Errors and How to Fix Them

FAQ

Q: Do I need PM2 if I’m using Docker?

A: No. Docker handles process lifecycle — if your container crashes, Docker restarts it (with --restart unless-stopped). Inside a Docker container, run Node.js directly as the main process (CMD ["node", "app.js"]). PM2 is for bare-metal or VPS deployments without containerization.

Q: Can I deploy a Next.js app the same way?

A: Yes, with one distinction. Next.js in production runs as a Node.js server (npm run start on port 3000 by default) — proxy_pass to it exactly like any Node.js app. If you use next export for a fully static build, serve it like the React static example above. For App Router with server components, always use the Node.js server mode.

Q: How do I update my app without downtime?

A: Pull new code, install dependencies, then use PM2 reload (not restart) for zero-downtime deploys: git pull && npm install --production && pm2 reload my-node-app. PM2 reload gracefully cycles workers one by one, keeping the app serving traffic throughout.

Q: What port should my Node.js app listen on?

A: Any port above 1024 that isn’t already in use — 3000, 4000, 5000, 8000 are all common choices. Check occupied ports with sudo ss -tlnp. The external port (80/443) is handled entirely by Nginx — your Node.js app never needs to know about it.

Q: How do I set environment variables for production?

A: Create a .env file in your app directory and use dotenv in your app, or pass variables directly via PM2 ecosystem file. Create ecosystem.config.js in your project root and run pm2 start ecosystem.config.js. This keeps environment config separate from application code and easy to manage per environment.

Wrapping Up — Your Deployment Stack Is Now Production-Ready

You now have the complete deployment stack: Node.js running under PM2, Nginx as a reverse proxy handling all public traffic via proxy_pass, HTTPS enforced by Let’s Encrypt, React static assets served directly by Nginx, and the ability to host multiple apps on a single server. This is the same stack used by the vast majority of Node.js deployments in production worldwide.

Your next step: Set up a simple deployment script — a shell script or GitHub Action that SSH’s into your server, pulls latest code, runs npm install, and calls pm2 reload. Automating deployment from day one eliminates human error and makes updates take seconds instead of minutes. Check out how to set up CI/CD deployment for Node.js with GitHub Actions for the complete automation guide.

See more: Deploy PHP, Laravel, CodeIgniter on Linux VPS with Nginx


Share this post:

Related Posts

Secrets to Deploying React/Node.js on a 1GB RAM VPS with PM2: Never Run Out of Memory 2026

For web developers, owning a 1GB RAM VPS at a low cost is an ideal starting point. However, in practice, deployment often turns into a "nightmare" when applications constantly crash for no apparent reason. The core of the problem lies in the following two key factors:

Deploy PHP, Laravel, CodeIgniter on Linux VPS with Nginx + MySQL Docker + SSL 2026

You just spun up a Linux VPS and you're staring at a blank terminal not knowing where to start? Shared hosting works fine for simple PHP — but the moment you need to control PHP versions, configure MySQL your way, or run a Laravel queue worker, shared hosting falls short.

Optimizing Project Environment Setup Time with Docker for Low-Config VPS

How to optimize project environment setup time with Docker for a 2-Core 4GB RAM VPS. Guide to the smoothest Nginx, PHP, MySQL, and Redis configuration. Check it out now!

IT Fresher & Junior Salary 2026: PHP, Node.js, React, Flutter - Real Market Data

Real salary benchmarks for IT freshers and junior developers in Vietnam in 2026, broken down by tech stack - so you know exactly what number to quote in interviews, or whether you're being underpaid right now.

IT Salary Levels in Ho Chi Minh City and Hanoi (2025 - 2026 Overview)

The IT industry in Vietnam continues to maintain a higher salary range compared to many other industries, especially in Ho Chi Minh City and Hanoi. These two cities are currently the country’s largest technology recruitment hubs, attracting outsourcing companies, product-based firms, fintech businesses, AI startups, and international tech corporations.

Salary Negotiation for Fresh IT Graduates: Stop Leaving Money on the Table

Salary negotiation for IT freshers is the process of discussing and adjusting your compensation after receiving a job offer — a step that most new graduates skip entirely because they believe "freshers have no bargaining power." The reality is the opposite: nearly every company builds a negotiation buffer into their offers even for entry-level candidates, and not negotiating means you're leaving m

Junior Developer Portfolio: What to Include to Get Interview Calls

A junior developer portfolio is the collection of real projects, skills, and professional information you present so employers can evaluate your abilities — substituting for the work experience section of your CV that's currently empty. For freshers and junior developers, a portfolio isn't something that's "nice to have" — it's the only evidence you can offer to prove you can actually build things

How to Write a Junior Developer Cover Letter With No Experience

Learn how to write a junior developer cover letter with no experience, including practical templates, real examples, common mistakes, and tips to pass the CV screening round.

I Failed My IT Job Interview — Here's What I Changed to Get Hired

Getting rejected from an IT job interview is something almost every fresher and junior developer goes through at least once — usually more. That first rejection can make you question your abilities, but in the vast majority of cases, interview failures have specific, identifiable, fixable causes that can be addressed in a matter of weeks if you know where to look.

Body Language in Tech Interviews: 7 Mistakes That Cost You the Offer

Body language in tech interviews refers to the non-verbal signals — posture, eye contact, hand gestures, and vocal tone — that recruiters observe alongside your technical answers. Mastering these signals helps you project confidence and professionalism from the very first second you walk into the room.


Previous Post
How to Introduce Yourself in an IT Job Interview — Script Templates for Freshers and Junior Developers
Next Post
Session vs JWT: The Complete Theory and Most Common Interview Questions Every Junior Developer Must Know