Deploy Next.js to a VPS without Vercel

How to deploy Next.js to a VPS: standalone output, systemd, nginx caching for /_next/static, SSL, zero-downtime releases, and what you give up versus Vercel.

Diagram: a Next.js standalone server behind nginx with SSL on a VPS, compared with Vercel

Key takeaways

  • Set output to standalone, copy public and .next/static into .next/standalone, and run node server.js with PORT and HOSTNAME set.
  • On one server, image optimization, ISR and Proxy (formerly middleware) work without extra setup; several instances need a shared cache handler and a fixed Server Functions key.
  • Let nginx serve /_next/static directly with a one-year immutable cache and keep old static files after a deploy to avoid version skew.
  • Vercel Pro costs $20 per developer seat per month at the time of writing; a VPS costs less but you take over the CDN, previews and operations.

To deploy Next.js to a VPS without Vercel, set output: 'standalone' in next.config, run next build, copy the public and .next/static folders into .next/standalone, and start node server.js under systemd with PORT and HOSTNAME set. Put nginx in front for the domain, SSL and long-lived caching of /_next/static. On a single server, image optimization, ISR and Proxy (formerly middleware) work out of the box; what you give up is Vercel's global CDN, preview deployments and the fact that someone else runs the infrastructure.

This guide covers only what is specific to Next.js. For the general setup (Node LTS, a dedicated user, firewall, systemd versus PM2) see deploying a Node.js app to a VPS.

What do you lose when you leave Vercel?

Less than many people expect, as long as you run one server. The Next.js self-hosting guide states which features work on a plain Node.js server:

Feature On one VPS (next start or standalone) What changes
Image optimization (next/image) Works with zero configuration Since Next.js 15, sharp is used automatically with next start and standalone output. Resizing uses your server's CPU and RAM.
ISR and the data cache Works; cache is stored on the local disk Several instances each keep their own cache unless you add a shared cache handler, for example Redis
Proxy (formerly middleware) Works Next.js 16 renamed middleware to Proxy and it runs on the Node.js runtime by default. It runs on your server, not at edge locations.
Streaming and Suspense Works nginx must not buffer the response (see below)
Preview deployments Not included Vercel creates one for every push to a non-production branch and every pull request. On a VPS you build that yourself or use a staging site.
Global CDN Not included Static files come from one location unless you add a CDN such as Cloudflare
Operations Your job OS updates, backups, monitoring and capacity are yours

The one real trap is running more than one instance. The Next.js docs list what then becomes necessary: a shared cache handler, the same NEXT_SERVER_ACTIONS_ENCRYPTION_KEY on every instance (otherwise you get "Failed to find Server Action" errors), and a deploymentId for version skew. If one VPS is enough, you can skip all of that.

Vercel vs a VPS: what does it cost?

At the time of writing (October 2026), Vercel's pricing page lists:

Vercel Hobby Vercel Pro Hetzner CX23 VPS
Price $0 $20 per developer seat per month €5.99 per month excl. VAT
Allowed use Personal, non-commercial Commercial projects Any
Included Usage caps; cannot buy more $20 usage credit, 1 TB Fast Data Transfer, 10M edge requests per month 2 vCPU, 4 GB RAM, 40 GB NVMe, 20 TB traffic
Above the included amount Not possible Data transfer from $0.15 per GB Extra traffic billed per TB

Hetzner figures come from its Cost-Optimized cloud plans page (EU locations). This is not apples to apples: Vercel's price includes a CDN, previews, builds and no server to maintain, while a VPS gives a fixed bill, more bandwidth and room for a database and other sites. For a team that values previews and zero operations, Vercel can be worth it.

How to deploy Next.js to a VPS by hand

These steps assume Node.js 22 or 24, nginx and a user called nextapp already exist.

Step 1: Turn on standalone output

// next.config.js
module.exports = {
  output: 'standalone',
}

