Report this

What is the reason for this report?

How To Set Up a Node.js Application for Production on Ubuntu

Updated on December 4, 2025
English

Not using Ubuntu 20.04?
Choose a different version or distribution.
Ubuntu 20.04
How To Set Up a Node.js Application for Production on Ubuntu

Introduction

Running a Node.js application locally is just the first step. To make it production ready, you need to configure it for real-world conditions that require stability, security, and performance. A properly configured production environment ensures your app can recover from crashes, serve traffic securely, and handle increased demand without downtime.

In this guide, you will build a complete production setup on Ubuntu using Node.js, PM2, Nginx, Let’s Encrypt, and UFW. Node.js runs the application, PM2 manages processes and restarts them automatically, Nginx handles incoming requests and serves traffic over HTTPS, Let’s Encrypt provides free SSL certificates, and UFW secures the server by controlling network access. Together, these tools form a strong foundation for deploying applications that are reliable, efficient, and secure.

Deploy your Node applications from GitHub using DigitalOcean App Platform. Let DigitalOcean focus on scaling your app.

Key Takeaways:

  • Use a process manager like PM2 to keep Node.js apps running continuously, auto-restart on crashes, and start on reboot using pm2 startup and pm2 save.
  • Run apps behind Nginx as a reverse proxy instead of exposing Node.js directly. Nginx handles SSL, static assets, and load balancing efficiently.
  • Always bind Node.js to localhost (127.0.0.1) in production to prevent direct public access and rely on Nginx for external traffic.
  • Enable HTTPS with Let’s Encrypt and Certbot. Let Nginx manage SSL termination and certificate renewals automatically with sudo certbot renew --dry-run.
  • Secure the server with UFW. Only allow SSH and “Nginx Full” (HTTP/HTTPS) traffic, blocking everything else.
  • Manage secrets via environment variables or .env files loaded by dotenv, and keep them out of version control.
  • Enable compression and caching in Nginx (gzip and Brotli) to improve load speed and reduce bandwidth usage.
  • Monitor with PM2 and system tools (pm2 monit, htop, or integration with tools like Grafana/Prometheus) to detect performance issues early.
  • Regularly apply updates and audits (sudo apt update && sudo apt upgrade, npm audit) to maintain system and dependency security.
  • Follow best practices such as running under a non-root user, automating backups, log rotation, and using HTTPS everywhere for a stable, secure deployment.

Prerequisites

This guide assumes that you have the following:

When you’ve completed the prerequisites, you will have a server serving your domain’s default placeholder page at https://example.com/.

Install and Verify Node.js

In this step, you’ll install Node.js using the NodeSource PPA and verify that the installation was successful. This method ensures that you have access to the latest Long-Term Support (LTS) version of Node.js and its associated package manager, npm.

Install Node.js via NodeSource PPA

First, install the NodeSource PPA to gain access to its repository. Make sure you’re in your home directory, and use curl to download the setup script for the most recent LTS version of Node.js.

  1. cd ~
  2. curl -sL https://deb.nodesource.com/setup_24.x -o nodesource_setup.sh

You can review the script contents with nano before running it:

  1. nano nodesource_setup.sh

When you’re ready, execute the script with sudo:

  1. sudo bash nodesource_setup.sh

This will add the PPA to your configuration and automatically update your local package cache.

Now, you can install Node.js:

  1. sudo apt install nodejs

To verify the installation and check the version of Node.js, run:

  1. node -v

You should see an output similar to this:

v24.11.1

Install npm and build-essential

The nodejs package also installs npm, the Node.js package manager. To confirm it’s installed and initialize its configuration file, run:

  1. npm -v

You should see a similar output:

11.6.2

In order for some npm packages to work (those that require compiling code from source, for example), you will need to install the build-essential package:

  1. sudo apt install build-essential

With Node.js and npm successfully installed, you now have the necessary tools to develop and manage your application dependencies. In the next section, you’ll create a simple Node.js app to test the runtime environment.

Create a Sample Node.js Application

Let’s write a simple Node.js application that returns “Hello World” to any HTTP requests. This sample app will verify that Node.js is installed correctly and demonstrate how applications should be configured to listen securely on localhost in production.

Create a simple Node.js server

Create a new file called hello.js in your home directory:

  1. cd ~
  2. nano hello.js

Add the following code to the file:

hello.js
const http = require('http');

const hostname = 'localhost';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World!\n');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

Save and close the file when finished.

This script creates a simple HTTP server that listens on port 3000 and responds with Hello World! to any incoming requests. Because it’s bound to localhost, it will only accept connections from the same machine.

Test the application

Run your application using Node.js:

  1. node hello.js

You should see output similar to:

Server running at http://localhost:3000/

In another terminal session, test your app with curl:

  1. curl http://localhost:3000

You should see the following output:

Hello World!

If you do not get the expected output, make sure that your Node.js application is running and configured to listen on the proper address and port.

Once you’re sure it’s working, kill the application by pressing CTRL+C in the original terminal to stop the server.

Why apps should bind to localhost in production

In production environments, Node.js applications typically sit behind a reverse proxy such as Nginx. The Node.js app listens on an internal address (like localhost:3000), and Nginx handles incoming web traffic, TLS termination, and routing.

