Tools: Latest: How I Deploy Node.js Apps to Production (2026)
How I Deploy Node.js Apps to Production (2026)
The Stack
1. Server Setup (One-Time)
2. Deploy the App
3. systemd Service (Built-in Process Manager)
4. Nginx Configuration
5. GitHub Actions CI/CD
6. Health Check Endpoint
7. Monitoring
Quick Checklist My exact deployment process. From code to live in minutes. What's your deployment process? Any tools I'm missing? 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
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)
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)
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)
# 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
# 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
# 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
# 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
# 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
# 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
# Create service file
sudo nano /etc/systemd/system/myapp.service
# Create service file
sudo nano /etc/systemd/system/myapp.service
# 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
[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
[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
# 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
# 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
sudo nano /etc/nginx/sites-available/myapp
sudo nano /etc/nginx/sites-available/myapp
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; }
}
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; }
}
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
# 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
# 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
# .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
# .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
# .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
// server.js
app.get('/health', (req, res) => { res.json({ status: 'ok', uptime: process.uptime(), memory: process.memoryUsage(), timestamp: new Date().toISOString(), });
});
// server.js
app.get('/health', (req, res) => { res.json({ status: 'ok', uptime: process.uptime(), memory: process.memoryUsage(), timestamp: new Date().toISOString(), });
});
// server.js
app.get('/health', (req, res) => { res.json({ status: 'ok', uptime: process.uptime(), memory: process.memoryUsage(), timestamp: new Date().toISOString(), });
});
# Log rotation (prevents disk filling)
sudo nano /etc/logrotate.d/myapp
# Log rotation (prevents disk filling)
sudo nano /etc/logrotate.d/myapp
# Log rotation (prevents disk filling)
sudo nano /etc/logrotate.d/myapp
/var/log/myapp.log { daily rotate 7 compress missingok notifempty copytruncate
}
/var/log/myapp.log { daily rotate 7 compress missingok notifempty copytruncate
}
/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
# Basic monitoring script (add to crontab)
*/5 * * * * curl -sf http://localhost:3000/health || systemctl restart myapp
# Basic monitoring script (add to crontab)
*/5 * * * * curl -sf http://localhost:3000/health || systemctl restart myapp
□ 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
□ 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
□ 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