During next build, Next.js traces which files each route needs and writes a self-contained folder to .next/standalone, including the required parts of node_modules and a minimal server.js. You deploy that folder without running npm install on the server. The output reference notes that server.js does not copy public or .next/static, because they "should ideally be handled by a CDN". Copy them yourself:

npm ci
npm run build
cp -r public .next/standalone/
cp -r .next/static .next/standalone/.next/

Step 2: Mind the build memory

next build usually needs more memory than the running app, and a build killed by the out-of-memory killer only prints Killed. The Next.js memory usage guide lists the levers: experimental.webpackMemoryOptimizations, disabling source maps (productionBrowserSourceMaps: false), and running type checks in CI instead of during the build. Practical options on a small VPS:

  • Add swap, so a peak slows the build instead of killing it.
  • Build in CI (GitHub Actions) and ship only .next/standalone. Standalone output makes this easy because the folder needs no node_modules install.

Step 3: Run server.js with systemd

Create /etc/systemd/system/nextapp.service:

[Unit]
Description=Next.js app
After=network.target

[Service]
User=nextapp
Group=nextapp
WorkingDirectory=/srv/nextapp/current
Environment=NODE_ENV=production
Environment=PORT=3000
Environment=HOSTNAME=127.0.0.1
EnvironmentFile=-/etc/nextapp.env
ExecStart=/usr/local/bin/node server.js
Restart=on-failure
KillSignal=SIGTERM
TimeoutStopSec=30
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/srv/nextapp

[Install]
WantedBy=multi-user.target

/srv/nextapp/current is a symlink to the release folder that contains server.js (the contents of .next/standalone). HOSTNAME=127.0.0.1 keeps the server off the public interface; the output docs confirm server.js reads PORT and HOSTNAME. ReadWritePaths matters because ISR writes its cache to disk inside .next. The 30-second stop timeout follows the self-hosting guide, which asks for a graceful SIGTERM and a 10 to 30 second drain so in-flight requests and after() callbacks finish.

Runtime-only secrets go in /etc/nextapp.env. Remember that NEXT_PUBLIC_* variables are inlined at build time, so changing them on the server does nothing until you rebuild.

sudo systemctl daemon-reload
sudo systemctl enable --now nextapp
curl -I http://127.0.0.1:3000

Step 4: nginx with cached /_next/static

Files under /_next/static have a content hash in their names, and Next.js already sends Cache-Control: public, max-age=31536000, immutable for them. Let nginx serve them straight from disk so Node never sees those requests. The $connection_upgrade map for WebSockets is the one from the Node.js guide:

upstream nextapp {
    server 127.0.0.1:3000;
}

server {
    listen 80;
    server_name example.com www.example.com;

    location /_next/static/ {
        alias /srv/nextapp/static/;
        add_header Cache-Control "public, max-age=31536000, immutable";
        access_log off;
    }

    location / {
        proxy_pass http://nextapp;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        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 $connection_upgrade;
    }
}

/srv/nextapp/static/ is a shared folder that every deploy adds its .next/static files to (cp -rn), and old files are kept for a while. That avoids a classic self-hosting bug: a visitor whose page was loaded before the deploy requests an old chunk that no longer exists. The Next.js docs call this version skew and note that keeping old immutable assets around prevents it.

For streaming and Suspense, nginx must not buffer. Either add proxy_buffering off; to the location / block, or send X-Accel-Buffering: no from headers() in next.config.js as the self-hosting guide shows.

Step 5: SSL with Certbot

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
sudo certbot renew --dry-run

Certbot gets a free Let's Encrypt certificate, adds the HTTPS server block and installs a renewal timer. If you want a CDN in front for visitors far from your server, see using Cloudflare with a VPS; the immutable headers above let it cache /_next/static safely.

Step 6: Zero-downtime releases

