DEV Community

Alex Chen
Alex Chen

Posted on

How I Deploy Node.js Apps to Production (2026)

How I Deploy Node.js Apps to Production (2026)

My complete deployment guide — from code to production in minutes.

The Stack I Use

App: Node.js + Express
Database: SQLite (or PostgreSQL for larger apps)
Process Manager: systemd
Reverse Proxy: Nginx
SSL: Let's Encrypt (free, auto-renewing)
Server: $5-10/month VPS (DigitalOcean, Hetzner, etc.)
CI/CD: GitHub Actions (free for public repos)
Enter fullscreen mode Exit fullscreen mode

Step 1: Prepare the App

# Make sure your app has:
# - package.json with start script
# - .env.example (template, no secrets!)
# - .gitignore (includes node_modules, .env, dist)
# - Build script (if TypeScript)

# package.json
{
  "scripts": {
    "start": "node dist/server.js",
    "build": "tsc",
    "dev": "tsx watch server.ts"
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Set Up the Server

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

# Update system
apt update && apt upgrade -y

# Install Node.js (via nvm)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash
source ~/.bashrc
nvm install 22
nvm use 22
nvm alias default 22

# Install PM2 globally (process manager)
npm install -g pm2

# Install Nginx
apt install nginx -y

# Install Certbot (for SSL)
apt install certbot python3-certbot-nginx -y

# Create app directory
mkdir -p /var/www/myapp
chown -R $USER:$USER /var/www/myapp
Enter fullscreen mode Exit fullscreen mode

Step 3: Deploy the Code

# Option A: Git pull (simplest)
cd /var/www/myapp
git clone https://github.com/yourname/myapp.git .
npm ci --production
npm run build  # If TypeScript

# Option B: GitHub Actions (automated)
# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Deploy to server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USERNAME }}
          key: ${{ secrets.SSH_KEY }}
          script: |
            cd /var/www/myapp
            git pull origin main
            npm ci --production
            npm run build
            pm2 restart myapp
Enter fullscreen mode Exit fullscreen mode

Step 4: Process Management

# Start with PM2
cd /var/www/myapp
pm2 start npm --name "myapp" -- start

# Useful PM2 commands
pm2 list                    # List all processes
pm2 logs myapp              # View logs
pm2 logs myapp --lines 100  # Last 100 lines
pm2 restart myapp           # Restart
pm2 stop myapp              # Stop
pm2 delete myapp            # Remove
pm2 monit                   # Real-time monitoring

# Save PM2 config (survives reboot)
pm2 startup                 # Generates startup script
pm2 save                    # Save current process list

# Or use ecosystem.config.js for configuration
module.exports = {
  apps: [{
    name: 'myapp',
    script: 'npm',
    args: 'start',
    cwd: '/var/www/myapp',
    instances: 1,
    autorestart: true,
    watch: false,
    max_memory_restart: '500M',
    env: {
      NODE_ENV: 'production',
      PORT: 3000,
    },
  }],
};
Enter fullscreen mode Exit fullscreen mode

Step 5: Nginx Reverse Proxy

# /etc/nginx/sites-available/myapp
server {
    listen 80;
    server_name myapp.com www.myapp.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;
    }
}
Enter fullscreen mode Exit fullscreen mode
# Enable site
ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
nginx -t           # Test config
systemctl reload nginx
Enter fullscreen mode Exit fullscreen mode

Step 6: SSL with Let's Encrypt

# Get free SSL certificate (auto-renewing!)
certbot --nginx -d myapp.com -d www.myapp.com

# Certbot auto-modifies your Nginx config to:
# - Redirect HTTP to HTTPS
# - Add SSL certificate paths
# - Add security headers

# Test auto-renewal
certbot renew --dry-run

# Auto-renewal is set up by certbot automatically
# Check: systemctl status certbot.timer
Enter fullscreen mode Exit fullscreen mode

Step 7: Security Hardening

# Add to your Nginx server block:

# 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;
add_header Content-Security-Policy "default-src 'self'" always;

# Rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

location /api/ {
    limit_req zone=api burst=20 nodelay;
    proxy_pass http://127.0.0.1:3000;
}

# Block common attack paths
location ~ /\. {
    deny all;
}
Enter fullscreen mode Exit fullscreen mode
# Firewall (UFW)
ufw allow 22/tcp      # SSH
ufw allow 80/tcp      # HTTP
ufw allow 443/tcp     # HTTPS
ufw enable

# Fail2Ban (auto-block suspicious IPs)
apt install fail2ban -y
systemctl enable fail2ban

# Disable root login
# Edit /etc/ssh/sshd_config:
# PermitRootLogin no
# PasswordAuthentication no (use SSH keys only!)
systemctl restart sshd
Enter fullscreen mode Exit fullscreen mode

Step 8: Monitoring

# PM2 monitoring
pm2 monit

# Server resources
htop
df -h
free -h

# Application logs
pm2 logs myapp

# Nginx access logs
tail -f /var/log/nginx/access.log

# Nginx error logs
tail -f /var/log/nginx/error.log

# Set up log rotation (prevent disk fill-up!)
# /etc/logrotate.d/myapp
/var/log/myapp/*.log {
    daily
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    create 0640 www-data adm
}
Enter fullscreen mode Exit fullscreen mode

Step 9: Environment Variables

# Create .env file on server (NOT in git!)
cat > /var/www/myapp/.env << EOF
NODE_ENV=production
PORT=3000
DATABASE_URL=/var/www/myapp/data.db
JWT_SECRET=your-random-secret-here
EOF

# Set permissions
chmod 600 .env
chown $USER:$USER .env

# In your app, use dotenv:
# npm install dotenv
// .env is loaded automatically in production
Enter fullscreen mode Exit fullscreen mode

Deployment Checklist

□ App builds without errors
□ .env.example committed (no secrets!)
□ .env created on server with production values
□ Process manager configured (PM2/systemd)
□ Nginx reverse proxy working
□ SSL certificate installed
□ HTTP redirects to HTTPS
□ Security headers configured
□ Rate limiting enabled
□ Firewall configured (UFW)
□ SSH key-only authentication
□ Log rotation set up
□ Monitoring in place
□ Backup strategy for database
□ Auto-restart on crash (PM2)
□ Auto-restart on server reboot (PM2 startup)
□ GitHub Actions CI/CD configured (optional but recommended)
Enter fullscreen mode Exit fullscreen mode

What's your deployment setup? Any tips?

Follow @armorbreak for more DevOps content.

Top comments (0)