How to Deploy a Node.js Application on Ubuntu 26.04

By 

Published on

8 min read

Node.js application deployment flow through PM2, Nginx, and HTTPS on Ubuntu 26.04

Running node app.js on a developer laptop is one thing; getting the same code to stay up on a production server, restart after a crash, survive a reboot, and answer requests on standard HTTPS ports is another. A working Node.js deployment on Ubuntu 26.04 has three moving parts: the runtime, a process manager that keeps the app alive, and a reverse proxy that handles TLS and routes traffic to the application.

This guide explains how to deploy a Node.js application on Ubuntu 26.04 using PM2 as the process manager and Nginx as the reverse proxy, with a Let’s Encrypt certificate in front.

Prerequisites

Before starting you need:

  • Ubuntu 26.04 with a user that has sudo privileges
  • A domain name with an A record pointing at the server’s public IP
  • Ports 80 and 443 open in the firewall

We will use example.com and an app that listens on port 3000. Substitute your real values.

Step 1: Install Node.js

Ubuntu 26.04 includes Node.js in its repositories, but NodeSource makes it easier to install a specific upstream LTS release. The commands below install Node.js 24.x, the current LTS line at publication time.

Install the required packages and import the NodeSource signing key:

Terminal
sudo apt update
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 --yes -o /etc/apt/keyrings/nodesource.gpg

Add the Node.js 24.x repository:

Terminal
NODE_MAJOR=24
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

Update the package index and install Node.js:

Terminal
sudo apt update
sudo apt install -y nodejs

Confirm both binaries are in place:

Terminal
node --version
npm --version
output
v24.16.0
11.16.0

The exact patch versions will change as Node.js and npm updates are released. For other installation methods, see our guide on installing Node.js and npm on Ubuntu 26.04 .

Step 2: Prepare a Deploy User and Directory

Production apps should run as a non-root account that owns only its own files. Create a dedicated user:

Terminal
sudo adduser --system --group --home /var/www/app --shell /bin/bash app

The --system flag creates an account with password login disabled, which is appropriate for a service account. Administrators can still run deployment commands as this user through sudo.

Set up the deployment directory:

Terminal
sudo mkdir -p /var/www/app
sudo chown -R app:app /var/www/app

Step 3: Get the Application onto the Server

Most deployments pull the code from Git. Clone the repository as the app user:

Terminal
sudo apt install -y git
sudo -H -u app git clone https://github.com/example/myapp.git /var/www/app

Install dependencies inside that directory:

Terminal
sudo -H -u app bash -c 'cd /var/www/app && npm ci --omit=dev'

npm ci is the right command for deploys: it reads package-lock.json and refuses to fall back when the lock file is out of sync. --omit=dev skips development-only dependencies, which keeps the production install smaller.

If the application has a build step that depends on development packages, install all dependencies first, build the project, and then remove development-only packages:

Terminal
sudo -H -u app bash -c \
  'cd /var/www/app && npm ci && npm run build && npm prune --omit=dev'

For private repositories, configure an SSH deploy key or a short-lived access token for the app account. Do not store repository credentials in the application directory.

For a quick test without a real repository, write a minimal Express app instead:

Terminal
sudo -H -u app bash -c 'cd /var/www/app && npm init -y && npm install express'
sudo -H -u app nano /var/www/app/server.js
/var/www/app/server.jsjs
const express = require("express");
const app = express();
const port = 3000;

app.get("/", (req, res) => {
  res.send("Hello from Node.js on Ubuntu 26.04");
});

app.listen(port, "127.0.0.1", () => {
  console.log(`App listening on http://127.0.0.1:${port}`);
});

Notice that the listen call binds to 127.0.0.1, not 0.0.0.0. The reverse proxy will reach the app over loopback, and binding to localhost keeps the application off the public network entirely.

Step 4: Install PM2 and Run the App

PM2 is a Node.js process manager that handles restarts, logs, and clustering. Install it globally:

Terminal
sudo npm install -g pm2

Configure any required environment variables before starting the process. Keep API keys, database passwords, and other secrets outside the Git repository, and add local .env files to .gitignore.

Start the application under PM2 as the app user:

Terminal
sudo -H -u app pm2 start /var/www/app/server.js --name myapp --time
output
[PM2] Starting /var/www/app/server.js in fork_mode (1 instance)
[PM2] Done.
┌─────┬────────┬─────────────┬─────────┬─────────┬──────────┬────────┐
│ id  │ name   │ namespace   │ version │ mode    │ pid      │ uptime │
├─────┼────────┼─────────────┼─────────┼─────────┼──────────┼────────┤
│ 0   │ myapp  │ default     │ 1.0.0   │ fork    │ 12345    │ 0s     │
└─────┴────────┴─────────────┴─────────┴─────────┴──────────┴────────┘

Verify the app responds on loopback:

Terminal
curl http://127.0.0.1:3000
output
Hello from Node.js on Ubuntu 26.04

To make PM2 start the app at boot, generate the systemd unit and freeze the process list:

Terminal
sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u app --hp /var/www/app
sudo -H -u app pm2 save

The first command writes and enables pm2-app.service; the second saves the current process list so PM2 knows what to start. Confirm the unit is enabled:

Terminal
sudo systemctl status pm2-app

Step 5: Install and Configure Nginx

Install Nginx:

Terminal
sudo apt install nginx

Create a server block that proxies requests to the Node.js app:

Terminal
sudo nano /etc/nginx/sites-available/example.com
/etc/nginx/sites-available/example.comnginx
server {
    listen 80;
    listen [::]:80;

    server_name example.com www.example.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        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;
    }
}

The headers preserve the original host, client IP, and protocol for the application. If the app uses WebSockets, add the upgrade settings from the Nginx reverse proxy guide .

Enable the site and test the configuration:

Terminal
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Open the firewall:

Terminal
sudo ufw allow 'Nginx Full'

Visit http://example.com and confirm the page renders. Requests now travel from the browser to Nginx on port 80, then to Node.js on loopback port 3000.

Step 6: Add HTTPS with Certbot

Install Certbot and the Nginx plugin:

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

Certbot rewrites the server block to terminate TLS, adds an HTTP-to-HTTPS redirect, and registers a renewal timer. Verify:

Terminal
curl -I https://example.com
sudo certbot renew --dry-run

The first command should return a successful HTTP status over HTTPS. The renewal dry run confirms that Certbot can renew the certificate before it expires.

Step 7: Manage the App with PM2

Once the deployment is live, the day-to-day operations all go through PM2:

Terminal
sudo -H -u app pm2 ls
sudo -H -u app pm2 logs myapp
sudo -H -u app pm2 restart myapp
sudo -H -u app pm2 stop myapp

PM2 writes application output under /var/www/app/.pm2/logs. Install the log rotation module so those files do not grow without limit:

Terminal
sudo -H -u app pm2 install pm2-logrotate
sudo -H -u app pm2 set pm2-logrotate:max_size 10M
sudo -H -u app pm2 set pm2-logrotate:retain 7

To deploy a new version, pull only fast-forward changes, install dependencies, and restart the process:

Terminal
sudo -H -u app bash -c \
  'cd /var/www/app && git pull --ff-only && npm ci --omit=dev'
sudo -H -u app pm2 restart myapp --update-env
sudo -H -u app pm2 save

This guide starts the app in fork mode, so pm2 restart briefly interrupts requests. Zero-downtime pm2 reload requires cluster mode with at least two instances:

Terminal
sudo -H -u app pm2 delete myapp
sudo -H -u app pm2 start /var/www/app/server.js --name myapp -i 2 --time
sudo -H -u app pm2 save
sudo -H -u app pm2 reload myapp

Cluster mode works well for stateless HTTP applications. Apps that keep sessions, scheduled jobs, or WebSocket state in memory need an external store or additional coordination before running multiple instances.

Troubleshooting

502 Bad Gateway from Nginx
The Node.js app is not running, not listening on the expected port, or bound to the wrong address. Check sudo -H -u app pm2 ls and confirm the process is online, then run sudo ss -ltnp | grep 3000 to confirm it is listening on loopback.

PM2 does not start at boot
The systemd unit was not enabled, or pm2 save was skipped. Check sudo systemctl status pm2-app, re-run the startup command, and then run sudo -H -u app pm2 save.

Certbot cannot validate the domain
Confirm that the domain’s A and AAAA records point to this server, port 80 is reachable, and sudo ufw status shows Nginx Full allowed. If an incorrect AAAA record points elsewhere, Let’s Encrypt validation can fail even when the A record is correct.

FAQ

Why use Nginx in front of Node.js?
Nginx handles the public ports and TLS certificate while the Node.js process stays on an unprivileged local port. It also gives you one place for request limits, static files, access logs, and routing several applications by domain.

Can I run multiple Node.js apps on the same server?
Yes. Give each app its own port, its own PM2 entry, and its own Nginx server block. The reverse proxy routes by hostname so each app gets the requests for its domain only.

Should I use PM2 or systemd directly?
Both work. PM2 adds Node.js-specific process controls, clustering, and reloads, while systemd is simpler when you only need to supervise one process. In this setup, systemd starts PM2 at boot and PM2 manages the application.

Conclusion

A production Node.js deployment on Ubuntu 26.04 uses PM2 to keep the application running and Nginx to handle public HTTP and HTTPS traffic. Keep the app bound to loopback, rotate its logs, and test the update procedure before the server carries production traffic.

For related setup, see our guides on installing Node.js on Ubuntu 26.04 and configuring Nginx as a reverse proxy .

Linuxize Weekly Newsletter

A quick weekly roundup of new tutorials, news, and tips.

About the authors

Dejan Panovski

Dejan Panovski

Dejan Panovski is the founder of Linuxize, an RHCSA-certified Linux system administrator and DevOps engineer based in Skopje, Macedonia. Author of 800+ Linux tutorials with 20+ years of experience turning complex Linux tasks into clear, reliable guides.

View author page