Table of Contents
- Deployment Options Overview
- Deploying on a VPS (DigitalOcean Droplet)
- Runtime Management with PM2
- Monitoring and Logs with PM2
- Next Steps: NGINX and Docker
- Reference Diagrams
- Reference Tables
- 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
- In the DigitalOcean dashboard, choose Create > Droplets
- Select region (e.g.: New York / NYC1)
- Choose OS: Ubuntu (latest LTS version)
- Choose plan: Basic shared CPU, Regular disk (~$4/month, 10 GB storage)
- Authentication method: Password (less secure) or SSH key pair (recommended in production)
- 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:
| Route | Method | Description |
|---|---|---|
/ | GET | Form to enter a long URL |
/shorten_url | POST | Generates a unique short URL |
/expand/:short | GET | Displays the long version of the short URL |
/crash | GET | Simulates 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:
| Option | Description |
|---|---|
name | Application identifier in PM2 |
script | Path to the JavaScript entry file |
watch | Directory to watch for automatic restarts |
env | Environment variables (non-sensitive only) |
node_args | Arguments passed directly to Node.js |
Security: Never store secrets (passwords, API keys, database credentials) in
ecosystem.config.jssince this file is typically committed to the Git repository. Use a.envfile 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:
| Panel | Content |
|---|---|
| Top left | Process list + real-time CPU/Memory usage |
| Top right | Real-time logs |
| Bottom left | Active requests, HTTP req/min |
| Bottom right | Metadata: 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:
| Instruction | Description |
|---|---|
FROM node:22-alpine | Lightweight Node.js base image (Alpine Linux) |
WORKDIR /usr/src/app | Working directory in the container |
RUN apk add --no-cache git | Install git temporarily to clone the repo |
RUN npm install | Install dependencies |
RUN npm install pm2 -g | Install PM2 in the image |
COPY .env ./src/.env | Copy local .env file into the image |
EXPOSE 3000 | Document the exposed port |
CMD ["pm2-runtime", ...] | Start command — pm2-runtime is designed for containers |
PM2 vs pm2-runtime difference:
pm2-runtimeis designed to run in the foreground in a Docker container, unlikepm2which 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
| Option | Control | Management | Cost | Use Cases |
|---|---|---|---|---|
| Bare metal server | Total | Full manual | Variable + hardware | Enterprises with dedicated infra |
| VPS (IaaS) | High | OS + stack | Low (~$4-20/month) | Personal projects, startups |
| PaaS | Medium | Application only | Medium | Fast deployment, no ops |
| Serverless | Low | Code only | Pay-per-use | Lightweight APIs, event-driven tasks |
| Docker + VPS | High | Dockerfile + orchestration | Low | Reproducibility, microservices |
Important Environment Variables
| Variable | Values | Description |
|---|---|---|
NODE_ENV | production / development | Enables optimizations and correct Express behavior |
PORT | 3000 (default) | Node server listening port |
APP_DEBUG | 0 / 1 | Enables/disables application debug mode |
Security reminder:
NODE_ENV=productionin ecosystem is safe. Secrets (API keys, DB passwords) must be in a.envfile that is not committed.
PM2 Reference Commands
| Command | Description |
|---|---|
pm2 start <script> | Start an application |
pm2 start ecosystem.config.js | Start via ecosystem file |
pm2 list | List 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 logs | Display logs |
pm2 logs --lines 50 | Display last 50 lines |
pm2 monit | Open the monitoring dashboard |
pm2 startup | Configure automatic start on boot |
pm2 save | Save current process state |
pm2 ecosystem | Generate an ecosystem.config.js file |
Performance Metrics — Apache Bench
| Metric | Description |
|---|---|
| Requests per second | Number of requests processed per second |
| Time per request | Average time per request (in ms) |
| Transfer rate | Average throughput in KB/s |
| Failed requests | Number of failed requests |
| Concurrency Level | Number 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:
- Model comparison (IaaS, PaaS, Serverless, Docker) to choose the right approach
- VPS with DigitalOcean: Ubuntu server setup, installing Node via
nvm, deploying from Git - PM2 as process manager: automatic restart, ecosystem config, startup on boot
- Monitoring:
pm2 logs,pm2 monit, load testing with Apache Bench - NGINX as a reverse proxy in front of Node.js
- 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