Binding your application to localhost provides several security and reliability benefits:

  • Prevents external exposure: Only Nginx can access the Node.js process directly.
  • Enables HTTPS offloading: Nginx manages SSL certificates and encryption, offloading this work from Node.js.
  • Simplifies firewall rules: The firewall can block access to all internal application ports while allowing HTTP/HTTPS.

This setup ensures that your Node.js app is isolated, secure, and only accessible through the Nginx reverse proxy.

Install and Configure PM2 Process Manager

In this section, you’ll install PM2, a production process manager for Node.js applications. PM2 allows you to keep your applications running continuously, automatically restart them after crashes, and set them to start on system boot.

Installing PM2

Use npm to install PM2 globally:

  1. sudo npm install -g pm2

The -g flag installs PM2 system-wide, so it’s available for all users and applications.

Once it’s installed, start your sample application using PM2:

  1. pm2 start hello.js

You’ll see an output table that shows PM2 has started managing your app:

[PM2] Spawning PM2 daemon with pm2_home=/home/user/.pm2
[PM2] PM2 Successfully daemonized
[PM2] Starting /root/nodeapp/hello.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    │ 3826     │ 0s     │ 0    │ online    │ 0%       │ 33.3mb   │ root     │ disabled │
└────┴──────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘

As indicated above, PM2 automatically assigns an App name (based on the filename, without the .js extension) and a PM2 id. PM2 also maintains other information, such as the PID of the process, its current status, and memory usage.

Enabling Auto-Restart on Crash and Reboot

PM2 automatically restarts your application if it crashes or is killed. To make sure PM2 itself starts automatically when the server reboots, run:

  1. pm2 startup systemd

PM2 will generate a command that looks something like this (replace sammy with your username):

[PM2] To setup the Startup Script, copy/paste the following command:
sudo env PATH=$PATH:/home/sammy/.nvm/versions/node/v24.11.1/bin /home/sammy/.nvm/versions/node/v24.11.1/lib/node_modules/pm2/bin/pm2 startup systemd -u sammy --hp /home/sammy

Note: Your path will vary depending on your Node.js version and username.

Run that command, then save your running apps so PM2 remembers them for the next reboot:

  1. pm2 save

To confirm everything is set up properly, check the service status:

  1. systemctl status pm2-sammy

Note: For a detailed overview of systemd, please review Systemd Essentials: Working with Services, Units, and the Journal.

If you encounter issues, reboot your server and check that your app starts automatically.

Using PM2 Ecosystem File

When you’re ready to run more than one app or manage different environments, PM2’s ecosystem file is a huge help. It’s a simple configuration file that lets you define environment variables, ports, and deployment settings all in one place.

Generate a starter file by running:

  1. pm2 ecosystem

Then edit the generated ecosystem.config.js file:

ecosystem.config.js
module.exports = {
  apps: [
    {
      name: 'hello',
      script: 'hello.js',
      env: {
        NODE_ENV: 'development',
        PORT: 3001
      },
      env_production: {
        NODE_ENV: 'production',
        PORT: 3000
      }
    }
  ]
};

To start your app in production mode, use:

  1. pm2 start ecosystem.config.js --env production

The ecosystem file makes it easy to manage multiple environments and securely store environment variables outside your source code. With PM2 in place, your app will survive crashes, reboots, and updates, all while running quietly in the background.

Additional PM2 commands

In addition to those we have covered, PM2 provides many subcommands that allow you to manage or look up information about your applications.

Stop an application with this command (specify the PM2 App name or id):

  1. pm2 stop app_name_or_id

Restart an application:

  1. pm2 restart app_name_or_id

List the applications currently managed by PM2:

  1. pm2 list

Get information about a specific application using its App name:

  1. pm2 info app_name

To view live logs for your application:

  1. pm2 logs

The PM2 process monitor can be pulled up with the monit subcommand. This displays the application status, CPU, and memory usage:

  1. pm2 monit

Note that running pm2 without any arguments will also display a help page with example usage.

Now that your Node.js application is running and managed by PM2, let’s set up the reverse proxy.

Set Up Nginx as a Reverse Proxy

With your Node.js app running under PM2, the next step is to make it accessible to users through the web. Instead of exposing the app directly on port 3000, you’ll use Nginx as a reverse proxy. Nginx will handle all incoming HTTP and HTTPS requests and pass them to your Node.js app running on localhost.

Basic Reverse Proxy Configuration

Open your site’s Nginx configuration file. If you followed the setup from the prerequisites section, this file should be located in /etc/nginx/sites-available/example.com:

  1. sudo nano /etc/nginx/sites-available/example.com

Inside the server block, locate (or create) the location / section and replace its contents with the following configuration. If your Node.js app listens on a different port, update the highlighted value accordingly.

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

    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
...
}

This tells Nginx to listen on port 80 (HTTP) and forward all incoming traffic to your Node.js app running on port 3000.

To test multiple apps, you can define additional location blocks. For example, if you have another Node.js app running on port 3001, add this to the same server block:

server {
...
    location /app2 {
        proxy_pass http://localhost:3001;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
...
}

Save the file, then test the configuration for syntax errors by running:

  1. sudo nginx -t

If everything checks out, reload Nginx:

  1. sudo systemctl reload nginx

At this point, visiting your server’s domain (for example, http://example.com) should load the Node.js app running through the Nginx proxy.

Adding HTTPS (SSL) Configuration

To secure your app, configure Nginx to handle HTTPS traffic. If you already have SSL certificates from Let’s Encrypt, ensure they’re defined in your Nginx config. Here’s an example of how the updated server block might look:

server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name example.com www.example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;

    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}

The first server block automatically redirects all HTTP requests to HTTPS, and the second block handles secure traffic on port 443.

Multiple Application Routing (Optional)

If your server hosts multiple applications, Nginx can handle them all by defining different paths or subdomains. For instance, you might have:

  • https://example.com/ forwards to port 3000
  • https://example.com/app2 forwards to port 3001
  • https://api.example.com/ forwards to port 4000

Each app can be managed by its own PM2 process and served through Nginx.

Once you’ve confirmed everything is working, your Node.js app should now be accessible securely through Nginx.

Configure UFW Firewall for Production

Now that your Node.js app is running behind Nginx, let’s add a final layer of network security. Ubuntu ships with a built-in firewall called UFW (Uncomplicated Firewall). It’s lightweight, simple to configure, and perfect for blocking unwanted access to your server.

Checking Firewall Status

Before making any changes, check whether UFW is already enabled:

  1. sudo ufw status

If it says inactive, don’t worry, we’ll enable it shortly. But first, we’ll define which types of traffic should be allowed. It’s always best to set up the rules before turning it on.

Allow Essential Traffic

At a minimum, you’ll need to allow SSH so you can still connect to your server, and HTTP/HTTPS so Nginx can serve web traffic. To do that, run the following commands:

  1. sudo ufw allow OpenSSH
  2. sudo ufw allow 'Nginx Full'

Now we can enable the firewall:

  1. sudo ufw enable

You’ll be prompted to confirm: type y and press Enter.

Check the firewall again to make sure your rules are active:

  1. sudo ufw status

You should see something like this:

Status: active

To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW       Anywhere
Nginx Full                 ALLOW       Anywhere
OpenSSH (v6)               ALLOW       Anywhere (v6)
Nginx Full (v6)            ALLOW       Anywhere (v6)

That’s it! Only SSH and web traffic are now permitted; everything else is blocked.

Why a Firewall Matters in Production

Firewalls are fundamental to server security because they establish strict control over what network traffic can reach your system. Even well-configured applications and web servers can be vulnerable if unnecessary ports are left open.

Here are a few key reasons why firewalls are critical in production:

  • Minimize exposure: Only explicitly permitted ports are accessible, reducing the number of potential attack vectors.
  • Containment: In the event of a breach or misconfiguration, a firewall limits how far an attacker can move within your system or network.
  • Compliance and auditing: Many security standards require active firewall protection with auditable access rules.
  • Defense in depth: Firewalls complement other layers of security such as TLS, authentication, and application hardening.

For most production servers, keeping a firewall active with only the essential ports open strikes the right balance between accessibility and protection. It’s a low-maintenance safeguard that provides an extra layer of control and helps prevent unauthorized network access.

With UFW configured, your system is now more secure at the network level.

Enable SSL with Let’s Encrypt

Now that your application is publicly accessible through Nginx, it’s time to add HTTPS for secure communication. SSL encrypts all traffic between your server and users, ensuring privacy and data integrity. The easiest way to add SSL to your site is by using Let’s Encrypt, a free and automated certificate authority.

Install Certbot and the Nginx Plugin

Let’s Encrypt works through a tool called Certbot, which handles the entire process of requesting, installing, and renewing certificates. On Ubuntu, you can install Certbot along with its Nginx plugin in a single step:

  1. sudo apt update
  2. sudo apt install certbot python3-certbot-nginx -y

This installs both the Certbot client and its Nginx integration plugin, which will automatically configure HTTPS in your existing Nginx server block.

Obtain SSL Certificates

Once Certbot is installed, you can use it to request a new SSL certificate for your domain. Replace example.com and www.example.com with your actual domain names:

  1. sudo certbot --nginx -d example.com -d www.example.com

During the process, Certbot will:

  • Verify that your domain points to this server (using an HTTP challenge)
  • Obtain an SSL certificate from Let’s Encrypt
  • Automatically edit your Nginx configuration to enable HTTPS
  • Reload Nginx so the new settings take effect

You’ll be prompted for an email address (used for renewal notifications) and asked to agree to the terms of service. Once the setup completes, you should see a confirmation message stating that your certificate has been successfully installed.

You can double-check your certificate details with:

  1. sudo certbot certificates

Then visit your domain to verify that the site is now being served securely.

Automatic Renewal

Let’s Encrypt certificates are only valid for 90 days, but there’s no need to worry about manual renewals. Certbot installs a system timer that runs twice daily to check for expiring certificates and renew them automatically.

You can test this renewal process at any time with:

  1. sudo certbot renew --dry-run

If the dry run completes without errors, your certificates will renew automatically in the background, and Nginx will reload to apply the new ones.

Why HTTPS Matters

Running your application over HTTPS is no longer optional, it’s an essential part of running any production system. SSL/TLS ensures that all communication between your server and clients is encrypted and tamper-proof, protecting against eavesdropping and data manipulation.

Beyond security, HTTPS offers several additional advantages:

  • Trust and credibility: Browsers label non-HTTPS sites as “Not Secure”, which can undermine user confidence.
  • Improved search ranking: Major search engines use HTTPS as a ranking signal.
  • Access to modern web features: HTTP/2, QUIC, and other performance enhancements require TLS.
  • Compliance: Many privacy regulations and industry standards mandate encryption for transmitted data.

By using Let’s Encrypt, you get these benefits with minimal setup and zero cost. Certificates renew automatically, making it an easy long-term solution for secure deployments.

With HTTPS in place, your server is now serving encrypted, trusted traffic.

Manage Environment Variables Securely

Every production application needs a reliable way to handle configuration data such as API keys, database credentials, or secret tokens. Storing these values directly in your source code can expose them through version control, logs, or even client-side leaks. A safer approach is to use environment variables, which keep sensitive information separate from your codebase.

Using .env Files with dotenv

A common and straightforward method to manage environment variables in Node.js is by using a .env file together with the dotenv package. This setup lets your application read configuration values at runtime while keeping them out of the source code.

Run the following to install the dotenv package:

  1. npm install dotenv

Next, create a .env file in your project directory:

  1. nano .env

Add your configuration values in key-value pairs, like this:

.env
PORT=3000
NODE_ENV=production
DB_HOST=localhost
DB_USER=myuser
DB_PASS=securepassword
JWT_SECRET=myjwtsecretkey

Now, modify your main application file (for example, app.js) to load the environment variables at startup:

app.js
require('dotenv').config();

const express = require('express');
const app = express();

app.listen(process.env.PORT, () => {
  console.log(`Server running in ${process.env.NODE_ENV} mode on port ${process.env.PORT}`);
});

When your app runs, it reads the variables defined in the .env file and uses them to configure the runtime environment.

Environment Variables with PM2

If you are running your application with PM2, you can define environment variables directly in PM2 commands or configuration files. This helps keep configurations consistent across environments like development, staging, and production.

You can set variables inline when starting an app:

  1. pm2 start app.js --name "myapp" --env production

For a cleaner and more scalable solution, use the PM2 ecosystem configuration file. Generate it using:

  1. pm2 ecosystem

Then edit the ecosystem.config.js file to include your environment variables:

ecosystem.config.js
module.exports = {
  apps: [
    {
      name: 'myapp',
      script: 'app.js',
      env: {
        NODE_ENV: 'development',
        PORT: 3001
      },
      env_production: {
        NODE_ENV: 'production',
        PORT: 3000,
        DB_HOST: 'localhost',
        DB_USER: 'myuser',
        DB_PASS: 'securepassword'
      }
    }
  ]
};

Start the app in production mode:

  1. pm2 start ecosystem.config.js --env production

This ensures that the correct variables are loaded automatically each time the app restarts.

Best Practices for Managing Secrets

Managing secrets and environment variables securely is critical to maintaining the integrity of your infrastructure. The following best practices will help minimize the risks:

  • Keep secrets out of source control: Never commit .env files to your Git repository. Add them to .gitignore to avoid accidental exposure.

  • Limit access: Only trusted users and automated systems should have permission to read or modify environment configuration files.

  • Use environment-specific files: Maintain separate configurations for development, testing, staging, and production. For example, .env.development and .env.production.

  • Rotate secrets regularly: Change API keys, tokens, and passwords on a schedule to reduce the impact of potential leaks.

  • Use a secrets manager for larger systems: For complex applications or teams, consider using services like HashiCorp Vault instead of local .env files.

  • Avoid logging sensitive data: Be careful not to print or log secrets in your application or deployment logs.

  • Validate environment variables at startup: Use libraries such as joi or env-schema to ensure all required variables are set before the application runs.

  • Restrict file permissions: Use the chmod command to limit access to .env files. For example:

    1. chmod 600 .env

Following these practices helps protect sensitive information, prevents accidental exposure, and makes it easier to manage configuration across multiple environments.

Production Optimization Techniques

With your application up and running in a stable production environment, it is important to focus on performance and efficiency. Optimization not only improves user experience, but also reduces server costs and helps ensure smooth scalability. In this section, you will learn several techniques to fine-tune your Node.js and Nginx setup for better speed, reliability, and resource usage.

Enable Compression (gzip or Brotli)

Compression is one of the simplest ways to improve performance. It reduces the size of files sent from the server to the client, which speeds up page loads and reduces bandwidth consumption. Nginx supports two popular compression methods: gzip and Brotli.

Using gzip

To enable gzip compression, open the main Nginx configuration file:

  1. sudo nano /etc/nginx/nginx.conf

Inside the http block, add or uncomment the following directives:

gzip on;
gzip_types text/plain text/css application/javascript application/json application/xml;
gzip_min_length 256;
gzip_comp_level 5;

These settings enable gzip for common file types, compress files larger than 256 bytes, and apply a moderate compression level to balance CPU load and performance.

Using Brotli

If you have the Brotli module installed, you can use it for even better compression ratios:

brotli on;
brotli_comp_level 5;
brotli_types text/plain text/css application/javascript application/json application/xml;

Brotli is particularly effective for text-based assets like CSS and JavaScript. After making these changes, reload Nginx to apply them:

  1. sudo systemctl reload nginx

With compression enabled, clients will download assets faster, resulting in quicker load times and improved performance on both desktop and mobile devices.

Configure Caching in Nginx

Caching allows your server to reuse previously generated responses instead of regenerating them for every request. This greatly reduces the load on your Node.js application and speeds up response times for end users.

To cache static assets such as images, stylesheets, and scripts, add this configuration to your Nginx server block:

location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
    expires 7d;
    add_header Cache-Control "public, no-transform";
}

