50% off SaaS Starter Kit — only for the first 100 buildersGrab it →
← Back to blog
deploymentMay 25, 2026·10 min read

Self-Hosting Next.js: When Vercel Isn't the Right Fit

Vercel is great until it isn't. Here's when to ditch it, what self-hosting actually costs you, and how to do it without losing your mind.

Robert Seghedi

Robert Seghedi

Co-founder, peal.dev

Self-Hosting Next.js: When Vercel Isn't the Right Fit

We love Vercel. It's the easiest way to ship a Next.js app, the DX is excellent, and the edge network is genuinely fast. For most projects, it's the right call. But we've hit situations — and talked to enough developers to know we're not alone — where Vercel's pricing model, data residency requirements, or runtime constraints make it the wrong tool. This isn't a Vercel takedown. It's a pragmatic look at when you should consider leaving, and what that actually looks like in practice.

The Three Reasons People Actually Leave Vercel

People say they're leaving Vercel for a dozen reasons, but honestly it boils down to three: cost at scale, data compliance, and platform lock-in anxiety. Let's be specific about each one.

Cost is the obvious one. Vercel's pricing is consumption-based, which sounds great when you're small, but function invocations, bandwidth, and build minutes add up faster than you'd expect. We've seen teams hit $800/month on Vercel for apps that would run comfortably on a $40 VPS. The math gets especially brutal if your app has high traffic but low revenue — internal tools, open source projects, or early-stage products where you need to keep burn low.

Data compliance is less discussed but increasingly real. If you're building for European healthcare, finance, or enterprise customers, you'll get asked where your data lives and who can access it. Vercel processes request data through US infrastructure by default, and while they have GDPR compliance documentation, some customers won't sign off on it. We had one potential customer tell us point-blank: 'If it touches Vercel, it's not going through legal.' That's a deal-breaker you can't engineer around.

Lock-in anxiety is the squishiest reason but worth taking seriously. If your entire deployment infrastructure lives in Vercel — edge config, environment variables, cron jobs, blob storage — migrating later is painful. Some teams want the optionality of being able to move to AWS, Hetzner, or Fly.io without a two-week rewrite.

What You're Actually Giving Up

Before you spin up a VPS at 2am out of spite because your Vercel bill spiked, let's be honest about what you're trading away. Vercel isn't just hosting — it's a bunch of infrastructure you take for granted.

  • Automatic preview deployments for every PR (you can replicate this, but it takes work)
  • Global CDN with automatic asset optimization — self-hosting means managing this yourself or paying for a CDN separately
  • Instant rollbacks via the dashboard — on a VPS you're managing this with your deployment scripts
  • Zero-config SSL certificates and domain management
  • Built-in analytics and real user monitoring
  • Edge Functions and Edge Config with very low latency globally — genuinely hard to replicate cheaply

The preview deployment piece alone has saved us more arguments than we can count. When a non-technical stakeholder can click a link and see exactly what the PR looks like, you avoid a whole category of miscommunication. If you're moving to self-hosting, make sure you have a plan for this — or accept that you're trading it away.

The Self-Hosting Options (Ranked by Pain Level)

There's a spectrum here. Let's go from 'barely feels different' to 'you are now a DevOps engineer.'

Coolify or Dokku on a VPS is the sweet spot for most indie developers and small teams. You get a Heroku-like experience on infrastructure you control. Hetzner's CX21 (2 vCPU, 4GB RAM) costs about €5/month and handles a surprising amount of traffic for a Next.js app. Add Coolify on top and you get automatic deployments from Git, SSL, reverse proxy — the whole thing. This is what we'd recommend to 80% of people asking about self-hosting.

Docker on a managed VM (Fly.io, Render, Railway) sits in the middle. You're writing a Dockerfile, but you're not managing the underlying server. Fly.io in particular is worth considering — regions worldwide, pay-per-use, and the deploy experience is close to Vercel. The catch is that anything non-trivial in your Next.js app (ISR, image optimization, middleware) needs to be handled at the Docker layer.

Full Kubernetes is probably overkill unless you're an enterprise with dedicated platform engineering. We've seen startups set this up and spend more time managing the cluster than building product. Strong opinion: if you're considering Kubernetes for a Next.js app with under 100k daily users, you're over-engineering it.

A Real Dockerfile That Works

The official Next.js Docker example is fine but verbose. Here's a slimmed-down version that's served us well in production — multi-stage build, standalone output mode, non-root user for security:

FROM node:20-alpine AS base

# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json pnpm-lock.yaml* ./
RUN corepack enable pnpm && pnpm i --frozen-lockfile

# Build the application
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Standalone output packs everything needed to run the app
ENV NEXT_TELEMETRY_DISABLED=1
RUN corepack enable pnpm && pnpm build

# Production image — lean and mean
FROM base AS runner
WORKDIR /app

ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

# Don't run as root
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

CMD ["node", "server.js"]

The key thing here is `output: 'standalone'` in your `next.config.ts`. Without it, the Docker image drags in all of `node_modules` and ends up 500MB+. With standalone mode, you get a self-contained bundle that's much leaner. Add this to your config:

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  output: 'standalone',
  // If you're behind a reverse proxy (nginx, Caddy, Coolify's Traefik),
  // you need this to trust the X-Forwarded-* headers
  // without it, req.ip and HTTPS detection break
  experimental: {
    // Only needed if you're using Server Actions with CSRF protection
    // and sitting behind a proxy
  },
}

