How to self-host Next.js (and why you might want to)
The Reflex Team8 min16 May 2026
Vercel built Next.js and they offer the best zero-config deployment experience for it. That is not in dispute. What is in dispute is whether the pricing model makes sense once your project outgrows hobby scale.
This is not a "Vercel is bad" post. It is a "here is how to self-host Next.js when the economics or control requirements justify the operational overhead" guide.
When Vercel costs become uncomfortable
Vercel's pricing is transparent, but the numbers surprise teams who grew up on flat-rate VPS hosting. As of early 2026, the Pro plan is $20/seat/month — reasonable for small teams. But the real costs come from usage: bandwidth overage at $40/100GB, serverless function execution, and edge middleware invocations. A moderately trafficked e-commerce site with ISR can easily hit $200-500/month before you add team seats.
For comparison, a $48/month VPS on Hetzner or DigitalOcean gives you 4 vCPUs, 8GB RAM, and 20TB bandwidth. The trade-off is operational responsibility — you manage the server, the process, the deployments, and the SSL.
The breakpoint is usually around $150-200/month in Vercel bills. Below that, Vercel's DX advantage is worth the premium. Above that, the economics of self-hosting become compelling — especially if you already manage servers for your API.
Self-hosting with PM2
Next.js in production mode is a Node.js server. PM2 manages it like any other Node process:
npm run build
pm2 start npm --name "nextjs" -- start
pm2 save
pm2 startup
A proper ecosystem.config.js:
module.exports = {
apps: [{
name: 'nextjs',
script: 'node_modules/.bin/next',
args: 'start -p 3000',
instances: 'max',
exec_mode: 'cluster',
max_memory_restart: '512M',
env_production: {
NODE_ENV: 'production',
PORT: 3000,
},
}],
};
Cluster mode is critical — a single Next.js process uses one CPU core. With instances: 'max', PM2 forks one worker per core and load-balances incoming connections across them. On a 4-core VPS, that is a 4x throughput improvement for zero extra configuration.
Self-hosting with Docker
For teams already using containers, a production Dockerfile:
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
This uses Next.js's standalone output mode (set output: 'standalone' in next.config.js), which produces a self-contained server without node_modules. The final image is typically under 150MB.
nginx configuration
nginx sits in front as a reverse proxy, handling SSL, compression, static file caching, and connection management:
upstream nextjs {
server 127.0.0.1:3000;
keepalive 32;
}
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml;
location /_next/static/ {
alias /var/www/nextjs/.next/static/;
expires 365d;
add_header Cache-Control "public, immutable";
}
location /public/ {
alias /var/www/nextjs/public/;
expires 30d;
}
location / {
proxy_pass http://nextjs;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
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_cache_bypass $http_upgrade;
}
}
Static asset serving through nginx instead of Node.js is the single biggest performance win. Next.js's _next/static/ directory contains immutable hashed assets — serve them with a year-long cache header and let nginx handle the I/O instead of occupying a Node.js worker.
What you lose versus Vercel
Be honest about the trade-offs:
- Edge functions and middleware — Vercel runs these at the CDN edge. Self-hosted, they run on your origin. For most apps, the latency difference is negligible. For globally distributed audiences, it matters.
- ISR (Incremental Static Regeneration) — works on self-hosted Next.js, but Vercel's implementation integrates with their CDN for cache invalidation. Self-hosted ISR relies on your own caching strategy.
- Preview deployments — Vercel's per-PR preview URLs are excellent DX. Self-hosting this requires your own CI/CD pipeline with dynamic routing.
- Zero-config image optimisation — Next.js's
<Image>component works self-hosted but uses your server's CPU for on-the-fly resizing. Consider a CDN like Cloudflare or imgproxy.
What you gain
- Predictable costs — a flat monthly server bill instead of usage-based pricing that scales with traffic
- Full control — custom nginx rules, server-level caching strategies, co-location with your API for low-latency internal calls
- No vendor lock-in — your deployment is standard Node.js, portable to any Linux server
- Data residency — choose exactly where your server lives, which matters for GDPR compliance
How Reflex helps
Self-hosting means self-monitoring. Reflex's reflexd agent watches the PM2 process tree, tracks Node.js memory and event loop health, monitors nginx upstream status, and handles the 3am restart that would otherwise require you to SSH in. The Brain applies the same repair playbooks to your Next.js server that it uses for PHP-FPM, Python WSGI, or any other supervised process — because at the OS level, the failure modes are remarkably similar.