Technology Encyclopedia Home > How to Deploy a Next.js App on a VPS — SSR, Static Export, and Production Setup

How to Deploy a Next.js App on a VPS — SSR, Static Export, and Production Setup

The first time I deployed a Next.js app I made the mistake of treating it like a plain React build — ran npm run build, uploaded the out folder, pointed Nginx at it. Worked fine until I needed SSR or API routes. Then nothing worked.

Next.js is more flexible than most frameworks, which also means there's more to understand about deployment. If you're using server-side rendering or API routes, you need a running Node.js process. If your app is purely static, you can export it and serve plain files. Both approaches are valid — you just need to know which one applies to you.

This guide covers both deployment modes: running Next.js as a full Node.js server, and exporting it as static files. I'll explain which approach fits which use case.

I run this on Tencent Cloud Lighthouse. Next.js with SSR works well on the 2 vCPU / 4 GB RAM plan. For static export, the Starter plan is sufficient. A practical advantage for Next.js deployments specifically: Lighthouse's snapshot feature makes rolling back a deployment simple — take a snapshot before each major update, and if the new version has a problem you can restore the previous state in minutes. The OrcaTerm browser terminal also means you can check logs and restart the PM2 process from any device without needing your development machine.


Table of Contents

  1. Next.js Deployment: Two Approaches
  2. Prerequisites
  3. Part 1 — Set Up the Server
  4. Part 2 — Install Node.js with NVM
  5. Part 3A — Deploy as a Node.js Server (SSR)
  6. Part 3B — Deploy as a Static Export
  7. Part 4 — Configure Nginx
  8. Part 5 — Enable HTTPS
  9. Part 6 — Auto-Deploy with GitHub Actions
  10. The Gotcha: Image Optimization with Static Export
  11. Environment Variables in Next.js

Key Takeaways

  • SSR mode needs PM2 + Nginx; static export just needs Nginx serving files
  • Run next build before every deployment and next start to serve it
  • PM2's --watch flag breaks production — use CI/CD for updates instead
  • For image optimization in production, configure the images.domains in next.config.js
  • WebSocket connections (for Next.js live features) require Nginx WebSocket proxy headers

