
How to Set Up a Node.js Application for Production on Ubuntu 24
Setting up a Node.js application for production on Ubuntu 24 is a critical skill that every developer and system administrator should master. Unlike development environments where quick restarts and manual processes are acceptable, production deployments require robust process management, security hardening, reverse proxy configuration, and automated startup procedures. This guide will walk you through the complete setup process, from initial server preparation to advanced monitoring configurations, while covering common pitfalls and troubleshooting scenarios that you’ll inevitably encounter in real-world deployments.
Understanding Node.js Production Requirements
Running Node.js in production differs significantly from development environments. Your application needs to handle crashes gracefully, restart automatically, serve multiple concurrent requests efficiently, and maintain security standards. The key components include:
- Process Management: Tools like PM2 or systemd to keep your application running
- Reverse Proxy: Nginx or Apache to handle static files and SSL termination
- Environment Configuration: Proper separation of development and production settings
- Security Measures: Firewall configuration, user permissions, and dependency management
- Monitoring and Logging: Application performance monitoring and centralized logging
Initial Server Setup and Node.js Installation
Start with a fresh Ubuntu 24 server. Whether you’re using a VPS or dedicated server, the initial setup process remains consistent.
First, update your system and install essential packages:
sudo apt update && sudo apt upgrade -y
sudo apt install curl wget gnupg2 software-properties-common apt-transport-https ca-certificates lsb-release -y
Install Node.js using NodeSource repository for the latest LTS version:
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt install nodejs -y
# Verify installation
node --version
npm --version
Create a dedicated user for your Node.js application. Never run production applications as root:
sudo adduser nodeapp
sudo usermod -aG sudo nodeapp
su - nodeapp
Application Deployment and Configuration
Let’s deploy a sample Express.js application to demonstrate the complete process. Create your application directory:
mkdir -p /home/nodeapp/myapp
cd /home/nodeapp/myapp
Here’s a basic Express application structure for production:
// app.js
const express = require('express');
const helmet = require('helmet');
const compression = require('compression');
const rateLimit = require('express-rate-limit');
const app = express();
const PORT = process.env.PORT || 3000;
// Security middleware
app.use(helmet());
app.use(compression());
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
app.use(limiter);
// Basic routes
app.get('/', (req, res) => {
res.json({
message: 'Production Node.js App Running',
environment: process.env.NODE_ENV,
timestamp: new Date().toISOString()
});
});
// Health check endpoint
app.get('/health', (req, res) => {
res.status(200).json({ status: 'OK', uptime: process.uptime() });
});
// Error handling
app.use((err, req, res, next) => {
console.error('Error:', err.stack);
res.status(500).json({ error: 'Something went wrong!' });
});
app.listen(PORT, '127.0.0.1', () => {
console.log(`Server running on port ${PORT}`);
});
module.exports = app;
Create the package.json file with production dependencies:
{
"name": "production-node-app",
"version": "1.0.0",
"description": "Production-ready Node.js application",
"main": "app.js",
"scripts": {
"start": "NODE_ENV=production node app.js",
"dev": "NODE_ENV=development nodemon app.js",
"test": "jest"
},
"dependencies": {
"express": "^4.18.2",
"helmet": "^7.0.0",
"compression": "^1.7.4",
"express-rate-limit": "^6.7.0"
},
"devDependencies": {
"nodemon": "^3.0.1",
"jest": "^29.5.0"
},
"engines": {
"node": ">=18.0.0"
}
}
Install dependencies and test the application:
npm install --only=production
NODE_ENV=production node app.js
Process Management with PM2
PM2 is the most popular process manager for Node.js applications in production. Install it globally:
sudo npm install -g pm2
Create a PM2 ecosystem file for advanced configuration:
// ecosystem.config.js
module.exports = {
apps: [{
name: 'myapp',
script: './app.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000
},
log_date_format: 'YYYY-MM-DD HH:mm Z',
error_file: './logs/err.log',
out_file: './logs/out.log',
log_file: './logs/combined.log',
time: true,
max_memory_restart: '1G',
node_args: '--max-old-space-size=1024'
}]
};
Create the logs directory and start your application:
mkdir logs
pm2 start ecosystem.config.js
pm2 save
pm2 startup
The startup command will provide a command to run as root – execute it to enable automatic startup on system reboot.
Nginx Reverse Proxy Configuration
Install and configure Nginx as a reverse proxy:
sudo apt install nginx -y
sudo systemctl start nginx
sudo systemctl enable nginx
Create a virtual host configuration:
sudo nano /etc/nginx/sites-available/myapp
Add the following configuration:
server {
listen 80;
server_name your-domain.com www.your-domain.com;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Static file serving (if needed)
location /static/ {
alias /home/nodeapp/myapp/public/;
expires 30d;
add_header Cache-Control "public, immutable";
}
}
Enable the site and test the configuration:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
SSL Certificate Setup with Let’s Encrypt
Install Certbot for free SSL certificates:
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d your-domain.com -d www.your-domain.com
Set up automatic renewal:
sudo crontab -e
# Add this line:
0 12 * * * /usr/bin/certbot renew --quiet
Firewall Configuration
Configure UFW (Uncomplicated Firewall) for basic security:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow 'Nginx Full'
sudo ufw --force enable
sudo ufw status
Performance Monitoring and Optimization
Install PM2 monitoring tools:
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 10M
pm2 set pm2-logrotate:retain 30
Here’s a comparison of different process management solutions:
Feature | PM2 | Forever | Systemd | Docker |
---|---|---|---|---|
Cluster Mode | ✓ | ✗ | ✗ | ✓ (with orchestration) |
Built-in Monitoring | ✓ | ✗ | ✗ | ✗ |
Log Management | ✓ | ✓ | ✓ | ✓ |
Zero-downtime Reload | ✓ | ✗ | ✗ | ✓ |
Resource Usage | Medium | Low | Low | Medium-High |
Environment Variables and Security
Create a secure environment configuration:
sudo nano /home/nodeapp/myapp/.env
NODE_ENV=production
PORT=3000
DB_HOST=localhost
DB_USER=myapp_user
DB_PASS=secure_password_here
JWT_SECRET=your_jwt_secret_here
SESSION_SECRET=your_session_secret_here
Secure the environment file:
sudo chown nodeapp:nodeapp /home/nodeapp/myapp/.env
sudo chmod 600 /home/nodeapp/myapp/.env
Install and configure dotenv for environment variable management:
npm install dotenv --save
Update your app.js to use environment variables:
require('dotenv').config();
// ... rest of your application code
Database Configuration and Security
If you’re using MongoDB, install and secure it:
sudo apt install -y mongodb
sudo systemctl start mongodb
sudo systemctl enable mongodb
# Create application-specific database user
mongo
use myapp
db.createUser({
user: "myapp_user",
pwd: "secure_password_here",
roles: ["readWrite"]
})
exit
For PostgreSQL setup:
sudo apt install postgresql postgresql-contrib -y
sudo -u postgres createuser --interactive myapp_user
sudo -u postgres createdb myapp_db
sudo -u postgres psql -c "ALTER USER myapp_user PASSWORD 'secure_password_here';"
Common Issues and Troubleshooting
Here are the most frequent problems you’ll encounter and their solutions:
Issue 1: Application crashes after deployment
# Check PM2 logs
pm2 logs myapp
# Check system logs
sudo journalctl -u nginx -f
sudo tail -f /var/log/nginx/error.log
Issue 2: Port 3000 already in use
# Find process using the port
sudo lsof -i :3000
sudo netstat -tulpn | grep :3000
# Kill the process
sudo kill -9 PID
Issue 3: Nginx 502 Bad Gateway
- Verify Node.js application is running:
pm2 status
- Check if the application is listening on 127.0.0.1:3000
- Verify Nginx configuration:
sudo nginx -t
- Check firewall rules:
sudo ufw status
Issue 4: High memory usage
# Monitor memory usage
pm2 monit
# Restart application if memory limit exceeded
pm2 restart myapp
# Adjust memory limit in ecosystem.config.js
max_memory_restart: '512M'
Performance Optimization Strategies
Enable Node.js performance optimizations:
# Update ecosystem.config.js with performance settings
module.exports = {
apps: [{
name: 'myapp',
script: './app.js',
instances: 'max',
exec_mode: 'cluster',
node_args: [
'--max-old-space-size=2048',
'--optimize-for-size'
],
env: {
NODE_ENV: 'production',
UV_THREADPOOL_SIZE: 128
}
}]
};
Configure Nginx for better performance:
# Add to /etc/nginx/nginx.conf in http block
worker_processes auto;
worker_connections 1024;
keepalive_timeout 65;
client_max_body_size 50M;
# Enable caching
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=10g
inactive=60m use_temp_path=off;
Backup and Deployment Automation
Create a deployment script for easy updates:
#!/bin/bash
# deploy.sh
APP_DIR="/home/nodeapp/myapp"
BACKUP_DIR="/home/nodeapp/backups/$(date +%Y%m%d_%H%M%S)"
echo "Creating backup..."
mkdir -p $BACKUP_DIR
cp -r $APP_DIR $BACKUP_DIR
echo "Pulling latest code..."
cd $APP_DIR
git pull origin main
echo "Installing dependencies..."
npm ci --only=production
echo "Running tests..."
npm test
echo "Reloading application..."
pm2 reload myapp
echo "Deployment complete!"
Make the script executable:
chmod +x deploy.sh
Monitoring and Logging Best Practices
Set up comprehensive logging:
// logger.js
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'myapp' },
transports: [
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/combined.log' })
]
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple()
}));
}
module.exports = logger;
Install monitoring tools:
npm install winston --save
pm2 install pm2-server-monit
This production setup provides a robust foundation for running Node.js applications on Ubuntu 24. The configuration handles automatic restarts, load balancing through clustering, security hardening, and comprehensive monitoring. For additional resources, check the official Node.js production guide and PM2 documentation. Remember to regularly update your dependencies, monitor performance metrics, and implement proper backup strategies for production reliability.

This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.
This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.