Tools: Deploy Next.js to a VPS: Nginx, PM2, SSL, and Zero-Downtime Deployments (2026)
Server Setup (Ubuntu 22.04)
Next.js Build for VPS
PM2 Configuration
Nginx Reverse Proxy
SSL with Let's Encrypt
Automated Deploy Script Deploying a Next.js app to a VPS gives you full control over your infrastructure, better performance per dollar than Vercel at scale, and no vendor lock-in. Here's the complete setup: Nginx, PM2, SSL, and automated deployments. The standalone output mode creates a self-contained server that doesn't need node_modules in production — just copy .next/standalone, .next/static, and public. Trigger from GitHub Actions: The Ship Fast Skill Pack at whoffagents.com includes a /deploy skill that generates Nginx configs, PM2 ecosystem files, and deploy scripts customized for your stack. $49 one-time. 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
# Update and -weight: 500;">install dependencies
-weight: 500;">apt -weight: 500;">update && -weight: 500;">apt -weight: 500;">upgrade -y
-weight: 500;">apt -weight: 500;">install -y nginx certbot python3-certbot-nginx -weight: 500;">git -weight: 500;">curl # Install Node.js 20
-weight: 500;">curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
-weight: 500;">apt -weight: 500;">install -y nodejs # Install PM2 globally
-weight: 500;">npm -weight: 500;">install -g pm2 # Create app user
adduser --system --group app
# Update and -weight: 500;">install dependencies
-weight: 500;">apt -weight: 500;">update && -weight: 500;">apt -weight: 500;">upgrade -y
-weight: 500;">apt -weight: 500;">install -y nginx certbot python3-certbot-nginx -weight: 500;">git -weight: 500;">curl # Install Node.js 20
-weight: 500;">curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
-weight: 500;">apt -weight: 500;">install -y nodejs # Install PM2 globally
-weight: 500;">npm -weight: 500;">install -g pm2 # Create app user
adduser --system --group app
# Update and -weight: 500;">install dependencies
-weight: 500;">apt -weight: 500;">update && -weight: 500;">apt -weight: 500;">upgrade -y
-weight: 500;">apt -weight: 500;">install -y nginx certbot python3-certbot-nginx -weight: 500;">git -weight: 500;">curl # Install Node.js 20
-weight: 500;">curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
-weight: 500;">apt -weight: 500;">install -y nodejs # Install PM2 globally
-weight: 500;">npm -weight: 500;">install -g pm2 # Create app user
adduser --system --group app
// next.config.js
module.exports = { output: 'standalone', // bundles Node.js server into .next/standalone
}
// next.config.js
module.exports = { output: 'standalone', // bundles Node.js server into .next/standalone
}
// next.config.js
module.exports = { output: 'standalone', // bundles Node.js server into .next/standalone
}
// ecosystem.config.js
module.exports = { apps: [{ name: 'nextjs-app', script: '.next/standalone/server.js', env: { NODE_ENV: 'production', PORT: 3000, }, instances: 'max', // one per CPU core exec_mode: 'cluster', max_memory_restart: '500M', error_file: '/var/log/app/error.log', out_file: '/var/log/app/out.log', }],
}
// ecosystem.config.js
module.exports = { apps: [{ name: 'nextjs-app', script: '.next/standalone/server.js', env: { NODE_ENV: 'production', PORT: 3000, }, instances: 'max', // one per CPU core exec_mode: 'cluster', max_memory_restart: '500M', error_file: '/var/log/app/error.log', out_file: '/var/log/app/out.log', }],
}
// ecosystem.config.js
module.exports = { apps: [{ name: 'nextjs-app', script: '.next/standalone/server.js', env: { NODE_ENV: 'production', PORT: 3000, }, instances: 'max', // one per CPU core exec_mode: 'cluster', max_memory_restart: '500M', error_file: '/var/log/app/error.log', out_file: '/var/log/app/out.log', }],
}
pm2 -weight: 500;">start ecosystem.config.js
pm2 save # persist across reboots
pm2 startup # generate systemd -weight: 500;">service
pm2 -weight: 500;">start ecosystem.config.js
pm2 save # persist across reboots
pm2 startup # generate systemd -weight: 500;">service
pm2 -weight: 500;">start ecosystem.config.js
pm2 save # persist across reboots
pm2 startup # generate systemd -weight: 500;">service
# /etc/nginx/sites-available/myapp
upstream nextjs { server 127.0.0.1:3000; keepalive 64;
} server { listen 80; server_name myapp.com www.myapp.com; # Redirect HTTP to HTTPS return 301 https://$host$request_uri;
} server { listen 443 ssl http2; server_name myapp.com www.myapp.com; ssl_certificate /etc/letsencrypt/live/myapp.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/myapp.com/privkey.pem; # Security headers add_header X-Frame-Options DENY; add_header X-Content-Type-Options nosniff; add_header Referrer-Policy strict-origin-when-cross-origin; # Cache static assets location /_next/static { alias /var/www/myapp/.next/static; expires 1y; add_header Cache-Control 'public, immutable'; } location /public { alias /var/www/myapp/public; expires 7d; } location / { proxy_pass http://nextjs; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection '-weight: 500;">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_cache_bypass $http_upgrade; }
}
# /etc/nginx/sites-available/myapp
upstream nextjs { server 127.0.0.1:3000; keepalive 64;
} server { listen 80; server_name myapp.com www.myapp.com; # Redirect HTTP to HTTPS return 301 https://$host$request_uri;
} server { listen 443 ssl http2; server_name myapp.com www.myapp.com; ssl_certificate /etc/letsencrypt/live/myapp.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/myapp.com/privkey.pem; # Security headers add_header X-Frame-Options DENY; add_header X-Content-Type-Options nosniff; add_header Referrer-Policy strict-origin-when-cross-origin; # Cache static assets location /_next/static { alias /var/www/myapp/.next/static; expires 1y; add_header Cache-Control 'public, immutable'; } location /public { alias /var/www/myapp/public; expires 7d; } location / { proxy_pass http://nextjs; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection '-weight: 500;">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_cache_bypass $http_upgrade; }
}
# /etc/nginx/sites-available/myapp
upstream nextjs { server 127.0.0.1:3000; keepalive 64;
} server { listen 80; server_name myapp.com www.myapp.com; # Redirect HTTP to HTTPS return 301 https://$host$request_uri;
} server { listen 443 ssl http2; server_name myapp.com www.myapp.com; ssl_certificate /etc/letsencrypt/live/myapp.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/myapp.com/privkey.pem; # Security headers add_header X-Frame-Options DENY; add_header X-Content-Type-Options nosniff; add_header Referrer-Policy strict-origin-when-cross-origin; # Cache static assets location /_next/static { alias /var/www/myapp/.next/static; expires 1y; add_header Cache-Control 'public, immutable'; } location /public { alias /var/www/myapp/public; expires 7d; } location / { proxy_pass http://nextjs; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection '-weight: 500;">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_cache_bypass $http_upgrade; }
}
certbot --nginx -d myapp.com -d www.myapp.com
# Auto-renewal is set up automatically # Test renewal
certbot renew --dry-run
certbot --nginx -d myapp.com -d www.myapp.com
# Auto-renewal is set up automatically # Test renewal
certbot renew --dry-run
certbot --nginx -d myapp.com -d www.myapp.com
# Auto-renewal is set up automatically # Test renewal
certbot renew --dry-run
#!/bin/bash
# deploy.sh
set -e APP_DIR=/var/www/myapp
REPO=-weight: 500;">git@github.com:youruser/yourrepo.-weight: 500;">git cd $APP_DIR
-weight: 500;">git pull origin main
-weight: 500;">npm ci --only=production
-weight: 500;">npm run build # Copy standalone output
cp -r .next/static .next/standalone/.next/static
cp -r public .next/standalone/public # Zero-downtime reload
pm2 reload ecosystem.config.js ---weight: 500;">update-env echo 'Deploy complete'
#!/bin/bash
# deploy.sh
set -e APP_DIR=/var/www/myapp
REPO=-weight: 500;">git@github.com:youruser/yourrepo.-weight: 500;">git cd $APP_DIR
-weight: 500;">git pull origin main
-weight: 500;">npm ci --only=production
-weight: 500;">npm run build # Copy standalone output
cp -r .next/static .next/standalone/.next/static
cp -r public .next/standalone/public # Zero-downtime reload
pm2 reload ecosystem.config.js ---weight: 500;">update-env echo 'Deploy complete'
#!/bin/bash
# deploy.sh
set -e APP_DIR=/var/www/myapp
REPO=-weight: 500;">git@github.com:youruser/yourrepo.-weight: 500;">git cd $APP_DIR
-weight: 500;">git pull origin main
-weight: 500;">npm ci --only=production
-weight: 500;">npm run build # Copy standalone output
cp -r .next/static .next/standalone/.next/static
cp -r public .next/standalone/public # Zero-downtime reload
pm2 reload ecosystem.config.js ---weight: 500;">update-env echo 'Deploy complete'
- name: Deploy uses: appleboy/ssh-action@master with: host: ${{ secrets.SERVER_HOST }} username: deploy key: ${{ secrets.SSH_KEY }} script: /var/www/myapp/deploy.sh
- name: Deploy uses: appleboy/ssh-action@master with: host: ${{ secrets.SERVER_HOST }} username: deploy key: ${{ secrets.SSH_KEY }} script: /var/www/myapp/deploy.sh
- name: Deploy uses: appleboy/ssh-action@master with: host: ${{ secrets.SERVER_HOST }} username: deploy key: ${{ secrets.SSH_KEY }} script: /var/www/myapp/deploy.sh