How I Deploy Node.js Apps to Production (2026)
My exact deployment process. From code to live in minutes.
The Stack
Server: Ubuntu on DigitalOcean/Linode/VPS ($5-10/month)
Runtime: Node.js 22 LTS
Process Manager: systemd (built-in, no PM2 needed)
Reverse Proxy: Nginx
SSL: Let's Encrypt (free, auto-renew)
CI/CD: GitHub Actions (free for public repos)
1. Server Setup (One-Time)
# Update system
sudo apt update && sudo apt upgrade -y
# Install Node.js (via NodeSource)
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
# Install Nginx
sudo apt install -y nginx
# Install Certbot (for SSL)
sudo apt install -y certbot python3-certbot-nginx
# Create app user (don't run as root!)
sudo useradd -m -s /bin/bash appuser
sudo usermod -aG sudo appuser
# Firewall
sudo ufw allow 22 # SSH
sudo ufw allow 80 # HTTP
sudo ufw allow 443 # HTTPS
sudo ufw enable
2. Deploy the App
# Create app directory
sudo mkdir -p /var/www/myapp
sudo chown appuser:appuser /var/www/myapp
# Clone or copy code
cd /var/www/myapp
git clone https://github.com/you/your-app.git .
# Install dependencies
npm ci --production
# Build (if needed)
npm run build
# Create .env file
cat > .env << 'EOF'
NODE_ENV=production
PORT=3000
DATABASE_URL=postgres://...
JWT_SECRET=your-secret-here
EOF
chmod 600 .env # Only owner can read
3. systemd Service (Built-in Process Manager)
# Create service file
sudo nano /etc/systemd/system/myapp.service
[Unit]
Description=My Node.js Application
After=network.target
[Service]
Type=simple
User=appuser
WorkingDirectory=/var/www/myapp
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=myapp
# Security hardening
NoNewPrivileges=true
ReadOnlyPaths=/var/www/myapp
PrivateTmp=true
[Install]
WantedBy=multi-user.target
# Start and enable
sudo systemctl daemon-reload
sudo systemctl enable myapp # Auto-start on boot
sudo systemctl start myapp
# Useful commands
sudo systemctl status myapp # Check status
sudo systemctl restart myapp # Restart
sudo systemctl stop myapp # Stop
sudo journalctl -u myapp -f # Follow logs
4. Nginx Configuration
sudo nano /etc/nginx/sites-available/myapp
server {
listen 80;
server_name example.com www.example.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;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# 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;
# Rate limiting zone (defined in nginx.conf)
limit_req zone=api burst=20 nodelay;
# Block sensitive files
location ~ /\. {
deny all;
}
}
# Enable site
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t # Test config
sudo systemctl reload nginx # Apply
# SSL (Let's Encrypt)
sudo certbot --nginx -d example.com -d www.example.com
# Auto-renew: certbot adds a cron job automatically
5. GitHub Actions CI/CD
# .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@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: appuser
key: ${{ secrets.SSH_KEY }}
script: |
cd /var/www/myapp
git pull origin main
npm ci --production
npm run build
sudo systemctl restart myapp
6. Health Check Endpoint
// server.js
app.get('/health', (req, res) => {
res.json({
status: 'ok',
uptime: process.uptime(),
memory: process.memoryUsage(),
timestamp: new Date().toISOString(),
});
});
7. Monitoring
# Log rotation (prevents disk filling)
sudo nano /etc/logrotate.d/myapp
/var/log/myapp.log {
daily
rotate 7
compress
missingok
notifempty
copytruncate
}
# Basic monitoring script (add to crontab)
*/5 * * * * curl -sf http://localhost:3000/health || systemctl restart myapp
Quick Checklist
□ Server updated and secured
□ App runs as non-root user
□ systemd service configured (auto-restart + auto-start)
□ Nginx reverse proxy with security headers
□ SSL certificate (Let's Encrypt)
□ .env file with 600 permissions
□ .gitignore includes .env
□ Health check endpoint
□ Log rotation configured
□ CI/CD pipeline set up
□ Firewall (ufw) configured
□ Rate limiting on Nginx
What's your deployment process? Any tools I'm missing?
Follow @armorbreak for more DevOps content.
Top comments (0)