Installing Node.js
There are several ways to install Node.js. Using NVM (Node Version Manager) is the recommended approach because it lets you run multiple Node versions side by side.
Method 1: NVM (Recommended)
# Install NVM
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
# Reload shell
source ~/.bashrc # or ~/.zshrc
# Install the latest LTS version
nvm install --lts
# Install a specific version
nvm install 20
nvm install 18.19.0
# Switch between versions
nvm use 20
nvm use 18
# Set a default version
nvm alias default 20
# List installed versions
nvm ls
# List available remote versions
nvm ls-remote --lts
Method 2: NodeSource Repository
# Ubuntu/Debian — Node.js 20.x LTS
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install nodejs -y
# CentOS/AlmaLinux
curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -
sudo dnf install nodejs -y
# Verify installation
node --version
npm --version
Method 3: Official Binaries
# Download and extract (useful for Docker or custom setups)
curl -fsSL https://nodejs.org/dist/v20.11.0/node-v20.11.0-linux-x64.tar.xz \
| sudo tar -xJ -C /usr/local --strip-components=1
node --version
Always use an LTS (Long Term Support) version for production. Even-numbered major versions (18, 20, 22) are LTS releases. Odd-numbered versions (19, 21, 23) are short-lived "Current" releases meant for testing new features. Check the Node.js website for the current LTS schedule.
npm Essentials
npm (Node Package Manager) is installed alongside Node.js and manages your project's dependencies.
Project Initialization & Dependencies
# Initialize a new project
npm init -y
# Install a dependency (saved to package.json)
npm install express
# Install a dev dependency
npm install --save-dev nodemon eslint
# Install all dependencies from package.json
npm install
# Install production dependencies only (CI/deployment)
npm ci --only=production
# Update packages
npm update
# Check for outdated packages
npm outdated
# Audit for security vulnerabilities
npm audit
npm audit fix
npm Scripts
// package.json
{
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"build": "webpack --mode production",
"test": "jest --coverage",
"lint": "eslint src/",
"migrate": "node scripts/migrate.js"
}
}
# Run scripts
npm start # runs "start" script
npm run dev # runs "dev" script
npm test # shortcut for "test" script
npm run build # runs "build" script
package-lock.json
package-lock.json pins exact dependency versions for reproducible builds. Always commit it to version control. Use npm ci (not npm install) in CI/CD pipelines — it's faster and installs exact versions from the lock file.
Building an Express Application
// server.js
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Security headers
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
res.setHeader('X-XSS-Protection', '1; mode=block');
next();
});
// Routes
app.get('/', (req, res) => {
res.json({ status: 'ok', message: 'Server is running' });
});
app.get('/health', (req, res) => {
res.status(200).json({ status: 'healthy', uptime: process.uptime() });
});
// Error handling middleware (must be last)
app.use((err, req, res, next) => {
console.error(`[${new Date().toISOString()}] Error:`, err.message);
res.status(err.status || 500).json({
error: process.env.NODE_ENV === 'production'
? 'Internal Server Error'
: err.message
});
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Environment Variables
# .env file (never commit this!)
NODE_ENV=production
PORT=3000
DB_HOST=localhost
DB_USER=appuser
DB_PASS=Str0ng!P@ss
DB_NAME=myapp_production
SESSION_SECRET=a-very-long-random-string
API_KEY=your-api-key-here
// Load .env file using dotenv
// npm install dotenv
require('dotenv').config();
// Access variables
const dbHost = process.env.DB_HOST;
const port = process.env.PORT || 3000;
// Validate required variables at startup
const required = ['DB_HOST', 'DB_USER', 'DB_PASS', 'SESSION_SECRET'];
for (const key of required) {
if (!process.env[key]) {
console.error(`Missing required env var: ${key}`);
process.exit(1);
}
}
Production Deployment with PM2
PM2 is a production process manager for Node.js that handles automatic restarts, clustering, log management, and monitoring.
Installing & Using PM2
# Install PM2 globally
npm install -g pm2
# Start your application
pm2 start server.js --name myapp
# Start with environment variables
pm2 start server.js --name myapp --env production
# Cluster mode (use all CPU cores)
pm2 start server.js --name myapp -i max
# Specify number of instances
pm2 start server.js --name myapp -i 4
# List running processes
pm2 list
# Monitor in real-time
pm2 monit
# View logs
pm2 logs
pm2 logs myapp
pm2 logs myapp --lines 100
# Restart / Reload / Stop
pm2 restart myapp
pm2 reload myapp # Zero-downtime reload (cluster mode)
pm2 stop myapp
pm2 delete myapp
# Save process list (survives reboot)
pm2 save
# Set PM2 to start on boot
pm2 startup
# Run the command it outputs, then:
pm2 save
PM2 Ecosystem File
// ecosystem.config.js
module.exports = {
apps: [{
name: 'myapp',
script: 'server.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'development',
PORT: 3000
},
env_production: {
NODE_ENV: 'production',
PORT: 3000
},
max_memory_restart: '500M',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
error_file: '/var/log/pm2/myapp-error.log',
out_file: '/var/log/pm2/myapp-out.log',
merge_logs: true,
watch: false,
max_restarts: 10,
restart_delay: 5000
}]
};
# Start with ecosystem file
pm2 start ecosystem.config.js --env production
Alternative: systemd Service
# /etc/systemd/system/myapp.service
[Unit]
Description=My Node.js Application
After=network.target
[Service]
Type=simple
User=deploy
WorkingDirectory=/var/www/myapp
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal
Environment=NODE_ENV=production
Environment=PORT=3000
[Install]
WantedBy=multi-user.target
# Enable and start
sudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myapp
# Check status
sudo systemctl status myapp
journalctl -u myapp -f
Nginx Reverse Proxy for Node.js
# /etc/nginx/sites-available/myapp
server {
listen 80;
server_name example.com www.example.com;
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;
# Timeouts for long-running requests
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Serve static files directly (bypass Node)
location /static/ {
alias /var/www/myapp/public/;
expires 7d;
add_header Cache-Control "public, immutable";
}
}
# Enable and test
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Node.js can serve HTTP directly, but Nginx in front provides: SSL termination, static file serving (faster than Node), gzip compression, load balancing across cluster instances, rate limiting, and security headers. Node handles application logic; Nginx handles HTTP.
Troubleshooting Node.js
Common Errors
# "Error: Cannot find module 'express'"
# Dependencies not installed
npm install
# Or in production:
npm ci
# "EACCES: permission denied"
# Don't use sudo with npm! Fix permissions:
sudo chown -R $(whoami) ~/.npm
# Or use NVM (avoids permission issues entirely)
# "EADDRINUSE: address already in use :::3000"
# Another process is using that port
lsof -i :3000
kill -9 <PID>
# Or use a different port:
PORT=3001 node server.js
# "ECONNREFUSED" connecting to database
# Database isn't running or wrong host/port
systemctl status mysql
# Check connection settings in .env
# "JavaScript heap out of memory"
# Increase Node's memory limit
node --max-old-space-size=4096 server.js
# In PM2:
pm2 start server.js --node-args="--max-old-space-size=4096"
Debugging
# Run with Node's built-in debugger
node --inspect server.js
# Opens debugger on chrome://inspect in Chrome
# Run with breakpoint on first line
node --inspect-brk server.js
# Debug a specific issue with verbose logging
DEBUG=express:* node server.js
# Add debug logging to your app
const debug = require('debug')('myapp:server');
debug('Server starting on port %d', port);
Memory Leaks
// Monitor memory usage in your app
setInterval(() => {
const used = process.memoryUsage();
console.log({
rss: `${Math.round(used.rss / 1024 / 1024)}MB`,
heapTotal: `${Math.round(used.heapTotal / 1024 / 1024)}MB`,
heapUsed: `${Math.round(used.heapUsed / 1024 / 1024)}MB`,
external: `${Math.round(used.external / 1024 / 1024)}MB`
});
}, 30000);
// Common causes of memory leaks:
// - Event listeners not being removed
// - Growing arrays/objects in module scope
// - Unclosed database connections
// - Large file reads without streams
Unhandled Errors & Crashes
// Catch unhandled promise rejections (Node 15+ crashes on these)
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
// In production, log and exit gracefully
process.exit(1);
});
// Catch uncaught exceptions
process.on('uncaughtException', (err) => {
console.error('Uncaught Exception:', err);
process.exit(1);
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM received. Shutting down gracefully...');
server.close(() => {
console.log('Server closed.');
process.exit(0);
});
// Force exit after 10 seconds
setTimeout(() => process.exit(1), 10000);
});
Node.js Security
- Never expose .env files — add to .gitignore, restrict via web server
- Validate all input — use libraries like
joiorzodfor schema validation - Use Helmet.js —
app.use(require('helmet')())sets security headers automatically - Rate limit APIs — use
express-rate-limitto prevent abuse - Keep dependencies updated — run
npm auditregularly - Use parameterized queries — never concatenate user input into SQL strings
- Set NODE_ENV=production — disables verbose error output and enables optimizations
// Security middleware stack
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const cors = require('cors');
app.use(helmet());
app.use(cors({ origin: 'https://example.com' }));
app.use(rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
message: 'Too many requests, please try again later.'
}));
Deployment Checklist
NODE_ENV=productionis set- Dependencies installed with
npm ci --only=production - Process manager (PM2 or systemd) configured with auto-restart
- Nginx reverse proxy with SSL termination
- Log rotation configured (PM2 logrotate or journald)
- Health check endpoint (
/health) available - Graceful shutdown handlers for SIGTERM/SIGINT
- Environment variables validated at startup
- Error handling middleware catches all errors
npm auditpasses with no critical vulnerabilities
For zero-downtime deployments, use PM2 in cluster mode with pm2 reload (not restart). This spins up new workers before killing old ones. Combine with Nginx upstream health checks for maximum reliability. For containerized deployments, see our Docker guide.