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 (1)

Collapse
 
jaafar12781 profile image
jaafar_soufiane22

Really solid walkthrough. The deployment checklist at the end is especially useful since it's easy to forget things like log rotation or automatic backups. My recent Node.js projects have mostly been on Hostinger with their Node.js hosting, but the production workflow still ends up looking very similar. PM2 and Nginx have been reliable enough that I haven't felt the need to change much. Interested to hear what everyone else is using for production these days.