DEV Community

Alex Chen
Alex Chen

Posted on

How to Set Up a Node.js Server from Scratch (2026)

How to Set Up a Node.js Server from Scratch (2026)

Complete guide: fresh server → running Node.js app with SSL, auto-restart, and monitoring.

Prerequisites

  • A VPS (DigitalOcean $4/mo, Hetzner, Linode, etc.)
  • A domain name (optional but recommended)
  • SSH access to the server

Step 1: Initial Server Setup

# SSH into your server
ssh root@your-server-ip

# Update everything
apt update && apt upgrade -y

# Create a non-root user (security best practice)
adduser deploy
usermod -aG sudo deploy

# Switch to new user (or use su - deploy)
su - deploy

# Install essential tools
sudo apt install -y curl wget git vim ufw software-properties-common build-essential
Enter fullscreen mode Exit fullscreen mode

Step 2: Install Node.js via NVM

# Don't use apt for Node.js! It's outdated.
# Use nvm instead:

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash
source ~/.bashrc

nvm install 22          # Install latest LTS
nvm use 22              # Use it
nvm alias default 22    # Make default

node -v                 # v22.x.x
npm -v                  # 10.x.x

# Global packages I always need
npm install -g pm2 nodemon
Enter fullscreen mode Exit fullscreen mode

Step 3: Firewall Setup

# Configure UFW firewall
sudo ufw allow OpenSSH        # Port 22 (SSH)
sudo ufw allow 80             # HTTP
sudo ufw allow 443            # HTTPS
sudo ufw enable               # Enable firewall
sudo ufw status verbose       # Check rules

# Optional: Allow additional ports
# sudo ufw allow 3000         # If your app runs on port 3000 directly
Enter fullscreen mode Exit fullscreen mode

Step 4: Deploy Your App

# Clone your repo (or copy files)
cd /var/www/
sudo mkdir myapp && cd myapp
sudo chown deploy:deploy .
git clone https://github.com/you/your-app.git .

# Install dependencies
npm ci --production   # ci = clean install (respects package-lock.json)

# If TypeScript:
npm run build

# Test it works
npm start &
# Test with: curl http://localhost:3000
kill %1
Enter fullscreen mode Exit fullscreen mode

Step 5: Process Manager (PM2)

# Start your app with PM2
pm2 start npm --name "myapp" -- start

# Check status
pm2 list
pm2 logs myapp           # View logs
pm2 monit                # Real-time monitoring dashboard

# Useful commands
pm2 restart myapp        # Restart
pm2 stop myapp           # Stop
pm2 delete myapp         # Remove
pm2 info myapp           # Detailed info

# Save process list (survives reboot)
pm2 startup              # Shows command to run
# Run the command it shows:
sudo env PATH=$PATH:/home/deploy/.nvm/versions/node/v22.x/bin pm2 startup systemd -u deploy --hp /home/deploy
pm2 save                 # Save current process list

# Now PM2 starts automatically on boot!
Enter fullscreen mode Exit fullscreen mode

Ecosystem Config (Recommended)

// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'myapp',
    script: 'npm',
    args: 'start',
    cwd: '/var/www/myapp',

    // Auto-restart on crash
    autorestart: true,

    // Max restarts per second (prevent infinite loop)
    max_restarts: 10,

    // Restart if using too much memory
    max_memory_restart: '500M',

    // Environment variables
    env: {
      NODE_ENV: 'production',
      PORT: 3000,
      HOST: '127.0.0.1',
    },

    // Logging
    log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
    error_file: '/var/log/myapp/error.log',
    out_file: '/var/www/myapp/out.log',
    merge_logs: true,
  }],
};

// Usage:
// pm2 start ecosystem.config.js
// pm2 reload all     // Zero-downtime reload!
Enter fullscreen mode Exit fullscreen mode

Step 6: Nginx Reverse Proxy

# Install Nginx
sudo apt install nginx -y