This configuration instructs browsers to cache static assets for seven days. You can adjust the duration depending on how often your static files change.

For APIs or dynamic content, caching can be applied more selectively. For example, you might cache responses for frequently accessed endpoints while excluding endpoints that return user-specific data. Nginx also supports microcaching (short-term caching in seconds) for high-traffic APIs.

Logging Best Practices

Logs are essential for monitoring, troubleshooting, and performance tuning. Both PM2 and Nginx provide comprehensive logging tools that help you keep track of what is happening across your system.

PM2 Logs

PM2 manages your application logs automatically. To view them in real time, use:

  1. pm2 logs

Over time, logs can grow large and consume disk space. To prevent this, install the PM2 log rotation module:

  1. pm2 install pm2-logrotate

Then configure the rotation policy:

  1. pm2 set pm2-logrotate:max_size 10M
  2. pm2 set pm2-logrotate:retain 7

This setup keeps log files under 10 MB each and retains seven days of logs. You can adjust these values based on your storage capacity and logging needs.

Nginx Logs

Nginx maintains both access logs (records of each request) and error logs (information about issues and failures). These logs are typically stored in /var/log/nginx/.

You can review access logs manually or generate a detailed traffic report using tools like goaccess:

  1. sudo apt install goaccess
  2. sudo goaccess /var/log/nginx/access.log -o /var/www/html/report.html --log-format=COMBINED

