There was a problem loading the comments.

How to Deploy a Node.js Application on a VPS or Dedicated server (Ubuntu 24.04 and AlmaLinux 9) - Complete Go-Live Tutorial

Support Portal  »  Knowledgebase  »  Viewing Article

  Print

This tutorial takes a blank Ubuntu 24.04 or AlmaLinux 9 server to a live, HTTPS-secured Node.js application. You will install Node.js, set up the PM2 process manager, upload your app from your local machine, put Nginx in front as a reverse proxy, open the firewall, and add a free Let's Encrypt certificate. Distro-specific commands are labelled at each step.


How the production stack fits together

On a bare server there is no control panel doing the wiring, so you assemble the pieces yourself. The standard, reliable layout is:

Internet ──► Nginx (ports 80/443, TLS) ──► Node.js app (127.0.0.1:3000) ──► database / cache
  • Nginx faces the internet, terminates HTTPS, and reverse-proxies requests to your app.
  • Your Node.js app listens only on localhost (127.0.0.1), never on the public interface.
  • PM2 keeps the app running, restarts it on crash, and brings it back after a reboot.

Before you start

  • A fresh VPS or dedicated server running Ubuntu 24.04 LTS or AlmaLinux 9.
  • Login as a non-root user with sudo privileges. Do not run the application as root.
  • A domain name with an A record already pointing to your server's IP address. This must be in place before requesting a certificate.
  • Your application code in a git repository or on your local machine, with a valid package.json and a package-lock.json.

Replace example.com, youruser, and the app name myapp with your own values throughout.


Step 1: Update the server

Ubuntu 24.04

sudo apt update && sudo apt upgrade -y

AlmaLinux 9

sudo dnf update -y


Step 2: Install Node.js 22 LTS

We install from the NodeSource repository, which tracks upstream LTS releases more closely than the distribution's own packages. To use Node.js 24 instead, change 22 to 24 in the commands below.

Ubuntu 24.04

sudo apt install -y ca-certificates curl gnupg
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
NODE_MAJOR=22
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
sudo apt update
sudo apt install -y nodejs

AlmaLinux 9

sudo dnf module reset nodejs -y
curl -fsSL https://rpm.nodesource.com/setup_22.x | sudo bash -
sudo dnf install -y nodejs

The dnf module reset line prevents AlmaLinux's built-in AppStream module from overriding the NodeSource package. If node -v later shows an unexpected version, run sudo dnf module disable nodejs -y and reinstall.

Confirm both distributions:

node -v
npm -v

You should see v22.x.x and a matching npm version.


Step 3: Install the PM2 process manager

sudo npm install -g pm2

PM2 runs the same way on both distributions. It keeps your app alive and manages logs and restarts.


Step 4: Prepare the application directory

sudo mkdir -p /var/www/myapp
sudo chown -R youruser:youruser /var/www/myapp

Owning the directory as your non-root user means you can deploy without sudo and the app runs under an unprivileged account.


Step 5: Upload your app from your local machine

Choose one of these methods. Whichever you use, do not upload the node_modules folder. Native modules must be compiled on the server, so you install dependencies there in the next step.

Option A: rsync (recommended for direct uploads). Run this from your local machine, in your project folder:

rsync -avz --exclude 'node_modules' --exclude '.git' --exclude '.env' \
  ./ youruser@YOUR_SERVER_IP:/var/www/myapp/

Option B: git (recommended for ongoing deployments). Run this on the server:

cd /var/www
git clone https://github.com/youraccount/myapp.git myapp
cd myapp

Option C: scp (simplest one-off). Run this from your local machine:

scp -r ./ youruser@YOUR_SERVER_IP:/var/www/myapp/

Now, on the server, install production dependencies from the lockfile and run any build step your app needs (for example a React or Next.js build):

cd /var/www/myapp
npm ci --omit=dev
# npm run build   # only if your app has a build step


Step 6: Configure the app to listen on localhost

Your app must bind to 127.0.0.1 so it is reachable only through Nginx, not directly from the internet. Confirm your startup file reads the port from the environment and binds to localhost:

const port = process.env.PORT || 3000;
app.listen(port, '127.0.0.1', () => {
  console.log('App listening on 127.0.0.1:' + port);
});

If you use environment variables, create a .env file in the app directory (never commit it to git) or set them in the PM2 config in the next step.


Step 7: Start the app with PM2 and enable boot startup

cd /var/www/myapp
NODE_ENV=production pm2 start app.js --name myapp
pm2 startup

The pm2 startup command prints a single sudo env ... command. Copy that command, run it exactly as printed, then save the current process list so it is restored on reboot:

pm2 save

Check the app is running:

pm2 status
curl -I http://127.0.0.1:3000


Step 8: Install and configure Nginx as a reverse proxy

Ubuntu 24.04

sudo apt install -y nginx

AlmaLinux 9

sudo dnf install -y nginx
sudo systemctl enable --now nginx

On both distributions, create the site configuration file /etc/nginx/conf.d/myapp.conf:

server {
    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 Upgrade $http_upgrade;
        proxy_set_header 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;
        proxy_cache_bypass $http_upgrade;
    }
}