# Create config file
sudo nano /etc/nginx/sites-available/myapp
Enter fullscreen mode Exit fullscreen mode
server {
    listen 80;
    server_name your-domain.com www.your-domain.com;  # Or your IP

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;

        # WebSocket support
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';

        # Headers
        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;

        # Timeouts
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }

    # Static assets caching (if you serve any)
    location /static/ {
        alias /var/www/myapp/public/;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}
Enter fullscreen mode Exit fullscreen mode
# Enable site
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default  # Remove default site

# Test config
sudo nginx -t

# Reload Nginx
sudo systemctl reload nginx

# Your app is now live at http://your-server-ip or http://your-domain.com!
Enter fullscreen mode Exit fullscreen mode

Step 7: Free SSL with Let's Encrypt

# Install Certbot
sudo apt install certbot python3-certbot-nginx -y

# Get certificate (auto-configures Nginx!)
sudo certbot --nginx -d your-domain.com -d www.your-domain.com

# Follow prompts:
# - Enter email
# - Agree to terms
# - Choose redirect (option 2: redirect HTTP to HTTPS)

# Done! Your site now has HTTPS.

# Test auto-renewal (certificates expire in 90 days)
sudo certbot renew --dry-run

# Auto-renewal is already set up via systemd timer
sudo systemctl status certbot.timer
Enter fullscreen mode Exit fullscreen mode

Step 8: Security Hardening

# Add to your Nginx config (inside server block):

# Hide Nginx version
server_tokens off;

# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

# Block access to hidden files
location ~ /\. {
    deny all;
}

# Limit request size (prevent huge uploads)
client_max_body_size 10M;
Enter fullscreen mode Exit fullscreen mode
# Additional security steps:

# Disable root SSH login
sudo sed -i 's/#PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
sudo systemctl restart sshd

# Install fail2ban (auto-block brute force attempts)
sudo apt install fail2ban -y
sudo systemctl enable fail2ban
sudo systemctl start fail2ban

# Automatic security updates
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure unattended-upgrades  # Select "Yes"
Enter fullscreen mode Exit fullscreen mode

Step 9: Monitoring & Log Rotation

# Create log directory
sudo mkdir -p /var/log/myapp
sudo chown deploy:deploy /var/log/myapp

# Log rotation (prevent disk fill!)
sudo nano /etc/logrotate.d/myapp
Enter fullscreen mode Exit fullscreen mode
/var/log/myapp/*.log {
    daily
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    create 0640 deploy deploy
}
Enter fullscreen mode Exit fullscreen mode
# Quick health check script
cat > /var/www/myapp/health.sh << 'EOF'
#!/bin/bash
if pm2 pid myapp > /dev/null; then
  echo "✅ App running (PID: $(pm2 pid myapp))"
else
  echo "❌ App NOT running!"
  exit 1
fi

HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/)
echo "HTTP Status: $HTTP_STATUS"

DISK_USAGE=$(df -h / | awk 'NR==2{print $5}')
echo "Disk usage: $DISK_USAGE"

MEM_FREE=$(free -h | awk '/Mem:/{print $7}')
echo "Free memory: $MEM_FREE"
EOF
chmod +x /var/www/myapp/health.sh

# Run anytime: ./health.sh
Enter fullscreen mode Exit fullscreen mode

Complete Deployment Checklist

□ Server updated (apt upgrade)
□ Non-root user created
□ Node.js installed via nvm
□ UFW firewall configured (SSH + HTTP + HTTPS only)
□ App code deployed to /var/www/myapp/
□ Dependencies installed (npm ci)
□ PM2 configured and running
□ PM2 startup saved (survives reboot)
□ Nginx installed and configured as reverse proxy
□ DNS pointing to server IP
□ SSL certificate installed (Let's Encrypt)
□ HTTP redirects to HTTPS
□ Security headers configured
□ Root SSH login disabled
□ Fail2Ban running
□ Log rotation configured
□ Health check script working
□ Tested: curl https://your-domain.com
Enter fullscreen mode Exit fullscreen mode

What does your deployment setup look like? Anything you'd add?

Follow @armorbreak for more DevOps content.

Top comments (0)