Deployment guide

Deploy a Next.js Application on a VPS

Deploy on a VPS Cloud →

Tutorial

Deploy a Next.js Application on a VPS

Development13 min read6 steps

Next.js combines server-side rendering, static generation, and API routes in a single React framework. Deploying it on a VPS rather than on a proprietary platform frees you from function quotas, bandwidth limits, and vendor lock-in, while keeping SSR fully operational.

Contents· Why Self-Host Next.js on a VPS?1/14
  1. 01Why Self-Host Next.js on a VPS?
  2. 02Concrete Benefits of a Self-Hosted Next.js
  3. 03Hardware and Software Prerequisites
  4. 04Docker Multi-Stage Build: node:20-alpine
  5. 05Environment Variables: .env.local vs .env.production
  6. 06Deploy Next.js Step by Step
  7. 07Complete Nginx Configuration with Upstream
  8. 08ISR Strategy: Revalidation and Cache Persistence
  9. 09Server Components vs Client Components: RAM and CPU Impact
  10. 10CI/CD: GitHub Actions or Forgejo
  11. 11Monitoring with PM2
  12. 12Troubleshooting: Common Production Errors
  13. 13Backing Up .next/cache
  14. 14Deploy Next.js in One Click from the Marketplace

Why Self-Host Next.js on a VPS?

Next.js is often associated with a specific hosting platform, but its standalone Node.js server runs perfectly on any VPS. Self-hosting becomes worthwhile as soon as you make intensive use of SSR, ISR (incremental static regeneration), or API routes: these features consume billed invocations on managed platforms, whereas they are free and without quota on your server. You control the ISR cache on disk, the bandwidth of optimized images, and the function execution time, with no 10-second limit. For an agency hosting several client sites, a VPS pools the costs and simplifies billing.

Concrete Benefits of a Self-Hosted Next.js

  • SSR and API routes with no invocation quota or per-function billing.
  • Persistent ISR cache on disk, no revalidation lost between deployments.
  • Included bandwidth, ideal for image- and video-rich sites.
  • Several Next.js projects on a single VPS, pooled under Nginx.
  • next/image image optimization served locally with no per-transformation extra cost.
  • Build and deployment controlled via Git, CI/CD, or a simple git pull and rebuild.

Hardware and Software Prerequisites

The Next.js build is memory-hungry: plan for at least 2 GB of RAM (4 GB for a large project with many pages), otherwise the build may fail for lack of memory. On the runtime side, 1 to 2 vCPU are enough to serve SSR. Install Node.js 18 or 20 LTS, either natively with nvm or via a node:20-alpine Docker image. Use PM2 to supervise the Node process, or Docker for isolation. A domain pointed to the VPS is required for SSL. Enable output: 'standalone' in next.config.js for a lightweight deployment.

Docker Multi-Stage Build: node:20-alpine

A Docker multi-stage build reduces the final image to the essentials and avoids shipping build tools into production. The three-stage strategy — deps, builder, runner — is the most common approach for Next.js in standalone mode.

# Stage 1 — dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

# Stage 2 — build
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

# Stage 3 — runner (final image)
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]

The deps stage installs production-only dependencies (--omit=dev). The builder stage copies the full source and runs npm run build with access to modules. The runner stage contains only the standalone folder generated by Next.js, static assets, and the public folder — no source node_modules, no build configuration files. The final image is often two to three times smaller than a naive image.

Environment Variables: .env.local vs .env.production

Next.js distinguishes two families of variables according to their scope.

Public variables (NEXT_PUBLIC_*): they are bundled into the JavaScript bundle at build time and exposed to the browser. They are suitable for a public API URL, a Google Analytics ID, or a feature flag. Never place a secret in them.

Server variables: read only on the Node side (API routes, Server Components, getServerSideProps). They are never sent to the client. This is where third-party API keys, database connection strings, and JWT secrets live.

# .env.local — local development (never committed)
NEXT_PUBLIC_API_URL=http://localhost:4000
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb

# .env.production — production values (never committed either)
NEXT_PUBLIC_API_URL=https://api.example.com
DATABASE_URL=postgresql://user:[email protected]:5432/mydb

On the VPS, two approaches coexist: inject variables into the PM2 process environment via its ecosystem file (ecosystem.config.js), or pass them to Docker via --env-file .env.production. The second is preferable: the file stays on the server's disk, outside the git repository, and Docker mounts it at container startup without exposing it in logs.

Note: NEXT_PUBLIC_* variables must be known at build time, not just at startup. If you change a public variable after the build, you must rebuild the application.