systemctl restart drops requests for a few seconds while the new server boots. A blue/green switch avoids that:

  1. Build into a new folder, for example /srv/nextapp/releases/20261021-1200, and copy its static files into the shared static folder.
  2. Start it on the idle port with a systemd template unit (nextapp@3001.service, using Environment=PORT=%i).
  3. Wait until curl -fsS http://127.0.0.1:3001/ answers.
  4. Change the server line in the upstream block to port 3001, then nginx -t && systemctl reload nginx.
  5. Stop the old instance after its in-flight requests finish (systemctl stop nextapp@3000).

Note that ISR pages regenerate from an empty cache in the new release folder, and keep the previous release so a rollback is steps 2 to 5 pointing at it. Auto-deploy from GitHub explains how to trigger this from a push.

Deploying Next.js with ZoPanel

ZoPanel runs the app as a service behind nginx with SSL and health-checked releases, inside a hosting account with its own resource limits:

  1. Create a website of type Git deploy with the repository and branch; keep Free SSL (Let's Encrypt) on. ZoPanel detects Next.js from package.json and proposes your start script, or npx next start. Image optimization works because next start uses sharp automatically.
  2. ZoPanel sets PORT and HOST=127.0.0.1, and next start reads PORT on its own. Do not combine output: 'standalone' with next start. If you prefer standalone, turn Auto-detect off in Build & run, append the two cp commands to the build command with &&, and set the start command to node .next/standalone/server.js. Standalone's server.js reads HOSTNAME, not HOST, so add HOSTNAME=127.0.0.1 under Environment variables.
  3. Each deploy builds in a new release folder within the package's CPU and memory limits. The new release starts in the idle slot and must answer the Health check path below status 500 within 90 seconds before nginx switches to it; otherwise the live release keeps running. Rollback keeps the last 5 builds.
  4. Turn on Deploy when main is pushed and add the webhook to GitHub, GitLab or Gitea.

Things to know: NODE_ENV=production is set only at runtime so devDependencies still install for the build; NEXT_PUBLIC_* values added in Environment variables are available to the build; and files the app writes inside a release folder (including the ISR cache) start fresh with each release, so put uploads in Persistent paths. If a build is killed, raise the package's RAM. Use every CPU core runs one process per CPU, which makes the app multi-instance: only turn it on if you have handled the shared cache and in-memory state described above. ZoPanel does not give you preview deployments or a global CDN. Details are in Node.js apps and Git deploy.

Conclusion

Self-hosting Next.js on a VPS is a solved problem for a single server: standalone output (or next start), systemd, nginx serving /_next/static with an immutable cache, Certbot, and a blue/green switch. Scaling to several instances or wanting previews is where Vercel earns its price. If you want the build, health check, rollback and SSL handled for you, try ZoPanel's Free plan (3 hosting accounts, 10 websites, 10 databases, no time limit) on a test VPS and deploy your Next.js repository.

Frequently asked questions

Can I use Vercel's free Hobby plan for a commercial Next.js site?

No. Vercel's pricing page describes Hobby as "for personal, non-commercial use" with fixed usage caps. A commercial project needs Pro, which at the time of writing costs $20 per developer seat per month, or a self-hosted setup.

Does next/image work on a VPS?

Yes. Since Next.js 15, sharp is used automatically with next start and standalone output, so images are resized on your server. On glibc-based Linux, the Next.js docs point to sharp's memory allocator settings if image optimization uses too much memory. You can also set unoptimized and optimize images yourself.

How much RAM do I need to build Next.js on a VPS?

It depends on the project size, so measure it: run next build --experimental-debug-memory-usage or watch memory while it builds. On a small VPS, add swap or build in CI and deploy the standalone folder. The running server usually needs far less memory than the build.

Do I need Docker to self-host Next.js?

No. node server.js under systemd behind nginx is simpler on a single VPS. Docker is useful when you want the same image on several servers or in an orchestrator, and the Next.js deployment docs cover that option.

Does ISR work when I self-host?

Yes. On a single instance, ISR and revalidatePath/revalidateTag use the local filesystem cache without any setup. With several instances you need a shared cache handler, for example Redis, so a revalidation on one instance reaches the others.