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 buildis 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, raiseNODE_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; keepproxy_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.