# 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.

Source: https://zopanel.net/blog/deploy-nextjs-to-vps  
Updated: 2026-10-21

## 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](/blog/deploy-nodejs-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](https://nextjs.org/docs/app/guides/self-hosting) 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](https://nextjs.org/blog/next-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](https://nextjs.org/docs/app/guides/self-hosting#configuring-caching), for example Redis |
| Proxy (formerly middleware) | Works | [Next.js 16 renamed middleware to Proxy](https://nextjs.org/docs/app/api-reference/file-conventions/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](https://vercel.com/docs/deployments/environments). 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](https://vercel.com/pricing) 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](https://www.hetzner.com/cloud/cost-optimized/) (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

```js
// 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](https://nextjs.org/docs/app/api-reference/config/next-config-js/output) notes that `server.js` does **not** copy `public` or `.next/static`, because they "should ideally be handled by a CDN". Copy them yourself:

```bash
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](https://nextjs.org/docs/app/guides/memory-usage) 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`:

```ini
[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](https://nextjs.org/docs/app/api-reference/config/next-config-js/output) 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.

```bash
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](/blog/deploy-nodejs-vps):

```nginx
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](https://nextjs.org/docs/app/guides/self-hosting#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

```bash
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](https://letsencrypt.org/getting-started/) 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](/blog/cloudflare-with-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](/blog/git-auto-deploy) 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](/docs/nodejs) and [Git deploy](/docs/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](/pricing) (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](https://nextjs.org/docs/app/getting-started/deploying#docker) 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.