This command creates an interactive HTML report with metrics on visitor locations, traffic volume, and response times.

Well-managed logs are critical for identifying performance bottlenecks, failed requests, or security anomalies before they impact users.

Resource Management and Node.js Tuning

Node.js applications run on a single thread by default. To utilize all available CPU cores and improve concurrency, you can use clustering and other process management techniques.

Use Clustering

PM2 can automatically start multiple Node.js processes to take advantage of multi-core CPUs:

  1. pm2 start app.js -i max

This command starts as many instances of your app as there are CPU cores on your server. PM2 will manage load balancing and restart processes if they fail.

Monitor Memory and CPU Usage

Regularly monitor your app’s resource usage to prevent memory leaks or excessive CPU consumption:

  1. pm2 monit

This opens an interactive dashboard showing live memory and CPU metrics for each process.

Offload Static Assets to Nginx

Let Nginx serve static files directly instead of routing them through Node.js. This significantly reduces CPU usage on the Node.js side and ensures faster delivery of cached assets.

Environment Configuration

Use environment variables to fine-tune performance settings such as log verbosity, API rate limits, or cache durations. Managing configuration through environment variables keeps your app flexible and easier to scale.

Monitoring and Metrics

After optimizing your production environment, the next essential step is setting up effective monitoring and metrics collection. Monitoring gives you visibility into your application’s performance, resource utilization, and system health. It helps you detect problems early, track long-term trends, and make data-driven improvements.

Let’s see how you can monitor your Node.js application, Nginx web server, and system resources using built-in tools and external services.

Why Monitoring Matters

In production, every second counts. Without monitoring, performance issues or service disruptions can go unnoticed until they affect users. Monitoring helps you:

  • Detect failures quickly and automatically alert you when they occur.
  • Analyze system performance under different traffic loads.
  • Identify slow endpoints, bottlenecks, or memory leaks.
  • Make informed scaling and optimization decisions.
  • Maintain uptime and reliability across deployments.

A well-designed monitoring setup includes both real-time monitoring for immediate alerts and historical metrics for performance analysis over time.

Monitoring Node.js Applications with PM2

PM2, the process manager used to run your Node.js app, includes powerful monitoring features.

Real-Time Monitoring

To get an overview of running processes, use:

  1. pm2 list

This displays all active applications along with their status, uptime, CPU, and memory usage. For a more detailed, live view, use:

  1. pm2 monit

This opens an interactive dashboard showing key performance metrics, such as CPU and memory utilization per process. Monitoring these metrics helps you detect resource exhaustion, runaway processes, or memory leaks.

Log Monitoring

PM2 also provides centralized log management. To review your application logs, run:

  1. pm2 logs

You can view logs for a specific app using:

  1. pm2 logs myapp

If you notice frequent restarts, check PM2’s process list with:

  1. pm2 status

It will show how many times each process has restarted, which can be an indicator of errors or crashes.

Advanced Monitoring (PM2 Plus)

For larger deployments, PM2 offers PM2 Plus, a web-based dashboard that tracks metrics such as CPU usage, memory consumption, and application uptime across all servers. You can set up alerts for threshold breaches and view performance trends over time.

PM2 can also integrate with popular third-party monitoring systems like Datadog, New Relic, and Prometheus for unified visibility.