Deploy Next.js Step by Step

  1. Prepare the Server

    Over SSH, install Node.js 20 LTS and PM2 (npm install -g pm2), or Docker. Clone the repository and create .env.production with your variables (NEXT_PUBLIC_* for the client, server secrets for the API routes).

  2. Build the Application

    Run npm ci, then npm run build. With output: 'standalone', Next.js generates a self-contained .next/standalone folder containing only the necessary dependencies, which greatly lightens the image.

  3. Start the Node Server

    Start the server with pm2 start node --name nextjs -- .next/standalone/server.js on port 3000, or via a Docker container. Configure pm2 startup and pm2 save for automatic restart on VPS reboot.

  4. Configure Nginx as the Front End

    Create a server block that does proxy_pass http://localhost:3000, forwards the Host and X-Forwarded-For headers, and serves /_next/static/ directly from disk to relieve Node. Enable gzip compression.

  5. Install the SSL Certificate

    Obtain a Let's Encrypt certificate via Certbot for yourdomain.com, force the HTTPS redirect, and configure automatic renewal. Verify that the X-Forwarded-Proto headers are properly forwarded for SSR.

  6. Set Up Deployments

    Automate the git pull && npm ci && npm run build && pm2 reload nextjs cycle via a script or a Git webhook. PM2's reload ensures a zero-downtime restart between two versions.

Complete Nginx Configuration with Upstream

A complete Nginx block for Next.js goes beyond the minimal proxy_pass. You need to serve static assets directly from disk, manage cache headers, enable compression, and correctly forward client information.

upstream nextjs_upstream {
  server 127.0.0.1:3000;
  keepalive 64;
}

server {
  listen 80;
  server_name yourdomain.com www.yourdomain.com;
  return 301 https://$host$request_uri;
}

server {
  listen 443 ssl http2;
  server_name yourdomain.com www.yourdomain.com;

  ssl_certificate     /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

  gzip on;
  gzip_types text/plain text/css application/javascript application/json image/svg+xml;

  # Next.js static assets — long-duration cache
  location /_next/static/ {
    alias /home/deploy/myapp/.next/static/;
    expires 1y;
    add_header Cache-Control "public, immutable";
  }

  # Public files (images, favicons…)
  location /public/ {
    alias /home/deploy/myapp/public/;
    expires 30d;
  }

  # Everything else to Next.js
  location / {
    proxy_pass http://nextjs_upstream;
    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;
  }
}

The upstream block with keepalive 64 maintains a pool of persistent connections to Node, avoiding TCP negotiation on every request. The proxy_http_version 1.1 directive is required for keepalive connections to actually work. _next/static assets receive Cache-Control: immutable because Next.js places content hashes in their URLs — the file never changes at the same URL.

ISR Strategy: Revalidation and Cache Persistence

ISR (Incremental Static Regeneration) allows Next.js to regenerate a static page in the background after a given delay, without a full rebuild. On a VPS, this requires the cache to survive restarts.

// app/products/[id]/page.tsx (App Router)
export const revalidate = 3600; // regenerate every hour

// pages/products/[id].tsx (Pages Router)
export async function getStaticProps() {
  return {
    props: { ... },
    revalidate: 3600
  };
}

On the VPS, the ISR cache is stored in .next/cache. To make it survive redeployments, two options exist.

Option 1 — persistent volume: mount .next/cache outside the deployment folder. During a git pull and rebuild, copy the old cache before overwriting .next, then restore it.

Option 2 — Redis cache handler: for multiple Node instances or a sharing requirement between machines, Next.js supports custom cache handlers since version 13.4. This allows externalizing the ISR cache to Redis, guaranteeing consistency between instances.

For low-traffic sites, option 1 is sufficient. Option 2 becomes necessary when you have multiple Node processes (PM2 cluster) or multiple VPSs behind a load balancer.

Server Components vs Client Components: RAM and CPU Impact

Since Next.js 13 and the App Router, components are Server Components by default. The distinction has direct consequences for your VPS resource consumption.

Server Components run only on the server side. They can read the database directly, access the filesystem, and are never sent to the browser's JavaScript bundle. The result is pure HTML, which reduces the client bundle size and improves Core Web Vitals (LCP). On the server, they consume CPU on each non-cached SSR request.

Client Components ('use client' at the top of the file) are hydrated in the browser. They are necessary for user interactions: events, local state, hooks (useState, useEffect). They increase the size of the JavaScript bundle sent to the client.

