Intermediate

Node.js: Deploying Applications

Deploy Node apps to a VPS, manage runtime with PM2, monitor logs and look ahead to NGINX/Docker.

Table of Contents

  1. Deployment Options Overview
  2. Deploying on a VPS (DigitalOcean Droplet)
  3. Runtime Management with PM2
  4. Monitoring and Logs with PM2
  5. Next Steps: NGINX and Docker
  6. Reference Diagrams
  7. Reference Tables
  8. Key Code Snippets

1. Deployment Options Overview

Available Hosting Models

A back-end web development project typically starts on a local computer before being deployed to a server accessible from the internet. Several hosting models exist, each offering a different level of control and management.

Infrastructure as a Service (IaaS)

With an IaaS model, you essentially rent a server from a provider that manages the underlying hardware and network infrastructure. You retain full control over the server resources. These servers are often VPS (Virtual Private Servers) — virtual machines hosted in the provider’s data centers.

IaaS examples:

  • DigitalOcean (Droplets)
  • Linode (now Akamai Cloud)
  • AWS EC2
  • Azure VMs
  • GCP Compute Engine

Platform as a Service (PaaS)

A PaaS model goes further by also managing the deployment and execution software tools. You don’t need to connect directly to the server to configure it.

PaaS examples:

  • Render, Vercel, Netlify, Heroku
  • AWS Elastic Beanstalk
  • Azure App Service
  • GCP Cloud Run

Serverless

With a serverless solution, there is technically still a server, but you don’t have to worry about it. You provide the application code, and the provider manages everything else. Ideal for simple functions, lightweight APIs, or on-demand workloads.


2. Deploying on a VPS (DigitalOcean Droplet)

Creating a Droplet

  1. In the DigitalOcean dashboard, choose Create > Droplets
  2. Select region (e.g.: New York / NYC1)
  3. Choose OS: Ubuntu (latest LTS version)
  4. Choose plan: Basic shared CPU, Regular disk (~$4/month, 10 GB storage)
  5. Authentication method: Password (less secure) or SSH key pair (recommended in production)
  6. Name the Droplet (e.g.: nodejs-deploy-demo)

Essential Unix Commands

# List current directory contents
ls -la

# Display current directory (Print Working Directory)
pwd

# Change directory
cd /var/www

# Create a directory
mkdir www

# Connect to server via SSH
ssh root@<IP_ADDRESS>

Installing Node.js with nvm

# Install nvm (Node Version Manager) — copy command from official Node docs
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

# Reload the shell without disconnecting
source ~/.bashrc

# Install a specific version of Node
nvm install 22.7

# Verify installation
node -v
npm -v

Cloning and Launching the Application

# Clone the Git repository
cd /var/www
git clone https://github.com/jonfriskics/url_shortener

cd url_shortener

# Install dependencies
npm install

# Create the .env file
echo "APP_DEBUG=0" > src/.env

# Launch the Node.js application
node --experimental-sqlite --env-file=src/.env src/app.mjs

The application runs on port 3000 and is accessible at http://<IP_ADDRESS>:3000.

Demo Application — URL Shortener

The demo app is a URL shortener with the following routes:

RouteMethodDescription
/GETForm to enter a long URL
/shorten_urlPOSTGenerates a unique short URL
/expand/:shortGETDisplays the long version of the short URL
/crashGETSimulates a crash (calls process.exit(1))

The APP_DEBUG=1 environment variable displays a “Debug mode enabled” banner in the interface.

Rebuilding a Droplet for Practice

DigitalOcean offers a Rebuild Droplet option (in the Destroy panel) that starts from a fresh server. This is ideal for repeating installation steps and becoming comfortable with server commands.

Note: After a rebuild, remove the old SSH signature to avoid the “man-in-the-middle” error:

ssh-keygen -R <IP_ADDRESS>

3. Runtime Management with PM2

Why PM2?

A classic Node.js server launched with node app.mjs stops permanently on crash. PM2 (Process Manager 2) monitors Node processes and automatically restarts them. It is an essential tool for keeping a Node application running in production.

Installing PM2

# Install PM2 globally (available from any directory)
npm install pm2 -g

Core PM2 Commands

# Start an application (with node-args if needed)
pm2 start src/app.mjs --node-args="--experimental-sqlite --env-file=src/.env"

# List running applications
pm2 list

# Stop an application
pm2 stop app

# Delete a PM2 entry
pm2 delete app

# Zero-downtime reload
pm2 reload deploy-demo

# Restart
pm2 restart deploy-demo

Ecosystem Configuration File

Rather than managing launch options on the command line, PM2 uses an ecosystem file:

# Generate an ecosystem.config.js file
pm2 ecosystem

Demo ecosystem.config.js file:

module.exports = {
  apps: [{
    name: 'deploy-demo',
    script: './src/app.mjs',
    watch: './src',           // Watch for file changes
    env: {
      PORT: 3000,
      NODE_ENV: 'production'  // Enable production optimizations
      // Never put secrets here!
    },
    node_args: '--experimental-sqlite --env-file=src/.env'
  }]
};

Important ecosystem options:

OptionDescription
nameApplication identifier in PM2
scriptPath to the JavaScript entry file
watchDirectory to watch for automatic restarts
envEnvironment variables (non-sensitive only)
node_argsArguments passed directly to Node.js

Security: Never store secrets (passwords, API keys, database credentials) in ecosystem.config.js since this file is typically committed to the Git repository. Use a .env file for sensitive values.

Starting with the Ecosystem File

# Start via ecosystem
pm2 start ecosystem.config.js

# Reload with date format in logs
pm2 reload ecosystem.config.js --log-date-format "YYYY-MM-DD HH:mm"

Automatic Restart After Crash

PM2 automatically detects when Node.js stops unexpectedly and relaunches the process. Without PM2, a crash makes the application inaccessible until manual intervention. With PM2, recovery is transparent to users.

Automatic Restart After Server Reboot

# Configure PM2 to start on system boot
pm2 startup

# Save the current state of processes
pm2 save

These commands create a systemd service (/etc/systemd/system/pm2-root.service) and enable it with systemctl enable pm2-root.

To disable automatic restart of an app:

pm2 stop deploy-demo
pm2 save
# then reboot

Accessing Environment Variables in Node.js Code

// In app.mjs — access variables from ecosystem.config.js or .env
const debug = process.env.APP_DEBUG.toString()
const port = process.env.PORT || 3000
const nodeEnv = process.env.NODE_ENV

4. Monitoring and Logs with PM2

Logs with PM2

PM2 automatically stores logs in /root/.pm2/pm2.log.

# Display latest log lines
pm2 logs

# Display last 50 lines
pm2 logs --lines 50

# With custom timestamp
pm2 logs --log-date-format "YYYY-MM-DD HH:mm"

PM2 logs distinguish between:

  • Generic PM2 messages (start, crash, restart)
  • Process-specific logs (with application name as prefix)

Monitoring Dashboard — pm2 monit

pm2 monit

Displays a 4-panel dashboard in the terminal:

PanelContent
Top leftProcess list + real-time CPU/Memory usage
Top rightReal-time logs
Bottom leftActive requests, HTTP req/min
Bottom rightMetadata: uptime, script path

Navigation: arrows to select a panel, to scroll. Quit with Ctrl+C or Q.

Load Testing with Apache Bench

# Update apt packages
sudo apt update
sudo apt upgrade

# Install Apache Bench
sudo apt-get install apache2-utils

# Basic test: 1,000 requests
ab -n 1000 -c 10 http://<IP_ADDRESS>:3000/

# Intensive test: 10,000 requests, 500 simultaneous connections
ab -n 10000 -c 500 http://<IP_ADDRESS>:3000/

The -n (total requests) and -c (concurrent requests) options allow simulating multiple simultaneous users. Combining ab with pm2 monit in two side-by-side terminals lets you observe the traffic impact in real time.


5. Next Steps: NGINX and Docker

NGINX as a Reverse Proxy

NGINX is an HTTP server acting as a reverse proxy: it sits between clients and the Node.js server. Requests enter NGINX, which forwards them to Node, then returns the responses.

Client  ──→  NGINX (port 80/443)  ──→  Node.js (port 3000)

Advantage: NGINX handles “server” aspects (SSL/TLS, rate limiting, load balancing, compression) so the Node application focuses on business logic.

NGINX Installation and Configuration

# Update and install NGINX
sudo apt update && sudo apt upgrade
sudo apt-get install nginx

# Check the configuration
sudo nginx -T

# Manage the NGINX service
sudo systemctl start nginx
sudo systemctl stop nginx
sudo systemctl restart nginx

Basic configuration — /etc/nginx/sites-available/default:

server {
    listen 80;

    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;
    }
}

After modifying, reload NGINX:

sudo systemctl reload nginx

Deploying with Docker

Docker is an alternative to manual deployment: you package the code, dependencies, and configuration into a Docker image that can be run on any machine with the Docker Engine installed.

Note: This approach doesn’t require installing Node.js directly on the server — Node is included in the Docker image.

Demo Dockerfile

FROM node:22-alpine

WORKDIR /usr/src/app

RUN apk add --no-cache git && \
    git clone https://github.com/jonfriskics/url_shortener \
    . && \
    apk del git

RUN npm install

RUN npm install pm2 -g

COPY .env ./src/.env

