Next.js doesn’t need a specialised hosting platform. On your own VPS you get predictable costs, no function time limits, and full control over caching and data location. Server-side rendering, API routes, middleware and image optimisation all work when you run Next.js as a Node.js server.

What you need

  • Ubuntu with Node.js 20 or newer (check what your Next.js version requires) — see installing Node.js on Ubuntu.
  • Nginx for HTTPS and static assets.
  • RAM: next build is memory-hungry. Builds can need 1–2 GB+ for medium apps; on a 1–2 GB server, add swap or build elsewhere (CI) and deploy the result.

1. Use standalone output

In next.config.js (or .mjs/.ts):

const nextConfig = {
  output: 'standalone',
};
module.exports = nextConfig;

next build then produces .next/standalone/ — a minimal server (server.js) with only the node_modules it actually needs. Deployments become much smaller and don’t need a full npm install on the server.

2. Build

On the server (or in CI):

cd /srv/web/app
npm ci
npm run build

Standalone output doesn’t include static files and the public folder; copy them in:

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

Test it:

cd .next/standalone
PORT=3000 HOSTNAME=127.0.0.1 node server.js
curl -I http://127.0.0.1:3000

3a. Run with PM2

npm install -g pm2
cd /srv/web/app/.next/standalone
PORT=3000 HOSTNAME=127.0.0.1 pm2 start server.js --name web
pm2 save
pm2 startup     # run the command it prints

Environment variables: NEXT_PUBLIC_* variables are inlined at build time, so they must be set when running next build. Server-only variables are read at runtime — set them in a PM2 ecosystem file or .env.production. See the PM2 guide.

3b. Or run with Docker

FROM node:22-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-slim
WORKDIR /app
ENV NODE_ENV=production PORT=3000 HOSTNAME=0.0.0.0
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/.next/static ./.next/static
COPY --from=build /app/public ./public
USER node
EXPOSE 3000
CMD ["node", "server.js"]
docker build -t web:latest .
docker run -d --name web --restart unless-stopped -p 127.0.0.1:3000:3000 --env-file .env.production web:latest

Building the image in CI and pulling it on the server keeps the build’s memory spike off your production machine. See Docker multi-stage builds.

4. Nginx in front

server {
    listen 80;
    server_name example.com;

    location /_next/static/ {
        alias /srv/web/app/.next/static/;
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        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_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Serving /_next/static/ straight from disk is optional (Next.js serves it too) but faster. Files there have content hashes in their names, so a one-year immutable cache is safe. With Docker, either drop that block or copy the static files to the host.

sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d example.com

5. Image optimisation

next/image optimises images on the server using sharp, which current Next.js versions install automatically. Optimised images are cached in .next/cache/images — make sure that directory is writable and persists across restarts (a Docker volume, if containerised). On small servers, many large source images can cause CPU spikes; pre-size images where you can.

6. ISR and caching across restarts

Incremental Static Regeneration and the data cache store files in .next/cache. With one server, that just works. With several servers behind a load balancer, each has its own cache — configure a shared cache handler if pages must be consistent everywhere.

7. Deploy updates

A simple script for PM2 deployments:

#!/bin/bash
set -euo pipefail
cd /srv/web/app
git pull --ff-only
npm ci
npm run build
cp -r public .next/standalone/
cp -r .next/static .next/standalone/.next/
pm2 reload web

pm2 reload restarts gracefully. For true zero downtime with a single process, build into a new directory and switch a symlink before reloading, or run two instances behind Nginx.

Troubleshooting

  • Build killed / JavaScript heap out of memory — not enough RAM; add swap, raise NODE_OPTIONS=--max-old-space-size=…, or build in CI.
  • CSS/JS 404s — static files weren’t copied into the standalone folder.
  • Wrong client IP or protocol — check the X-Forwarded-* headers in Nginx.
  • Server Actions fail behind a proxy — the Host/origin must match; keep proxy_set_header Host $host.

Prefer static export? For sites without server features, output: 'export' produces plain HTML you can serve with Nginx alone — see deploying a React app with Nginx. A 2–4 GB Ubuntu VPS comfortably runs a production Next.js app.

  • Next.js
  • Node.js
  • Deployment