Most devs spin up a $50 Vercel plan without thinking. Here's how to run a production Next.js app on a $5 Hetzner box with zero downtime deploys, proper process management, and Nginx doing the heavy lifting — the same setup that handled 2M monthly visits without breaking a sweat.

In 2023, a fintech startup I was advising got a $14,000 Vercel bill. Fourteen thousand dollars. For a Next.js app with maybe 800k monthly active users. The CEO thought it was a hack. It wasn't. It was just Vercel's bandwidth pricing doing exactly what it said it would do in the fine print nobody reads.
That's when I sat down and rebuilt their deployment pipeline on a $5 Hetzner CAX11 ARM instance. Monthly bill dropped to $5.18. Same app. Same traffic. The only thing that changed was who controlled the server.
Zero downtime deployment isn't magic. It's just running two versions of your app simultaneously for about 30 seconds, then flipping which one gets traffic. That's it. Everything else is implementation detail.
The thing most tutorials skip: Next.js next start binds to a port. You can't run two processes on port 3000. So you run them on 3000 and 3001, let Nginx proxy to whichever one is healthy, then kill the old one. Blue-green deployment, $5 version.

Get your Hetzner box. Ubuntu 22.04. Then install PM2 — not just as a process manager but as your deployment brain.
npm install -g pm2@latest
pm2 startup systemdPM2's ecosystem.config.js is where most people stop reading docs too early. The increment_var field lets you offset ports per instance. You want two apps defined: app-blue on port 3000 and app-green on 3001. Your deploy script checks which one PM2 is currently routing traffic through, deploys to the idle one, waits for a health check to pass, then tells Nginx to switch upstream.
The health check matters more than people think. Don't just check if the port responds. Hit /api/health and have that endpoint actually query your database. A Next.js process that starts but can't reach Postgres is worse than no process at all because it'll return 500s instead of the old version returning real data.
Your Nginx config needs two upstream blocks and a variable to switch between them. Here's the non-obvious part: Nginx doesn't hot-reload upstream changes without a reload, but nginx -s reload is graceful. It finishes in-flight requests before workers restart. That's your zero-downtime guarantee.
upstream blue { server 127.0.0.1:3000; }
upstream green { server 127.0.0.1:3001; }
server {
location / { proxy_pass http://blue; }
}Your deploy script writes a new Nginx config pointing to the green upstream, runs nginx -t to validate it, and only then runs nginx -s reload. If nginx -t fails, the old config stays. You never go down.

Static assets. Next.js builds put a content hash in every filename, so /_next/static/chunks/abc123.js from the old build and /_next/static/chunks/def456.js from the new build are different files. During the 30-second switchover, users with the old HTML in their browser will request old chunk filenames. If you already deleted the old build directory, those requests 404.
Fix: keep two build directories. /var/app/blue and /var/app/green. Deploy the new build to the idle directory before you ever touch Nginx. Old users finish loading from the old directory. New users get the new one. Delete the old build directory only after 5 minutes, not immediately.
That one detail is the difference between zero downtime and "mostly zero downtime except for users with slow connections."