EXPOSE 3000

CMD ["pm2-runtime", "start", "ecosystem.config.js"]

Key Dockerfile instructions:

InstructionDescription
FROM node:22-alpineLightweight Node.js base image (Alpine Linux)
WORKDIR /usr/src/appWorking directory in the container
RUN apk add --no-cache gitInstall git temporarily to clone the repo
RUN npm installInstall dependencies
RUN npm install pm2 -gInstall PM2 in the image
COPY .env ./src/.envCopy local .env file into the image
EXPOSE 3000Document the exposed port
CMD ["pm2-runtime", ...]Start command — pm2-runtime is designed for containers

PM2 vs pm2-runtime difference: pm2-runtime is designed to run in the foreground in a Docker container, unlike pm2 which puts itself in the background.

Essential Docker Commands

# Build the Docker image
docker build -t nodejs-deploy-demo .

# Run the container
docker run -d -p 3000:3000 --name deploy-demo nodejs-deploy-demo

# See running containers
docker ps

# Stop the container
docker stop deploy-demo

# Remove the container
docker rm deploy-demo

6. Reference Diagrams

Node.js Deployment Strategies

flowchart TD
    A[Node.js Application] --> B{Deployment choice}

    B --> C[IaaS / VPS]
    B --> D[PaaS]
    B --> E[Serverless]
    B --> F[Containerization]

    C --> C1[DigitalOcean Droplet\nAWS EC2 / Azure VM]
    C1 --> C2[Node.js + nvm]
    C2 --> C3[PM2 Process Manager]
    C3 --> C4[NGINX Reverse Proxy]

    D --> D1[Render / Heroku\nAWS Elastic Beanstalk\nAzure App Service]

    E --> E1[AWS Lambda\nAzure Functions\nGCP Cloud Run Functions]

    F --> F1[Docker Image]
    F1 --> F2[Docker Container\n+ PM2 Runtime]
    F2 --> F3[VPS with Docker Engine\nKubernetes / Cloud Run]

Node.js Deployment Pipeline

flowchart LR
    Dev[Developer\nLocal code] -->|git push| Repo[Git Repository\nGitHub / GitLab]
    Repo -->|CI trigger| CI[CI/CD Pipeline]
    CI --> Test[Automated tests\nnpm test]
    Test --> Build[Build\nnpm run build]
    Build --> Docker[Build Docker Image\ndocker build]
    Docker --> Registry[Container Registry\nDocker Hub / ECR]
    Registry --> Deploy[Deployment\ndocker run / pm2 reload]
    Deploy --> Prod[Production\nVPS / Cloud]
    Prod --> Monitor[Monitoring\npm2 monit / logs]

Zero-Downtime Deployment Flow with PM2

sequenceDiagram
    participant Dev as Developer
    participant Server as VPS Server
    participant PM2 as PM2 Manager
    participant App as Node.js App

    Dev->>Server: git pull (new changes)
    Dev->>PM2: pm2 reload ecosystem.config.js
    PM2->>App: Start new instance (v2)
    Note over PM2,App: Both instances run in parallel
    PM2->>App: Transfer traffic to v2
    PM2->>App: Stop old instance (v1)
    Note over PM2,App: Zero downtime!
    PM2-->>Dev: Reload complete

Architecture with NGINX Reverse Proxy

flowchart LR
    Client[Browser\nClient] -->|HTTP :80| NGINX[NGINX\nReverse Proxy]
    NGINX -->|proxy_pass :3000| Node[Node.js App\nExpress + PM2]
    Node --> DB[(SQLite\nDatabase)]

    subgraph VPS [VPS Server]
        NGINX
        Node
        DB
    end

7. Reference Tables

Deployment Options Compared

OptionControlManagementCostUse Cases
Bare metal serverTotalFull manualVariable + hardwareEnterprises with dedicated infra
VPS (IaaS)HighOS + stackLow (~$4-20/month)Personal projects, startups
PaaSMediumApplication onlyMediumFast deployment, no ops
ServerlessLowCode onlyPay-per-useLightweight APIs, event-driven tasks
Docker + VPSHighDockerfile + orchestrationLowReproducibility, microservices

Important Environment Variables

VariableValuesDescription
NODE_ENVproduction / developmentEnables optimizations and correct Express behavior
PORT3000 (default)Node server listening port
APP_DEBUG0 / 1Enables/disables application debug mode

Security reminder: NODE_ENV=production in ecosystem is safe. Secrets (API keys, DB passwords) must be in a .env file that is not committed.

PM2 Reference Commands