export default nextConfig

The Stuff That Actually Breaks

Here's the honest part. We've self-hosted Next.js apps and there are footguns that will catch you off guard if you haven't done it before.

Image optimization breaks without a separate service. Next.js's built-in `<Image>` component does on-the-fly resizing and format conversion. In a Docker container, you need to either install the `sharp` package (make sure it's built for the right platform — Alpine Linux uses musl, not glibc), configure an external loader like Cloudinary or Imgix, or disable optimization entirely and serve images as-is. Sharp on Alpine is our usual call, but if you're doing cross-platform Docker builds on an M1 Mac targeting AMD64, you'll hit native module compilation issues. We learned this while deploying from a gas station on a phone hotspot because of course we did.

ISR (Incremental Static Regeneration) needs shared storage in multi-instance setups. If you're running more than one container behind a load balancer, each instance has its own local cache. A revalidation on instance A doesn't propagate to instance B. For most small deployments this doesn't matter because you're running one container, but the moment you scale horizontally, you need a shared cache — Redis or a similar store. Next.js 15 has better support for custom cache handlers, so this is more solvable than it used to be, but it's still extra infrastructure.

Environment variables need to be injected at runtime, not build time. On Vercel this is handled for you. On a self-hosted setup, you control this — which means you need to think about how secrets get into your container. Docker secrets, environment files, a secrets manager like AWS Secrets Manager or Vault — pick one and be consistent. The mistake we see people make is baking env vars into the Docker image at build time, which means your image contains production secrets and you can't reuse the same image across environments.

# When building, don't pass secrets as build args
# This gets baked into the image layers and is a security risk
# BAD:
docker build --build-arg DATABASE_URL=postgres://... -t myapp .

# Instead, pass them at runtime
# GOOD:
docker run \
  -e DATABASE_URL=postgres://... \
  -e NEXTAUTH_SECRET=... \
  -p 3000:3000 \
  myapp

# Or use an env file (make sure .env.production is in .gitignore)
docker run --env-file .env.production -p 3000:3000 myapp

Reverse Proxy Setup You Can Actually Use

You're going to need something in front of your Next.js container to handle SSL termination, gzip compression, and routing. Caddy is our first choice for self-hosted setups because it handles Let's Encrypt certificates automatically with almost zero config:

# Caddyfile
yourdomain.com {
  reverse_proxy localhost:3000

  # Caddy handles HTTPS, HTTP/2, and gzip automatically
  # You don't need to configure any of this

  # Cache static assets at the proxy layer
  @static {
    path /_next/static/*
  }
  header @static Cache-Control "public, max-age=31536000, immutable"
}

If you're using Nginx instead, make sure you set the `X-Forwarded-Proto` and `X-Real-IP` headers, otherwise Next.js middleware won't know the request came in over HTTPS and you'll see weird redirect loops or auth failures. It's a classic '2am debugging session' category of bug.

The Honest Cost Comparison

Let's put numbers on this. A typical production app on Vercel Pro might run $50-200/month depending on traffic. Migrating to self-hosting:

  • Hetzner CX21 (2 vCPU, 4GB): ~€5/month. Handles moderate traffic easily.
  • Hetzner CX31 (4 vCPU, 8GB) with managed Postgres and a separate Redis: ~€30/month total
  • Fly.io for global distribution with 2-3 regions: ~$30-60/month depending on usage
  • Add a CDN (Cloudflare free tier handles most static asset caching) and you're set

The hidden cost is your time. Setting up the initial infrastructure properly takes a day or two. Ongoing maintenance — OS patches, monitoring, occasional debugging — is maybe an hour a month if things are running smoothly. If your time is worth anything, the cost savings need to be real to justify it. For a $50/month Vercel bill, it's probably not worth the switch unless you have compliance requirements or genuinely enjoy this stuff. For a $500/month bill, the math changes fast.

Self-hosting isn't free — it trades money for time and responsibility. The question isn't 'can I self-host' but 'should I, given what my time costs and what problems I'm actually solving.'

If you're building on one of our peal.dev templates, they're structured to work on both Vercel and self-hosted setups from day one — no Vercel-specific APIs, environment handling that works in Docker, and image handling that degrades gracefully without platform-specific loaders. We've been burned enough times by templates that assumed Vercel that we made this a hard requirement.

When to Stay on Vercel

After all that, here's when you should not self-host: when you're early stage and every hour matters, when your traffic is genuinely global and edge latency is a product requirement, when you don't have anyone on the team who wants to own infrastructure, or when your Vercel bill is under $100/month and you have no compliance requirements. Vercel's DX is legitimately excellent and the time you'd spend self-hosting is usually better spent on the product. The 'just use Vercel' advice isn't lazy — it's often correct.

But when your bill hits a threshold that hurts, when a customer's legal team says no, or when you want to own your infrastructure stack, self-hosting Next.js is absolutely viable. It's not the dark art it used to be — standalone output mode, Docker, and tools like Coolify have taken most of the pain out of it. Just go in with eyes open about what you're taking on, plan for the image optimization and ISR edge cases, and don't do the migration at 2am on a Friday.

Newsletter

Liked this post? There's more where it came from.

Dev guides, honest build stories, and the occasional 2am debugging confession — straight to your inbox. No spam, unsubscribe anytime.

Browse templates
Written by humansWeekly dropsSubscriber perks

Join the Discord

Ask questions, share builds, get help from founders