The Moment You Take Your App Live
I remember the first time I deployed a Node.js application to a VPS. It was 2 AM, my hands were shaking, and I was convinced something would go catastrophically wrong. I’d spent three weeks building this app, and now I was about to expose it to the world. I followed 27 different tutorials, each telling me something slightly different, and ended up with a half-working deployment that crashed every few hours.
Fast forward to today, and I’ve deployed hundreds of applications—from small side projects to enterprise systems handling millions of requests. The process has become second nature, and I’ve refined it to the point where I can deploy a new app in under 30 minutes.
In this guide, I’m going to show you exactly how to deploy a Node.js application to a Linux VPS, step-by-step. No skipped steps, no “assume you know this” moments. I’ll show you every single command, every configuration file, and every troubleshooting step you’ll need.
By the end of this tutorial, you’ll have:
- ✅ A production-ready VPS environment
- ✅ Your Node.js app running with PM2 (process manager)
- ✅ Nginx as a reverse proxy
- ✅ SSL/HTTPS with Let’s Encrypt
- ✅ Automatic deployment scripts
- ✅ Monitoring and logging set up
Prerequisites
Before we start, make sure you have:
- ✅ A VPS running Ubuntu 22.04 LTS (or 24.04 LTS)
- ✅ A domain name pointing to your VPS’s IP address
- ✅ Your Node.js application ready (GitHub repo or local copy)
- ✅ Basic terminal/SSH knowledge
- ✅ Patience and a cup of coffee ☕
Cost Breakdown:
- VPS: ~$5-10/month (DigitalOcean, Linode, Vultr)
- Domain: ~$10-15/year
- SSL Certificate: FREE (Let’s Encrypt)
- Total: ~$7-15/month
Step 1: Initial VPS Setup (5 minutes)
1.1 SSH into Your VPS
# Replace with your VPS IP address
ssh root@your-server-ip
# If you're on Windows, use PowerShell or WSL
# If you're on Mac/Linux, the terminal works perfectly
Troubleshooting:
- If you get “Connection refused,” make sure your VPS is running and SSH is enabled
- If you get “Permission denied,” check your password or SSH keys
1.2 Update System Packages
# Update package lists
apt update
# Upgrade all packages to the latest versions
apt upgrade -y
# Install essential tools
apt install -y git curl wget build-essential software-properties-common
1.3 Create a New User (Security Best Practice)
Running everything as root is dangerous. Let’s create a deploy user:
# Create a new user
adduser deploy
# Add user to sudo group
usermod -aG sudo deploy
# Switch to the new user
su - deploy
# Verify you're now the deploy user
whoami # Should output "deploy"
1.4 Set Up SSH Keys (Optional but Recommended)
# On your LOCAL machine, generate an SSH key if you don't have one
ssh-keygen -t rsa -b 4096 -C "your-email@example.com"
# Copy the public key to your VPS
ssh-copy-id deploy@your-server-ip
# Now you can SSH as deploy user without a password
ssh deploy@your-server-ip
Step 2: Install and Configure Nginx (3 minutes)
2.1 Install Nginx
sudo apt install -y nginx
# Start Nginx
sudo systemctl start nginx
# Enable Nginx to start on boot
sudo systemctl enable nginx
# Check status
sudo systemctl status nginx
2.2 Configure Firewall
# Allow SSH
sudo ufw allow 22
# Allow HTTP and HTTPS
sudo ufw allow 80
sudo ufw allow 443
# Enable the firewall
sudo ufw enable
# Check status
sudo ufw status
Now visit your VPS IP in a browser—you should see the Nginx welcome page.
Step 3: Install Node.js (2 minutes)
3.1 Install Node.js via NodeSource
# Install NodeSource repository
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
# Install Node.js
sudo apt install -y nodejs
# Verify installation
node --version # Should show v20.x or higher
npm --version # Should show 10.x or higher
3.2 Install PM2 (Process Manager)
PM2 will keep your app running even after crashes:
# Install PM2 globally
sudo npm install -g pm2
# Verify installation
pm2 --version
Step 4: Prepare Your Application (10 minutes)
4.1 Clone Your Repository
# If your code is on GitHub
cd ~
git clone https://github.com/yourusername/your-repo.git app
# Or if you're starting fresh, create a new directory
mkdir ~/app
cd ~/app
4.2 Set Up Your Application
If you’re starting fresh, let’s create a sample app:
# Create sample app
npx express-generator --view=ejs myapp
cd myapp
npm install
4.3 Create Essential Files
Create ~/app/ecosystem.config.js for PM2:
module.exports = {
apps: [{
name: 'myapp',
script: './bin/www', // or 'app.js' or 'server.js' depending on your app
instances: 'max', // Cluster mode with all CPU cores
exec_mode: 'cluster',
watch: false,
max_memory_restart: '1G',
env: {
NODE_ENV: 'production',
PORT: 3000
},
error_file: './logs/err.log',
out_file: './logs/out.log',
log_file: './logs/combined.log',
time: true
}]
};
4.4 Test Your App Locally
# Make sure your app runs
npm start
# Visit http://localhost:3000
# Stop the app (Ctrl+C)
Step 5: Configure the App for Production (5 minutes)
5.1 Set Up Environment Variables
Create .env file:
NODE_ENV=production
PORT=3000
DATABASE_URL=your_database_url
JWT_SECRET=your_secret_key_here
5.2 Create a Systemd Service File (Alternative to PM2)
If you prefer systemd over PM2:
sudo nano /etc/systemd/system/myapp.service
Add this content:
[Unit]
Description=My Node.js App
After=network.target
[Service]
Type=simple
User=deploy
WorkingDirectory=/home/deploy/app/myapp
ExecStart=/usr/bin/node /home/deploy/app/myapp/bin/www
Restart=on-failure
RestartSec=10
Environment=NODE_ENV=production
Environment=PORT=3000
[Install]
WantedBy=multi-user.target
Step 6: Configure Nginx as a Reverse Proxy (5 minutes)
6.1 Create Nginx Configuration
# Remove default config
sudo rm /etc/nginx/sites-enabled/default
# Create new config for your app
sudo nano /etc/nginx/sites-available/myapp
Add this configuration:
# HTTP server (redirects to HTTPS)
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
# Redirect all HTTP to HTTPS
location / {
return 301 https://$server_name$request_uri;
}
}
# HTTPS server
server {
listen 443 ssl http2;
server_name yourdomain.com www.yourdomain.com;
# SSL certificates (we'll add these in Step 8)
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
ssl_dhparam /etc/nginx/ssl/dhparam.pem;
# 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 "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
# Reverse proxy to Node.js
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
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 files (optional - let Node.js handle this or Nginx handle it)
location /static/ {
alias /home/deploy/app/myapp/public/;
expires 30d;
add_header Cache-Control "public, immutable";
}
}
6.2 Enable the Site
# Enable the site
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
# Test the configuration
sudo nginx -t
# If test passes, restart Nginx
sudo systemctl restart nginx
Step 7: Set Up SSL/HTTPS with Let’s Encrypt (5 minutes)
7.1 Install Certbot
sudo apt install -y certbot python3-certbot-nginx
7.2 Obtain SSL Certificate
# For your domain
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
# Follow the prompts:
# - Enter your email address
# - Agree to the terms of service
# - Choose whether to redirect HTTP to HTTPS (YES)
7.3 Verify SSL Renewal
# Test auto-renewal
sudo certbot renew --dry-run
# Certificate will auto-renew automatically
# To check status:
sudo systemctl status certbot.timer
7.4 Generate Strong Diffie-Hellman Parameters
# Create directory
sudo mkdir -p /etc/nginx/ssl
# Generate DH parameters (this takes a few minutes)
sudo openssl dhparam -out /etc/nginx/ssl/dhparam.pem 2048
Step 8: Deploy Your Application (5 minutes)
8.1 Start Your App with PM2
# Navigate to your app
cd ~/app
# Install dependencies
npm install --production
# Start with PM2
pm2 start ecosystem.config.js
# Save PM2 process list
pm2 save
# Set PM2 to start on system boot
pm2 startup
# Copy the command that's shown and run it
# It will look like:
# sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u deploy --hp /home/deploy
8.2 Monitor Your App
# Show running processes
pm2 list
# Monitor logs in real-time
pm2 logs
# Monitor performance
pm2 monit
Step 9: Set Up Firewall and Security (5 minutes)
9.1 Final Firewall Configuration
# Allow only necessary ports
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Enable the firewall
sudo ufw enable
# Check status
sudo ufw status
9.2 Install Fail2ban for Protection
sudo apt install -y fail2ban
# Create configuration
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
# Restart fail2ban
sudo systemctl restart fail2ban
sudo systemctl enable fail2ban
Step 10: Set Up Logging and Monitoring (5 minutes)
10.1 Nginx Log Rotation
Nginx logs will rotate automatically, but let’s verify:
sudo ls -la /etc/logrotate.d/nginx
# Should exist and be configured
10.2 PM2 Log Management
# Check log size
du -sh ~/.pm2/logs/
# Clear logs (if needed)
pm2 flush
# Rotate logs automatically
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 10M
pm2 set pm2-logrotate:retain 7
10.3 Set Up System Monitoring
# Install htop for system monitoring
sudo apt install -y htop
# Run htop to see system resources
htop
Step 11: Set Up Automatic Deployment (10 minutes)
11.1 Create a Deployment Script
Create ~/deploy.sh:
#!/bin/bash
echo "🚀 Starting deployment..."
echo "----------------------------------------"
# Pull latest changes
echo "📦 Pulling latest code..."
cd ~/app
git pull origin main
# Install dependencies
echo "📦 Installing dependencies..."
npm install --production
# Build the app (if needed)
# echo "🔨 Building the app..."
# npm run build
# Restart the app
echo "🔄 Restarting the app..."
pm2 reload ecosystem.config.js --update-env
# Show status
echo "📊 Current status:"
pm2 status
echo "✅ Deployment complete!"
echo "----------------------------------------"
Make it executable:
chmod +x ~/deploy.sh
11.2 Test the Deployment
# Simulate an update
./deploy.sh
Step 12: Post-Deployment Checklist
Essential Checks
# 1. Is the app running?
pm2 status
# 2. Can we access it?
curl -I https://yourdomain.com
# 3. Are logs being written?
pm2 logs --lines 10
# 4. Is Nginx running?
sudo systemctl status nginx
# 5. Are there any errors?
sudo tail -f /var/log/nginx/error.log
Quick Health Check Endpoint
Add this to your app:
// In app.js or server.js
app.get('/health', (req, res) => {
res.status(200).json({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage()
});
});
Test it:
curl https://yourdomain.com/health
Troubleshooting Common Issues
Issue 1: “Cannot find module” / “Module not found”
# Reinstall dependencies
cd ~/app
rm -rf node_modules
npm install --production
# Restart PM2
pm2 restart myapp
Issue 2: “EADDRINUSE: address already in use”
# Find what's using the port
sudo lsof -i :3000
# Kill the process
sudo kill -9 [PID]
# Or use PM2 to stop everything
pm2 kill
pm2 start ecosystem.config.js
Issue 3: Nginx shows 502 Bad Gateway
# Check if Node.js app is running
pm2 status
# Check Nginx logs
sudo tail -f /var/log/nginx/error.log
# Check app logs
pm2 logs myapp
# Restart everything
sudo systemctl restart nginx
pm2 restart myapp
Issue 4: SSL Certificate Not Working
# Check certificate status
sudo certbot certificates
# Force renewal
sudo certbot renew --force-renewal
# Restart Nginx
sudo systemctl restart nginx
Issue 5: App Crashes Immediately
# Check logs for errors
pm2 logs --lines 50
# Check the environment variables
pm2 env 0
# Start in foreground to see errors
cd ~/app
NODE_ENV=production node server.js
Security Best Practices
1. Regular Updates
# Weekly maintenance script
sudo apt update && sudo apt upgrade -y
sudo npm update -g
pm2 update
2. Secure .env Files
# Restrict permissions
chmod 600 ~/app/.env
# Never commit .env to version control
# Add to .gitignore: .env
3. Use a Non-Root User
We already did this with the deploy user. Always run your application as a non-root user.
4. Rate Limiting
Add rate limiting to your app:
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
app.use('/api/', limiter);
5. Regular Backups
# Create backup script
tar -czf backup-$(date +%Y%m%d).tar.gz ~/app
# Set up cron job for daily backups
crontab -e
# Add this line:
# 0 2 * * * tar -czf /backups/app-$(date +\%Y\%m\%d).tar.gz ~/app
Advanced: CI/CD with GitHub Actions
For those who want automated deployment, create .github/workflows/deploy.yml:
name: Deploy to VPS
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Deploy to VPS
uses: appleboy/ssh-action@v0.1.5
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
script: |
cd ~/app
git pull origin main
npm install --production
pm2 reload ecosystem.config.js --update-env
Add these secrets to your GitHub repository:
VPS_HOST: Your VPS IP or domainVPS_USER: Your deploy user (usually “deploy”)VPS_SSH_KEY: Your private SSH key
Performance Optimization Tips
1. Enable Gzip Compression
Add to Nginx config:
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;
2. Set Up Caching
Add to Nginx config:
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
3. Use Node.js Cluster Mode
We already set this with PM2’s instances: 'max'.
4. Database Optimization
- Use connection pooling
- Enable query caching
- Use indexes on frequently queried fields
5. Use a CDN
Consider using Cloudflare or AWS CloudFront for serving static assets.
Monitoring and Alerts
1. Set Up Uptime Monitoring
# Install Uptime Kuma (self-hosted monitoring)
# Or use services like UptimeRobot (free tier available)
2. Application Performance Monitoring (APM)
const { init } = require('@sentry/node');
init({
dsn: 'your-sentry-dsn',
environment: 'production'
});
3. Set Up Email Alerts
# Install mailutils
sudo apt install -y mailutils
# Create monitoring script
# Send email when service is down
Quick Reference: Essential Commands
SSH Commands
ssh deploy@your-server-ip
PM2 Commands
pm2 list # List all processes
pm2 logs # View all logs
pm2 logs myapp # View specific app logs
pm2 restart myapp # Restart app
pm2 reload myapp # Reload app (zero downtime)
pm2 stop myapp # Stop app
pm2 delete myapp # Delete app
pm2 monit # Monitor performance
pm2 save # Save process list
pm2 startup # Set up startup script
Nginx Commands
sudo nginx -t # Test configuration
sudo systemctl restart nginx # Restart Nginx
sudo systemctl reload nginx # Reload Nginx (graceful)
sudo systemctl status nginx # Check status
Firewall Commands
sudo ufw status # Check status
sudo ufw enable # Enable firewall
sudo ufw allow 443 # Allow port 443
sudo ufw delete allow 3000 # Remove rule
The Journey Complete
🎉 Congratulations! You’ve successfully deployed your Node.js application to a Linux VPS with production-ready configuration.
Let’s recap what you’ve accomplished:
- ✅ Set up a secure VPS environment
- ✅ Installed and configured Nginx
- ✅ Set up Node.js and PM2
- ✅ Configured SSL/HTTPS with Let’s Encrypt
- ✅ Set up a reverse proxy
- ✅ Created deployment scripts
- ✅ Implemented monitoring and logging
- ✅ Secured your server
Next Steps
- Set up monitoring — Use services like New Relic, Sentry, or self-hosted solutions
- Implement backups — Automate database and file backups
- Set up CI/CD — Automate deployments with GitHub Actions or GitLab CI
- Optimize performance — Use caching, CDN, and database optimization
- Scale up — Consider load balancing if traffic grows
Additional Resources
Common Mistakes to Avoid
- Running as root — Always use a non-root user
- Not setting up SSL — Always use HTTPS in production
- Forgetting to restart services — Use PM2 for automatic restarts
- Not setting environment variables — Use
.envfiles - Not monitoring — Set up logging and monitoring from day one
Final Thoughts
Deploying to a VPS used to be one of the most intimidating parts of web development for me. But with practice, it becomes second nature. The same commands you’ve learned today will serve you for years to come, whether you’re deploying a simple blog or a complex enterprise application.
Remember:
- Security first — Always use HTTPS and non-root users
- Monitor everything — Logs are your best friend
- Automate deployments — Save yourself time and prevent human error
- Test your backups — Regular backups are useless if you can’t restore them
You’ve got this! Your app is live, it’s secure, and it’s ready for the world. Now go build something amazing.
Have questions about deployment? Drop a comment below or reach out on Twitter — I’m happy to help!
Follow me for more: Complete deployment guides, DevOps tutorials, and full-stack development content. 🚀
P.S. — If you’re running a production app, here’s a free checklist I use before every major deployment:
- All tests are passing
- Database migrations are ready
- Environment variables are set
- SSL certificate is valid
- Monitoring is configured
- Backup system is working
- Rollback plan is documented
Deploy with confidence!
Discussion
Loading comments...