CommandDescription
pm2 start <script>Start an application
pm2 start ecosystem.config.jsStart via ecosystem file
pm2 listList processes
pm2 stop <name>Stop a process
pm2 delete <name>Remove a process from the list
pm2 reload <name>Zero-downtime reload
pm2 restart <name>Full restart
pm2 logsDisplay logs
pm2 logs --lines 50Display last 50 lines
pm2 monitOpen the monitoring dashboard
pm2 startupConfigure automatic start on boot
pm2 saveSave current process state
pm2 ecosystemGenerate an ecosystem.config.js file

Performance Metrics — Apache Bench

MetricDescription
Requests per secondNumber of requests processed per second
Time per requestAverage time per request (in ms)
Transfer rateAverage throughput in KB/s
Failed requestsNumber of failed requests
Concurrency LevelNumber of simulated simultaneous connections

8. Key Code Snippets

Basic Express Application (URL Shortener)

import express from 'express'
import { join } from 'node:path'
import sqlite from 'node:sqlite'
import crypto from 'crypto'

const app = express()
const database = new sqlite.DatabaseSync('./urls.db');

app.use(express.urlencoded({ extended: true }))
app.use(express.static(join(process.cwd(), 'src', 'public')))

app.set('view engine', 'ejs')
app.set('views', 'src/public/views')

// Create table if it doesn't exist
database.exec(`
  CREATE TABLE IF NOT EXISTS urls(
    key INTEGER PRIMARY KEY,
    long_url TEXT,
    short_url TEXT
  ) STRICT
`);

// Main route
app.get('/', (request, response) => {
  const query = database.prepare('SELECT * FROM urls');
  const debug = process.env.APP_DEBUG.toString()
  response.status(200).render('shorten', { data: { sqlite: query.all(), debug: debug } })
})

// Shortening route
app.post('/shorten_url', (request, response) => {
  const long_url = request.body.url_input
  const short_url = crypto
    .createHash('sha256')
    .update(long_url + Date.now())
    .digest('base64url')
    .substring(0, 8)
  // ... database insertion logic
})

// Crash simulation route (PM2 demonstration only)
app.get('/crash', (request, response) => {
  process.exit(1)
})

app.listen(process.env.PORT || 3000)

Complete ecosystem.config.js

module.exports = {
  apps: [{
    name: 'deploy-demo',
    script: './src/app.mjs',
    watch: './src',           // Watch for file changes
    env: {
      PORT: 3000,
      NODE_ENV: 'production'  // Enable production optimizations
      // Never put secrets here!
    },
    node_args: '--experimental-sqlite --env-file=src/.env'
  }]
};

Node.js Dockerfile with PM2

# Lightweight Node.js base image based on Alpine Linux
FROM node:22-alpine

# Set working directory in the container
WORKDIR /usr/src/app

# Install git, clone repo, then remove git (clean layer)
RUN apk add --no-cache git && \
    git clone https://github.com/jonfriskics/url_shortener . && \
    apk del git

# Install application dependencies
RUN npm install

# Install PM2 globally for process management
RUN npm install pm2 -g

# Copy the .env file into the image
COPY .env ./src/.env

# Document the port exposed by the container
EXPOSE 3000

# Use pm2-runtime (designed for containers, runs in foreground)
CMD ["pm2-runtime", "start", "ecosystem.config.js"]

Complete NGINX Configuration

server {
    listen 80;
    server_name your-domain.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;
    }
}

Server Setup Script (Automation)

#!/bin/bash
# Complete Node.js server setup script

# 1. Update system
sudo apt update && sudo apt upgrade -y

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

# 3. Install Node.js
nvm install 22.7

# 4. Install PM2
npm install pm2 -g

# 5. Create web directory
mkdir -p /var/www && cd /var/www

# 6. Install NGINX (optional)
sudo apt-get install -y nginx
# Configure /etc/nginx/sites-available/default
sudo systemctl reload nginx

Summary

This course covers the complete deployment cycle of a Node.js application:

  1. Model comparison (IaaS, PaaS, Serverless, Docker) to choose the right approach
  2. VPS with DigitalOcean: Ubuntu server setup, installing Node via nvm, deploying from Git
  3. PM2 as process manager: automatic restart, ecosystem config, startup on boot
  4. Monitoring: pm2 logs, pm2 monit, load testing with Apache Bench
  5. NGINX as a reverse proxy in front of Node.js
  6. Docker for containerizing the application with integrated PM2 (pm2-runtime)

The Node.js + PM2 + NGINX + Docker combination constitutes a robust, reproducible, and crash-resilient deployment stack for production.


Search Terms

node.js · deploying · applications · apis · microservices · backend · full-stack · web · pm2 · deployment · nginx · commands · application · configuration · docker · droplet · reference · apache · automatic · bench · dockerfile · ecosystem · environment · essential

Interested in this course?

Contact us to book it or get a custom training plan for your team.