Tools: How to Set Up a Node.js Server from Scratch (2026)
How to Set Up a Node.js Server from Scratch (2026)
Prerequisites
Step 1: Initial Server Setup
Step 2: Install Node.js via NVM
Step 3: Firewall Setup
Step 4: Deploy Your App
Step 5: Process Manager (PM2)
Ecosystem Config (Recommended)
Step 6: Nginx Reverse Proxy
Step 7: Free SSL with Let's Encrypt
Step 8: Security Hardening
Step 9: Monitoring & Log Rotation
Complete Deployment Checklist Complete guide: fresh server → running Node.js app with SSL, auto-restart, and monitoring. What does your deployment setup look like? Anything you'd add? Follow @armorbreak for more DevOps content. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse
# SSH into your server
ssh root@your-server-ip # Update everything
-weight: 500;">apt -weight: 500;">update && -weight: 500;">apt -weight: 500;">upgrade -y # Create a non-root user (security best practice)
adduser deploy
usermod -aG -weight: 600;">sudo deploy # Switch to new user (or use su - deploy)
su - deploy # Install essential tools
-weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y -weight: 500;">curl -weight: 500;">wget -weight: 500;">git vim ufw software-properties-common build-essential
# SSH into your server
ssh root@your-server-ip # Update everything
-weight: 500;">apt -weight: 500;">update && -weight: 500;">apt -weight: 500;">upgrade -y # Create a non-root user (security best practice)
adduser deploy
usermod -aG -weight: 600;">sudo deploy # Switch to new user (or use su - deploy)
su - deploy # Install essential tools
-weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y -weight: 500;">curl -weight: 500;">wget -weight: 500;">git vim ufw software-properties-common build-essential
# SSH into your server
ssh root@your-server-ip # Update everything
-weight: 500;">apt -weight: 500;">update && -weight: 500;">apt -weight: 500;">upgrade -y # Create a non-root user (security best practice)
adduser deploy
usermod -aG -weight: 600;">sudo deploy # Switch to new user (or use su - deploy)
su - deploy # Install essential tools
-weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y -weight: 500;">curl -weight: 500;">wget -weight: 500;">git vim ufw software-properties-common build-essential
# Don't use -weight: 500;">apt for Node.js! It's outdated.
# Use nvm instead: -weight: 500;">curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/-weight: 500;">install.sh | bash
source ~/.bashrc nvm -weight: 500;">install 22 # Install latest LTS
nvm use 22 # Use it
nvm alias default 22 # Make default node -v # v22.x.x
-weight: 500;">npm -v # 10.x.x # Global packages I always need
-weight: 500;">npm -weight: 500;">install -g pm2 nodemon
# Don't use -weight: 500;">apt for Node.js! It's outdated.
# Use nvm instead: -weight: 500;">curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/-weight: 500;">install.sh | bash
source ~/.bashrc nvm -weight: 500;">install 22 # Install latest LTS
nvm use 22 # Use it
nvm alias default 22 # Make default node -v # v22.x.x
-weight: 500;">npm -v # 10.x.x # Global packages I always need
-weight: 500;">npm -weight: 500;">install -g pm2 nodemon
# Don't use -weight: 500;">apt for Node.js! It's outdated.
# Use nvm instead: -weight: 500;">curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/-weight: 500;">install.sh | bash
source ~/.bashrc nvm -weight: 500;">install 22 # Install latest LTS
nvm use 22 # Use it
nvm alias default 22 # Make default node -v # v22.x.x
-weight: 500;">npm -v # 10.x.x # Global packages I always need
-weight: 500;">npm -weight: 500;">install -g pm2 nodemon
# Configure UFW firewall
-weight: 600;">sudo ufw allow OpenSSH # Port 22 (SSH)
-weight: 600;">sudo ufw allow 80 # HTTP
-weight: 600;">sudo ufw allow 443 # HTTPS
-weight: 600;">sudo ufw -weight: 500;">enable # Enable firewall
-weight: 600;">sudo ufw -weight: 500;">status verbose # Check rules # Optional: Allow additional ports
# -weight: 600;">sudo ufw allow 3000 # If your app runs on port 3000 directly
# Configure UFW firewall
-weight: 600;">sudo ufw allow OpenSSH # Port 22 (SSH)
-weight: 600;">sudo ufw allow 80 # HTTP
-weight: 600;">sudo ufw allow 443 # HTTPS
-weight: 600;">sudo ufw -weight: 500;">enable # Enable firewall
-weight: 600;">sudo ufw -weight: 500;">status verbose # Check rules # Optional: Allow additional ports
# -weight: 600;">sudo ufw allow 3000 # If your app runs on port 3000 directly
# Configure UFW firewall
-weight: 600;">sudo ufw allow OpenSSH # Port 22 (SSH)
-weight: 600;">sudo ufw allow 80 # HTTP
-weight: 600;">sudo ufw allow 443 # HTTPS
-weight: 600;">sudo ufw -weight: 500;">enable # Enable firewall
-weight: 600;">sudo ufw -weight: 500;">status verbose # Check rules # Optional: Allow additional ports
# -weight: 600;">sudo ufw allow 3000 # If your app runs on port 3000 directly
# Clone your repo (or copy files)
cd /var/www/
-weight: 600;">sudo mkdir myapp && cd myapp
-weight: 600;">sudo chown deploy:deploy .
-weight: 500;">git clone https://github.com/you/your-app.-weight: 500;">git . # Install dependencies
-weight: 500;">npm ci --production # ci = clean -weight: 500;">install (respects package-lock.json) # If TypeScript:
-weight: 500;">npm run build # Test it works
-weight: 500;">npm -weight: 500;">start &
# Test with: -weight: 500;">curl http://localhost:3000
kill %1
# Clone your repo (or copy files)
cd /var/www/
-weight: 600;">sudo mkdir myapp && cd myapp
-weight: 600;">sudo chown deploy:deploy .
-weight: 500;">git clone https://github.com/you/your-app.-weight: 500;">git . # Install dependencies
-weight: 500;">npm ci --production # ci = clean -weight: 500;">install (respects package-lock.json) # If TypeScript:
-weight: 500;">npm run build # Test it works
-weight: 500;">npm -weight: 500;">start &
# Test with: -weight: 500;">curl http://localhost:3000
kill %1
# Clone your repo (or copy files)
cd /var/www/
-weight: 600;">sudo mkdir myapp && cd myapp
-weight: 600;">sudo chown deploy:deploy .
-weight: 500;">git clone https://github.com/you/your-app.-weight: 500;">git . # Install dependencies
-weight: 500;">npm ci --production # ci = clean -weight: 500;">install (respects package-lock.json) # If TypeScript:
-weight: 500;">npm run build # Test it works
-weight: 500;">npm -weight: 500;">start &
# Test with: -weight: 500;">curl http://localhost:3000
kill %1
# Start your app with PM2
pm2 -weight: 500;">start -weight: 500;">npm --name "myapp" -- -weight: 500;">start # Check -weight: 500;">status
pm2 list
pm2 logs myapp # View logs
pm2 monit # Real-time monitoring dashboard # Useful commands
pm2 -weight: 500;">restart myapp # Restart
pm2 -weight: 500;">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:
-weight: 600;">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!
# Start your app with PM2
pm2 -weight: 500;">start -weight: 500;">npm --name "myapp" -- -weight: 500;">start # Check -weight: 500;">status
pm2 list
pm2 logs myapp # View logs
pm2 monit # Real-time monitoring dashboard # Useful commands
pm2 -weight: 500;">restart myapp # Restart
pm2 -weight: 500;">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:
-weight: 600;">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!
# Start your app with PM2
pm2 -weight: 500;">start -weight: 500;">npm --name "myapp" -- -weight: 500;">start # Check -weight: 500;">status
pm2 list
pm2 logs myapp # View logs
pm2 monit # Real-time monitoring dashboard # Useful commands
pm2 -weight: 500;">restart myapp # Restart
pm2 -weight: 500;">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:
-weight: 600;">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!
// ecosystem.config.js
module.exports = { apps: [{ name: 'myapp', script: '-weight: 500;">npm', args: '-weight: 500;">start', cwd: '/var/www/myapp', // Auto--weight: 500;">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 -weight: 500;">start ecosystem.config.js
// pm2 reload all // Zero-downtime reload!
// ecosystem.config.js
module.exports = { apps: [{ name: 'myapp', script: '-weight: 500;">npm', args: '-weight: 500;">start', cwd: '/var/www/myapp', // Auto--weight: 500;">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 -weight: 500;">start ecosystem.config.js
// pm2 reload all // Zero-downtime reload!
// ecosystem.config.js
module.exports = { apps: [{ name: 'myapp', script: '-weight: 500;">npm', args: '-weight: 500;">start', cwd: '/var/www/myapp', // Auto--weight: 500;">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 -weight: 500;">start ecosystem.config.js
// pm2 reload all // Zero-downtime reload!
# Install Nginx
-weight: 600;">sudo -weight: 500;">apt -weight: 500;">install nginx -y # Create config file
-weight: 600;">sudo nano /etc/nginx/sites-available/myapp
# Install Nginx
-weight: 600;">sudo -weight: 500;">apt -weight: 500;">install nginx -y # Create config file
-weight: 600;">sudo nano /etc/nginx/sites-available/myapp
# Install Nginx
-weight: 600;">sudo -weight: 500;">apt -weight: 500;">install nginx -y # Create config file
-weight: 600;">sudo nano /etc/nginx/sites-available/myapp
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 '-weight: 500;">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"; }
}
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 '-weight: 500;">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"; }
}
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 '-weight: 500;">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"; }
}
# Enable site
-weight: 600;">sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
-weight: 600;">sudo rm /etc/nginx/sites-enabled/default # Remove default site # Test config
-weight: 600;">sudo nginx -t # Reload Nginx
-weight: 600;">sudo -weight: 500;">systemctl reload nginx # Your app is now live at http://your-server-ip or http://your-domain.com!
# Enable site
-weight: 600;">sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
-weight: 600;">sudo rm /etc/nginx/sites-enabled/default # Remove default site # Test config
-weight: 600;">sudo nginx -t # Reload Nginx
-weight: 600;">sudo -weight: 500;">systemctl reload nginx # Your app is now live at http://your-server-ip or http://your-domain.com!
# Enable site
-weight: 600;">sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
-weight: 600;">sudo rm /etc/nginx/sites-enabled/default # Remove default site # Test config
-weight: 600;">sudo nginx -t # Reload Nginx
-weight: 600;">sudo -weight: 500;">systemctl reload nginx # Your app is now live at http://your-server-ip or http://your-domain.com!
# Install Certbot
-weight: 600;">sudo -weight: 500;">apt -weight: 500;">install certbot python3-certbot-nginx -y # Get certificate (auto-configures Nginx!)
-weight: 600;">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)
-weight: 600;">sudo certbot renew --dry-run # Auto-renewal is already set up via systemd timer
-weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">status certbot.timer
# Install Certbot
-weight: 600;">sudo -weight: 500;">apt -weight: 500;">install certbot python3-certbot-nginx -y # Get certificate (auto-configures Nginx!)
-weight: 600;">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)
-weight: 600;">sudo certbot renew --dry-run # Auto-renewal is already set up via systemd timer
-weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">status certbot.timer
# Install Certbot
-weight: 600;">sudo -weight: 500;">apt -weight: 500;">install certbot python3-certbot-nginx -y # Get certificate (auto-configures Nginx!)
-weight: 600;">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)
-weight: 600;">sudo certbot renew --dry-run # Auto-renewal is already set up via systemd timer
-weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">status certbot.timer
# 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;
# 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;
# 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;
# Additional security steps: # Disable root SSH login
-weight: 600;">sudo sed -i 's/#PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
-weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">restart sshd # Install fail2ban (auto-block brute force attempts)
-weight: 600;">sudo -weight: 500;">apt -weight: 500;">install fail2ban -y
-weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">enable fail2ban
-weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">start fail2ban # Automatic security updates
-weight: 600;">sudo -weight: 500;">apt -weight: 500;">install unattended-upgrades -y
-weight: 600;">sudo dpkg-reconfigure unattended-upgrades # Select "Yes"
# Additional security steps: # Disable root SSH login
-weight: 600;">sudo sed -i 's/#PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
-weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">restart sshd # Install fail2ban (auto-block brute force attempts)
-weight: 600;">sudo -weight: 500;">apt -weight: 500;">install fail2ban -y
-weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">enable fail2ban
-weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">start fail2ban # Automatic security updates
-weight: 600;">sudo -weight: 500;">apt -weight: 500;">install unattended-upgrades -y
-weight: 600;">sudo dpkg-reconfigure unattended-upgrades # Select "Yes"
# Additional security steps: # Disable root SSH login
-weight: 600;">sudo sed -i 's/#PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
-weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">restart sshd # Install fail2ban (auto-block brute force attempts)
-weight: 600;">sudo -weight: 500;">apt -weight: 500;">install fail2ban -y
-weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">enable fail2ban
-weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">start fail2ban # Automatic security updates
-weight: 600;">sudo -weight: 500;">apt -weight: 500;">install unattended-upgrades -y
-weight: 600;">sudo dpkg-reconfigure unattended-upgrades # Select "Yes"
# Create log directory
-weight: 600;">sudo mkdir -p /var/log/myapp
-weight: 600;">sudo chown deploy:deploy /var/log/myapp # Log rotation (prevent disk fill!)
-weight: 600;">sudo nano /etc/logrotate.d/myapp
# Create log directory
-weight: 600;">sudo mkdir -p /var/log/myapp
-weight: 600;">sudo chown deploy:deploy /var/log/myapp # Log rotation (prevent disk fill!)
-weight: 600;">sudo nano /etc/logrotate.d/myapp
# Create log directory
-weight: 600;">sudo mkdir -p /var/log/myapp
-weight: 600;">sudo chown deploy:deploy /var/log/myapp # Log rotation (prevent disk fill!)
-weight: 600;">sudo nano /etc/logrotate.d/myapp
/var/log/myapp/*.log { daily rotate 14 compress delaycompress missingok notifempty create 0640 deploy deploy
}
/var/log/myapp/*.log { daily rotate 14 compress delaycompress missingok notifempty create 0640 deploy deploy
}
/var/log/myapp/*.log { daily rotate 14 compress delaycompress missingok notifempty create 0640 deploy deploy
}
# 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=$(-weight: 500;">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
# 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=$(-weight: 500;">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
# 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=$(-weight: 500;">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
□ Server updated (-weight: 500;">apt -weight: 500;">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 (-weight: 500;">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: -weight: 500;">curl https://your-domain.com
□ Server updated (-weight: 500;">apt -weight: 500;">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 (-weight: 500;">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: -weight: 500;">curl https://your-domain.com
□ Server updated (-weight: 500;">apt -weight: 500;">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 (-weight: 500;">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: -weight: 500;">curl https://your-domain.com - A VPS (DigitalOcean $4/mo, Hetzner, Linode, etc.)
- A domain name (optional but recommended)
- SSH access to the server