Monitoring Nginx

Nginx sits at the front of your production stack, managing HTTP requests and SSL termination. Monitoring Nginx is essential for understanding how users interact with your system and how efficiently requests are being processed.

Access and Error Logs

Nginx logs every request it handles in the access log, usually located at:

/var/log/nginx/access.log

Errors are recorded separately in:

/var/log/nginx/error.log

You can review these logs in real time:

  1. sudo tail -f /var/log/nginx/access.log

Access logs show valuable information such as client IPs, response times, status codes, and user agents. These logs are useful for identifying patterns like high latency, frequent 404 errors, or abnormal request volumes.

For a summarized and visual report, you can use goaccess. It creates an interactive dashboard that highlights traffic sources, popular endpoints, and response distribution.

Enable Nginx Status Page

Nginx includes a lightweight stub_status module that exposes live server metrics such as current connections and request counts. Add the following block to your Nginx configuration:

location /nginx_status {
    stub_status;
    allow 127.0.0.1;
    deny all;
}

After reloading Nginx, you can view metrics by visiting:

http://localhost/nginx_status

These metrics can be integrated into monitoring dashboards or used for automated alerts.

Collecting Application Metrics from Node.js

For more granular application-level monitoring, you can collect custom metrics directly from your Node.js app. These metrics can track API response times, request rates, or database query durations.

Run the following command to install the Prometheus client library:

  1. npm install prom-client express

Add a /metrics endpoint to your application:

const express = require('express');
const client = require('prom-client');
const app = express();

client.collectDefaultMetrics({ timeout: 5000 });

app.get('/metrics', async (req, res) => {
  res.set('Content-Type', client.register.contentType);
  res.end(await client.register.metrics());
});

app.listen(3000, () => {
  console.log('Metrics available at /metrics');
});

You can connect this endpoint to Prometheus or Grafana to visualize metrics such as request latency, memory usage, and garbage collection performance.

System-Level Monitoring

In addition to application monitoring, keep an eye on overall server performance. Linux provides several tools for this purpose:

  • htop or top: Displays real-time CPU and memory usage.
  • iostat: Monitors disk input/output operations.
  • netstat or ss: Displays open network connections.
  • vmstat: Tracks processes, memory, and I/O statistics.

You can automate system monitoring using tools such as Netdata, Glances, or Grafana Agent, which gather metrics across your infrastructure and visualize them in dashboards.

Common Errors, Troubleshooting, and Best Practices

Even a well-configured production setup can occasionally run into issues. Understanding how to identify, diagnose, and resolve these problems is key to maintaining reliability and minimizing downtime. This section provides detailed troubleshooting steps for common errors and outlines best practices to help you keep your Node.js environment stable, secure, and maintainable over the long term.

Common Errors and Fixes

1. Application fails to start with PM2

  • Cause: Incorrect file paths, missing dependencies, or misconfigured environment variables.

  • Solution: Check logs using:

    1. pm2 logs

    Confirm the correct file path is used in your PM2 configuration. Reinstall dependencies if necessary:

    1. npm install --production

    Also verify that required environment variables are loaded properly from .env or your ecosystem file.

2. Port already in use error

  • Cause: Another process is already listening on the same port.

  • Solution: Identify and stop the conflicting process:

    1. sudo lsof -i :3000
    2. sudo kill -9 <PID>

    Alternatively, change your application port in the .env file or the ecosystem configuration.