The Upgrade and Connection headers allow WebSocket connections to pass through. On Ubuntu, remove the default site so it does not intercept requests:

sudo rm -f /etc/nginx/sites-enabled/default

Test the configuration and reload on both distributions:

sudo nginx -t
sudo systemctl reload nginx


Step 9: Open the firewall

Ubuntu 24.04 (ufw)

sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status

AlmaLinux 9 (firewalld)

sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload

AlmaLinux 9 only: allow Nginx to make the proxy connection. SELinux blocks this by default, which produces a 502 error even when everything else is correct. Run:

sudo setsebool -P httpd_can_network_connect 1

Only ports 22, 80, and 443 are open to the internet. Your Node.js app on port 3000 stays private on localhost.


Step 10: Secure the site with HTTPS

Ubuntu 24.04

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com

AlmaLinux 9

sudo dnf install -y epel-release
sudo dnf install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com

Certbot obtains the certificate, edits your Nginx config to serve HTTPS, and sets up automatic renewal. Confirm renewal works:

sudo certbot renew --dry-run


Step 11: Verify go-live

curl -I https://example.com

A working deployment returns 200 over HTTP/2 with a valid certificate. Open the site in a browser to confirm the padlock and your application load correctly. Your app is now live.


Tips and optimization

Run in production mode

Always set NODE_ENV=production. It enables framework optimizations and disables verbose debugging. Set it when starting PM2, or in a config file (below).

Use a PM2 ecosystem file

Instead of long command lines, define your app in ecosystem.config.js in the app directory. This makes deployments repeatable and keeps environment variables in one place:

module.exports = {
  apps: [{
    name: 'myapp',
    script: 'app.js',
    instances: 'max',
    exec_mode: 'cluster',
    env: {
      NODE_ENV: 'production',
      PORT: 3000
    }
  }]
};

Start it with pm2 start ecosystem.config.js.


Use all CPU cores with cluster mode

The instances: 'max' and exec_mode: 'cluster' settings above run one worker per CPU core and load-balance across them, which can multiply throughput on multi-core servers. Cluster mode suits stateless apps; if you keep session state in memory, move it to Redis first.


Serve static files through Nginx, not Node

Let Nginx serve images, CSS, JavaScript bundles, and other static assets directly. It is far faster at this than Node and frees your app for real work. Add a location block above the main one:

location /static/ {
    alias /var/www/myapp/public/;
    expires 30d;
    add_header Cache-Control "public, immutable";
}


Enable compression

Turn on gzip in Nginx to shrink responses. Add inside the server block or in the main Nginx config:

gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
gzip_min_length 1024;


Add security headers

Set baseline security headers in the Nginx server block, or use the Helmet middleware in Express:

add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;


Deploy updates with zero downtime

To ship a new version: pull or rsync the code, install dependencies, rebuild if needed, then reload rather than restart. Reload swaps workers gracefully with no dropped requests:

cd /var/www/myapp
git pull        # or rsync from local
npm ci --omit=dev
# npm run build
pm2 reload myapp


Rotate PM2 logs

Without rotation, PM2 logs grow until they fill the disk. Install the log-rotate module once:

pm2 install pm2-logrotate


Add swap on small servers

On servers with under 2 GB of RAM, an npm run build can run out of memory. A small swap file prevents the build from being killed:

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab


Keep the OS patched automatically

Ubuntu: sudo apt install -y unattended-upgrades then sudo dpkg-reconfigure unattended-upgrades. AlmaLinux: sudo dnf install -y dnf-automatic then enable the timer with sudo systemctl enable --now dnf-automatic.timer.


Frequently asked questions

I get "502 Bad Gateway" after configuring Nginx. What is wrong?

Three common causes, in order. First, the app is not running: check pm2 status and curl -I http://127.0.0.1:3000. Second, the port in your Nginx proxy_pass does not match the port your app listens on. Third, on AlmaLinux, SELinux is blocking the proxy connection: run sudo setsebool -P httpd_can_network_connect 1.


Should I upload my node_modules folder?

No. Exclude it from uploads and run npm ci --omit=dev on the server. This installs the exact versions from your lockfile and compiles any native modules for the server's architecture, which a copied folder from your local machine may not match.


Will my app keep running after I log out or the server reboots?

Yes, once PM2 boot startup is configured. After pm2 start, run pm2 startup, execute the command it prints, then run pm2 save. PM2 will relaunch your saved apps automatically on every boot.


Should I use Node.js 22 or 24?

Both are supported LTS releases. Node.js 22 is the conservative default for existing applications. Choose Node.js 24 if your app requires its newer features. Match the version to your app's engines field in package.json where one is set.


Do I need a domain and DNS set up before requesting HTTPS?

Yes. The domain's A record must point to your server's IP before you run Certbot, because Let's Encrypt validates ownership by reaching your server over that domain. Set DNS first, confirm it resolves, then request the certificate.


How do I view my application logs?

Use pm2 logs myapp for live application output, or pm2 logs myapp --lines 200 for recent history. Nginx access and error logs are in /var/log/nginx/.


Share via
Did you find this article useful?  

Related Articles

Tags

© Softsys Hosting