Next.js Deployment: Two Approaches {#approaches}

Node.js Server Static Export
SSR / ISR ✅ Yes ❌ No
API routes ✅ Yes ❌ No
Image optimization ✅ Built-in ⚠️ Requires workaround
Server requirements Node.js process running Just Nginx serving files
Use when Full-stack app, dynamic data Content-only site, no API routes

Choose the Node.js server approach if you use:

  • getServerSideProps
  • Next.js API routes
  • Middleware
  • Image optimization via next/image

Choose static export if your app is:

  • A blog, documentation site, or marketing page
  • Pre-renderable at build time
  • Not using SSR features

Prerequisites {#prerequisites}

Requirement Notes
Cloud server Tencent Cloud Lighthouse Ubuntu 22.04
Node.js 18+ We'll install via NVM
A Next.js project Next.js 13+ (App Router or Pages Router both work)

Part 1 — Set Up the Server {#part-1}

ssh ubuntu@YOUR_SERVER_IP
sudo apt update && sudo apt upgrade -y
sudo apt install -y git nginx

sudo ufw allow ssh
sudo ufw allow 'Nginx Full'
sudo ufw enable

Part 2 — Install Node.js with NVM {#part-2}

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
export NVM_DIR="$HOME/.nvm"
source "$NVM_DIR/nvm.sh"

nvm install 20
nvm use 20
nvm alias default 20

node --version   # v20.x.x
npm --version

Install PM2 (process manager):

npm install -g pm2

Part 3A — Deploy as a Node.js Server (SSR) {#part-3a}

This is the approach for apps with SSR, API routes, or dynamic features.

mkdir -p ~/apps && cd ~/apps
git clone https://github.com/your-username/your-nextjs-app.git
cd your-nextjs-app

npm install
npm run build

Test it starts:

npm start
# Next.js starts on port 3000
# Visit http://YOUR_SERVER_IP:3000 to verify
# Ctrl+C to stop

Start with PM2:

pm2 start npm --name "nextjs-app" -- start
pm2 startup
# Run the command PM2 outputs
pm2 save

Check it's running:

pm2 status
pm2 logs nextjs-app

Part 3B — Deploy as a Static Export {#part-3b}

For pure static sites, Next.js can export to HTML/CSS/JS files.

Update next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export',
  trailingSlash: true,  // Recommended for static hosts
  images: {
    unoptimized: true,  // Required for static export
  },
}

module.exports = nextConfig

Build locally and upload:

# On your local machine
npm run build

# Build creates an /out directory
ls out/

# Upload to server
scp -r out/* ubuntu@YOUR_SERVER_IP:/var/www/nextjsapp/

Or build on the server:

# On the server
cd ~/apps/your-nextjs-app
npm install && npm run build
sudo cp -r out/* /var/www/nextjsapp/

Part 4 — Configure Nginx {#part-4}

For Node.js Server (SSR)

sudo nano /etc/nginx/sites-available/nextjsapp
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;

    # Proxy all requests to Next.js
    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;
    }
}

For Static Export

sudo mkdir -p /var/www/nextjsapp
sudo nano /etc/nginx/sites-available/nextjsapp
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;

    root /var/www/nextjsapp;
    index index.html;

    # Cache static assets (Next.js hashes filenames)
    location /_next/static/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # All routes fall back to index.html
    location / {
        try_files $uri $uri.html $uri/ /index.html;
    }

    gzip on;
    gzip_types text/plain text/css application/javascript application/json;
    gzip_vary on;
}

Enable and reload:

sudo ln -s /etc/nginx/sites-available/nextjsapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Part 5 — Enable HTTPS {#part-5}

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Part 6 — Auto-Deploy with GitHub Actions {#part-6}

For Node.js Server

Create .github/workflows/deploy.yml:

name: Deploy Next.js

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: ${{ secrets.SERVER_IP }}
          username: ubuntu
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd ~/apps/your-nextjs-app
            git pull origin main
            npm install
            npm run build
            pm2 reload nextjs-app --update-env

For Static Export

name: Deploy Next.js Static

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Build
        run: npm ci && npm run build

      - name: Upload to server
        uses: appleboy/scp-action@v0.1.7
        with:
          host: ${{ secrets.SERVER_IP }}
          username: ubuntu
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          source: "out/"
          target: "/var/www/nextjsapp"
          strip_components: 1

The Gotcha: Image Optimization with Static Export {#gotcha}

Next.js's <Image> component uses a server-side image optimization pipeline. When you use output: 'export', this pipeline doesn't exist — so you need to either disable it or provide an external image optimization service.

The quick fix is unoptimized: true in next.config.js:

images: {
  unoptimized: true,
}

This tells Next.js to skip optimization and serve images as-is. Fine for most static sites.

If you want image optimization with static export, use an external CDN that supports image transformation. Tencent Cloud EdgeOne supports image processing at the edge — you can route image requests through EdgeOne for on-the-fly resizing and format conversion without a Next.js server.


Environment Variables in Next.js {#env-vars}

Next.js has two categories of environment variables:

Variable Available Use for
NEXT_PUBLIC_* Browser + server Public values (API URLs, analytics IDs)
Everything else Server-side only Secrets (API keys, database passwords)
# .env.production
NEXT_PUBLIC_API_URL=https://api.yourdomain.com
DATABASE_URL=postgresql://user:password@localhost/mydb   # Server-only

For the Node.js server deployment, set environment variables in PM2:

pm2 start npm --name "nextjs-app" -- start --env production
# or in ecosystem.config.js:
module.exports = {
  apps: [{
    name: 'nextjs-app',
    script: 'npm',
    args: 'start',
    env_production: {
      NODE_ENV: 'production',
      DATABASE_URL: 'postgresql://...',
    }
  }]
}

Troubleshooting {#troubleshooting}

Issue Likely Cause Fix
Connection refused Service not running or wrong port Check systemctl status SERVICE and verify firewall rules
Permission denied Wrong file ownership or permissions Check file ownership with ls -la and use chown/chmod to fix
502 Bad Gateway Backend service not running Restart the backend service; check logs with journalctl -u SERVICE
SSL certificate error Certificate expired or domain mismatch Run sudo certbot renew and verify domain DNS points to server IP
Service not starting Config error or missing dependency Check logs with journalctl -u SERVICE -n 50 for specific error
Out of disk space Logs or data accumulation Run df -h to identify usage; clean logs or attach CBS storage
High memory usage Too many processes or memory leak Check with htop; consider upgrading instance plan if consistently high
Firewall blocking traffic Port not open in UFW or Lighthouse console Open port in Lighthouse console firewall AND sudo ufw allow PORT

Frequently Asked Questions {#faq}

Should I use SSR mode or static export for my Next.js app?
Use SSR (Node.js server mode) if your app has API routes, uses getServerSideProps, or needs server-side rendering. Use static export (output: 'export') if your app is purely static with no server-side logic.

How many PM2 workers should I run for Next.js?
Start with one instance per available CPU core. For a 2-vCPU server, 2 instances is reasonable. Monitor memory — Next.js is more memory-heavy than lightweight Node.js apps.

What is the difference between next build and next start?
next build compiles your application for production. next start runs the compiled application. You must run next build before next start each time you deploy changes.

How do I handle Next.js image optimization on a VPS?
Next.js image optimization requires a running server (SSR mode). For static exports, disable it with images: { unoptimized: true } or use EdgeOne CDN for image processing at the edge.

Can Next.js API routes run on a VPS?
Yes — when running in SSR mode with PM2, API routes (/api/*) function normally. For static export, API routes are not included and must be hosted separately.

Deploy your Next.js app today:
👉 Tencent Cloud Lighthouse — Node.js-ready Ubuntu VPS
👉 View current pricing and promotions
👉 Explore all active deals and offers