3. Nginx 502 Bad Gateway

  • Cause: Nginx cannot reach the Node.js app, usually because the app crashed, stopped, or is bound to a different port.

  • Solution: Restart the Node.js app and reload Nginx:

    1. pm2 restart all
    2. sudo systemctl reload nginx

    Make sure the Nginx proxy_pass value matches your Node.js application’s listening address (for example, http://localhost:3000).

4. Permission denied errors

  • Cause: The application user lacks permission to read or write certain files or directories.

  • Solution: Set correct ownership and permissions:

    1. sudo chown -R $USER:$USER /var/www/myapp
    2. sudo chmod -R 755 /var/www/myapp

    Avoid running Node.js or PM2 as the root user.

5. Certbot renewal failure

  • Cause: DNS issues, expired certificates, or incorrect Nginx configuration.

  • Solution: Confirm DNS records point to your server and test renewal:

    1. sudo certbot renew --dry-run

    If renewal fails, review the Nginx configuration for syntax errors:

    1. sudo nginx -t

    You can also check Certbot logs in /var/log/letsencrypt/ for detailed errors.

6. Firewall blocking access

  • Cause: UFW rules prevent incoming HTTP or HTTPS connections.

  • Solution: Ensure Nginx traffic is allowed and the firewall is active:

    1. sudo ufw allow 'Nginx Full'
    2. sudo ufw enable
    3. sudo ufw status

Troubleshooting Workflow

When issues occur, follow a structured process:

  1. Check Logs: Review PM2, Nginx, and system logs first as most issues leave error traces here.

  2. Verify Services: Confirm Node.js, PM2, and Nginx are running and enabled at startup:

    1. pm2 status
    2. systemctl status nginx
  3. Validate Configuration Files: Run syntax checks before restarting services:

    1. sudo nginx -t

    For PM2 ecosystem files, check JSON or JavaScript formatting.

  4. Check Port Binding: Verify that your app is listening on the expected port:

    1. sudo netstat -tulnp | grep node
  5. Monitor System Resources: Use tools like htop or pm2 monit to check CPU, memory, and disk usage. Overloaded servers can cause slow responses or crashes.

  6. Test Connectivity: Use curl to simulate requests and verify that your endpoints respond correctly:

    1. curl http://localhost:3000

Best Practices for Stability, Security, and Maintenance

Following best practices ensures that your application remains reliable, secure, and easy to maintain as it grows.

Security Best Practices

  • Keep the system updated: Regularly update Node.js, PM2, Nginx, and Ubuntu packages using:

    1. sudo apt update && sudo apt upgrade -y

    This ensures security patches and performance improvements are applied.

  • Use HTTPS everywhere: Always enforce HTTPS through Nginx redirects and automatic certificate renewal with Certbot.

  • Restrict SSH access: Use key-based authentication, disable root logins, and limit SSH to known IPs.

  • Run as a non-root user: Use a dedicated application user to limit privileges in case of compromise.

  • Hide stack traces and error details: Never expose detailed error messages to users. Configure Express or other frameworks to handle errors gracefully.

  • Regularly audit dependencies: Run:

    1. npm audit

    and update or replace vulnerable packages.

Performance and Resource Management

  • Enable clustering: Use PM2 to run multiple Node.js instances across available CPU cores:

    1. pm2 start app.js -i max
  • Use caching effectively: Implement caching layers (such as Redis or in-memory caching) for frequently accessed data.

  • Monitor regularly: Set up dashboards with PM2 Plus, Grafana, or Prometheus to track performance trends.

  • Optimize logs: Enable log rotation to prevent excessive disk use and keep only relevant data.

  • Compress responses: Keep gzip or Brotli compression enabled in Nginx for faster delivery.

Maintenance and Operations

  • Automate backups: Schedule backups for your app data, configuration files, and SSL certificates.
  • Use version control: Store configurations and scripts in a Git repository to track changes over time.
  • Document your environment: Maintain detailed documentation of your setup, ports, environment variables, and deployment steps.
  • Use staging environments: Test updates and configuration changes in a staging server before pushing them to production.
  • Monitor uptime: Use external uptime monitoring tools like UptimeRobot, Pingdom, or StatusCake to detect outages quickly.
  • Plan for scaling: As traffic grows, consider horizontal scaling with load balancers or container orchestration tools like Docker and Kubernetes.
  • Schedule regular audits: Periodically review security settings, performance metrics, and configuration consistency across environments.

A proactive maintenance strategy, combined with continuous monitoring and strict adherence to security principles, helps ensure your Node.js application remains performant, reliable, and resilient in production.

FAQs

1. How do I deploy a Node.js app to production on Ubuntu?

To deploy a production-ready application, follow this standard workflow. Avoid running the app using node index.js directly in a terminal session, as it will stop if the session closes.

Here is a general workflow:

  1. Update System: Run sudo apt update && sudo apt upgrade.

  2. Create a Non-Root User: Never run your app as root. Create a dedicated user (e.g., deploy).

  3. Install Node.js: Use NodeSource binary distributions for the latest LTS version (don’t use the default Ubuntu apt version as it is often outdated).

    1. curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
    2. sudo apt-get install -y nodejs
  4. Clone & Install: Clone your repository containing the Node.js app, move into the directory, and run npm ci (clean install) instead of npm install to ensure exact dependency versions.

  5. Environment Variables: Create a .env file for secrets (DB strings, API keys).

  6. Start with a Process Manager: Instead of running node index.js (which stops when you close the terminal), use PM2 to daemonize the app:

    1. sudo npm install -g pm2
    2. pm2 start app.js --name "my-production-app"
  7. Ensure Startup Persistence: Tell the server to revive this app if the server reboots:

    • Generate the Startup Script: First, ask PM2 to generate the startup command for your specific OS (Ubuntu uses systemd).
    1. pm2 startup
    • Run the Generated Command: Copy and paste the command PM2 outputs into your terminal. It will look something like this: sudo env PATH=$PATH:/home/sammy/.nvm/versions/node/v24.11.1/bin /home/sammy/.nvm/versions/node/v24.11.1/lib/node_modules/pm2/bin/pm2 startup systemd -u sammy --hp /home/sammy

    • Freeze the Process List: Now that the startup system is active, save your currently running processes so PM2 remembers them on reboot.

    1. pm2 save

2. What’s the best way to manage Node.js processes on a VPS?

The industry standard for managing Node.js processes on a Virtual Private Server (VPS) is PM2.

While you can use systemd directly, PM2 offers features specifically designed for Node.js that standard service managers lack, such as:

  • Automatic Restart: Relaunches the app if it crashes.
  • Cluster Mode: Utilizes all CPU cores without changing your code.
  • Monitoring: Built-in commands to check RAM/CPU usage (pm2 monit).
  • Log Management: Auto-rotates logs to prevent disk bloat.

3. Should I use PM2 or systemd for Node.js in production?

The best practice is to use both.

You should use PM2 to manage the application and Systemd to manage PM2. If you only use PM2, your app will vanish if the server reboots.

How to combine them:

  1. Start your app with PM2: pm2 start app.js
  2. Generate the systemd startup script: pm2 startup
  3. Execute the command PM2 outputs (this tells Ubuntu’s systemd to launch PM2 on boot).
  4. Freeze the process list: pm2 save

4. How do I use Nginx as a reverse proxy for Node.js?

Node.js is excellent at handling application logic but poor at handling SSL, static files, and load balancing compared to Nginx.

The Setup:

  1. Install Nginx: sudo apt install nginx.
  2. Configure Nginx to listen on Port 80 and forward traffic to your Node app (usually running on localhost:3000).

Example Configuration (/etc/nginx/sites-available/default):

server {
    listen 80;
    server_name your_domain.com;

    location / {
        proxy_pass http://localhost:3000; # Forward to Node
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}

After saving, test the config with sudo nginx -t and restart Nginx.

5. How do I enable HTTPS for Node.js with Nginx?

Do not handle HTTPS inside your Node.js application logic. Let Nginx handle the encryption/decryption (SSL Termination).

The Easiest Method (Certbot):

  1. Install Certbot: sudo apt install certbot python3-certbot-nginx

  2. Run the automatic generator:

    1. sudo certbot --nginx -d your_domain.com
  3. Certbot will automatically edit your Nginx config file to enable HTTPS and set up a cron job to auto-renew the certificate.

6. How can I secure my Node.js app on Ubuntu?

Security requires a layered approach.

  • Operating System Level:

    • UFW Firewall: Enable the firewall and only allow necessary ports.

      1. sudo ufw allow ssh
      2. sudo ufw allow 'Nginx Full'
      3. sudo ufw enable
    • SSH Hardening: Disable password login and use SSH keys only.

  • Application Level:

    • Run as Non-Root: As mentioned in Q1, if an attacker compromises a root process, they own the server.
    • Helmet.js: Install this middleware (npm install helmet) in your app to set secure HTTP headers automatically.
    • Don’t expose ports: Ensure your Node app binds to localhost (127.0.0.1) so it cannot be accessed directly from the internet, forcing traffic through Nginx.

Summary

You’ve now set up a complete production environment for your Node.js application on Ubuntu. With Node.js running under PM2, Nginx managing web traffic, SSL encryption from Let’s Encrypt, and UFW protecting your network, your server is stable, secure, and ready for real-world use. By adding caching, compression, and proper monitoring, you’ve built a system that performs well and is easy to maintain. This setup gives you a solid foundation to grow your app, whether that means scaling across servers, automating deployments, or adding more advanced tooling down the line.

For more tutorials on Node.js, check out the following articles:

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about our products

About the author(s)

Kathleen Juell
Kathleen Juell
Author
See author profile

Former Developer at DigitalOcean community. Expertise in areas including Ubuntu, Docker, Ruby on Rails, Debian, and more.

Lisa Tagliaferri
Lisa Tagliaferri
Author
See author profile

Community and Developer Education expert. Former Senior Manager, Community at DigitalOcean. Focused on topics including Ubuntu 22.04, Ubuntu 20.04, Python, Django, and more.

Manikandan Kurup
Manikandan Kurup
Editor
Senior Technical Content Engineer I
See author profile

With over 6 years of experience in tech publishing, Mani has edited and published more than 75 books covering a wide range of data science topics. Known for his strong attention to detail and technical knowledge, Mani specializes in creating clear, concise, and easy-to-understand content tailored for developers.

Still looking for an answer?

Was this helpful?


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Thank you for the article, it’s very useful. I’d like to know what should be the Nginx setup if I wanted the second app accessed not through https://example.com/app2 but through https://example2.com? How to use different domains for different apps?

Why PM2 instead of systemctl?

Great tutorial! I have a WordPress site at /var/www/example.com/html (NGINX PHP) and I want my NodeJS apps to run in /var/www/example.com/html/nodejsapps How shall I do that?

Please could you explain the correct way to set up environment variables in production?

I have several sites/apps on the same Ubuntu 20.04 server with NGINX server blocks. The NodeJS apps are using different versions on NodeJS. Anyone who has experience/advice on how to install/configure nvm, volta or nvs in production to switch node versions and use different node versions for respective apps on the same Ubuntu server?

This comment has been deleted

I followed the instructions exactly and for some reason nodejs -v did not work but node -v did.

Thank you for this useful tutorial.

It’s possible to have one for Apache2 ?

Hi, thanks for the tutorial, it’s working great. However, I would like to be able to access the application from other computers on the LAN the server belongs to. At the moment, when I point to the IP of the server, I’m redirected to a standard Nginx landing page instead of the Node.js app. I guess there is an option in the server blocks to exclude an IP range ?

Hey guys are these tutorials still accurate? Visiting an http://your_domain in a browser gets refused because they use https by default now. I’ve used curl and I’m getting my content back but it doesn’t work in a browser. Am I correct?

Creative CommonsThis work is licensed under a Creative Commons Attribution-NonCommercial- ShareAlike 4.0 International License.
Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

The developer cloud

Scale up as you grow — whether you're running one virtual machine or ten thousand.

Get started for free

Sign up and get $200 in credit for your first 60 days with DigitalOcean.*

*This promotional offer applies to new accounts only.