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

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

Code Block

Copy

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