The practical rule on a VPS: keep Server Components for everything data-related, and limit Client Components to interaction zones. A component rendered 10,000 times per hour on the server consumes CPU; the same rendered client-side consumes the visitor's browser RAM, not your server.

CI/CD: GitHub Actions or Forgejo

Automating deployment avoids human errors and ensures that every merge to the main branch triggers a clean rebuild. Two common solutions on VPS: GitHub Actions (if your repository is on GitHub) and Forgejo (self-hosted forge, open-source alternative to GitHub/Gitea).

GitHub Actions — deployment workflow via SSH:

name: Deploy Next.js
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.VPS_HOST }}
          username: deploy
          key: ${{ secrets.VPS_SSH_KEY }}
          script: |
            cd /home/deploy/myapp
            git pull origin main
            npm ci
            npm run build
            pm2 reload nextjs

Forgejo — if you self-host your forge on the same VPS or a dedicated server, use a Forgejo Act runner. The workflow syntax is identical to GitHub Actions, which facilitates migration.

In both cases, secrets (SSH key, environment variables) are configured in the repository secrets and never appear in logs.

Monitoring with PM2

PM2 is both the process manager and the first monitoring tool for your Next.js application. Key commands:

# Process status
pm2 list

# Real-time logs
pm2 logs nextjs

# CPU and RAM metrics
pm2 monit

# Zero-downtime restart
pm2 reload nextjs

# Full restart (brief downtime)
pm2 restart nextjs

PM2 can also log metrics to a file and export them to external systems. For more advanced monitoring, enable the pm2-logrotate module to prevent log files from filling up the disk:

pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 50M
pm2 set pm2-logrotate:retain 7

In production, save the process list after each change: pm2 save. This list is automatically reloaded at system startup if you have run pm2 startup and validated the command it provides.

If you use ISR, mount a persistent volume for the .next/cache folder so that revalidated pages survive redeployments. Without this, each rebuild starts from an empty cache and causes a spike of on-the-fly generation. For several Node instances behind a load balancer, externalize this cache to shared storage or Redis with a custom cache handler.

Troubleshooting: Common Production Errors

Three to five errors recur systematically on the first deployment of a Next.js application on a VPS.

ENOMEM during npm run build
The Next.js build compiles in parallel and can exceed 2 GB of RAM on a medium-sized project. If the build stops with Killed or JavaScript heap out of memory, increase Node's memory limit:

NODE_OPTIONS="--max-old-space-size=4096" npm run build

Port 3000 already in use
An old PM2 process or orphan Node process is already listening on port 3000. Identify and stop it:

lsof -i :3000
kill -9 <PID>
# Or, if PM2 already has the process:
pm2 delete nextjs
pm2 start ecosystem.config.js --env production

MODULE_NOT_FOUND in production with output: 'standalone'
The .next/standalone folder contains a copy of the necessary dependencies, but not the static assets or the public folder. If you copy only standalone, these resources are missing and the server crashes at startup. Copy all three folders:

cp -r .next/static .next/standalone/.next/static
cp -r public .next/standalone/public

X-Forwarded-Proto error and HTTPS redirect loop
If Next.js detects that the request is HTTP while it has already passed through Nginx as HTTPS, it may create a redirect loop. Ensure Nginx forwards X-Forwarded-Proto: https and that your application reads this header to determine the actual protocol.

Backing Up .next/cache

The .next/cache folder contains two types of valuable data: revalidated ISR pages and the Webpack/SWC compilation cache. Losing this cache forces Next.js to regenerate all ISR pages on the fly at the first hit, which can create a load spike if your site has many static pages.

Set up a simple backup with rsync or tar before each deployment. The Webpack cache significantly speeds up subsequent rebuilds — on an average project, a rebuild with a full cache takes 40 to 60 % less time than a cold build.

Deploy Next.js in One Click from the Marketplace

The ServOrbit Marketplace offers a Next.js Stack template that automatically configures Node.js LTS, PM2, Nginx, and PostgreSQL on your VPS. In a few minutes, your production environment is ready to receive your React SSR application — no manual setup required.

Compared to the installation described in this guide, the template handles the initial setup and lets you jump straight to the "deploy your code" step.

Your Next.js Environment Ready in Minutes

The Next.js Stack template automatically configures Node.js LTS, PM2, Nginx, and PostgreSQL — ready to receive your React SSR application.

Need help?

Browse our help center and FAQ, or reach our team — callback, WhatsApp or email. Support in French, English and Arabic.

Message us on WhatsAppopens in a new tab