background-shape
Deploying Next.js 12, Vercel vs Self-Hosted
April 27, 2022 · 6 min read · by Muhammad Amal programming

TL;DR — Vercel for marketing sites, small apps, anything where the team’s time is worth more than the platform tax. Self-hosted (Docker on your own infra) for cost-sensitive workloads, regulated industries, or apps already living in an existing cluster. Feature parity is close; ISR + image optimization need slight setup on self-hosted.

After data fetching, middleware, and image optimization, the question is where to run all this. Vercel is the default — Next.js is their product — but plenty of teams (including this one) self-host. Both work. This post is the honest comparison.

This blog you’re reading runs on Vercel. The internal admin app runs in our own Kubernetes cluster. Different shapes; different choices.

What Vercel gives you for free

  • Zero-config deployment: git push → preview URL on every PR, prod on main merge
  • Edge network for static assets + Edge runtime for middleware/API routes
  • Automatic image optimization (no setup, no extra service to run)
  • Automatic ISR (revalidation just works)
  • Per-deployment isolated previews with their own URLs
  • Built-in analytics (Web Vitals, RUM)
  • Free tier covers small projects (with usage caps)

The pitch: you don’t think about infrastructure. You focus on the app.

What it costs

For a real production app with meaningful traffic:

  • Pro plan: $20/month base, then bandwidth + serverless invocation overages
  • 1 TB bandwidth included, $0.40/GB after
  • 1M serverless invocations included, $0.65/M after
  • Edge Functions billed separately
  • Image optimization billed by source images

For a marketing site with ~100K monthly visits: ~$30–60/month. Trivial.

For a SaaS app with 5M serverless invocations / month + 5 TB bandwidth: ~$500–2000/month. Real money, but compare to engineer time.

For a high-traffic content site (10M+ visits/month): ~$2000–5000/month. Now you’re thinking.

The honest framing: Vercel’s price scales with success. At some threshold, self-hosting saves money. The threshold is higher than most teams hit.

What self-hosting gives you

  • Cost predictability (your infra, your bandwidth, your CDN)
  • Data residency control (run in specific regions for compliance)
  • Existing infra reuse (you already have a Kubernetes cluster)
  • No vendor lock-in (the door swings both ways)
  • Custom infra integrations (in-cluster databases, private VPCs, on-prem services)

What self-hosting costs you (in effort)

  • A Dockerfile to write + maintain
  • A Node-running host (k8s, ECS, Cloud Run, Fly.io, bare VM)
  • CDN setup for static assets (Cloudflare, CloudFront, BunnyCDN)
  • Image optimization story (run Next.js’s built-in, or offload to Cloudinary/imgix)
  • CI pipeline for builds + deploys (the GitHub Actions patterns from February’s posts)
  • Preview environments (more work to replicate Vercel’s per-PR previews)

For a team that already runs production services on Kubernetes, self-hosting Next.js adds maybe two days of setup. For a team that’s never run a Node service in production, self-hosting adds two weeks of yak-shaving.

A working Dockerfile for self-hosted

# syntax=docker/dockerfile:1.4
FROM node:18-alpine AS deps
WORKDIR /app
RUN apk add --no-cache libc6-compat
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci

FROM node:18-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:18-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001

# Next.js standalone output for minimal image
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/public ./public

USER nextjs
EXPOSE 3000
ENV PORT=3000
CMD ["node", "server.js"]

The standalone output mode is the key. Add to next.config.js:

module.exports = {
  output: 'standalone',
};

This bundles only the production dependencies your app actually uses into .next/standalone/, with a minimal server.js. Final image: ~150 MB (vs 1+ GB with node_modules).

Kubernetes manifest

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nextjs-app
spec:
  replicas: 3
  selector:
    matchLabels: { app: nextjs-app }
  template:
    metadata:
      labels: { app: nextjs-app }
    spec:
      containers:
      - name: web
        image: ghcr.io/yourorg/web:sha-abc123
        ports:
        - containerPort: 3000
        readinessProbe:
          httpGet:
            path: /api/health
            port: 3000
          periodSeconds: 5
        livenessProbe:
          httpGet:
            path: /api/health
            port: 3000
          periodSeconds: 30
        resources:
          requests:
            cpu: 100m
            memory: 256Mi
          limits:
            cpu: 500m
            memory: 512Mi

/api/health is a Next.js API route that returns 200 if the app is alive:

// pages/api/health.ts
export default function handler(req, res) {
  res.status(200).json({ ok: true });
}

Front this with a CDN (Cloudflare, CloudFront) for static asset caching. Next.js sets long-lived cache headers on /_next/static/* — the CDN does the heavy lifting.

ISR on self-hosted

Vercel handles ISR transparently. Self-hosted requires slightly more care:

  • Single-instance Node servers: ISR works out of the box (in-memory cache + filesystem persistence).
  • Multi-instance deployments: revalidation happens per-instance. Each instance regenerates independently. Mostly fine; can lead to slight inconsistency across instances briefly.
  • For perfectly synced ISR across instances: external store (Redis adapter) — community plugins exist; not production-ready in 12.1.

For most apps the per-instance regeneration is acceptable. If you need strict consistency, use SSR instead.

Image optimization on self-hosted

Two options:

Run Next.js’s built-in optimizer. It works on Node hosts; default next/image Just Works. Each pod runs its own image-resizing process. Cache to local disk or PVC. Fine at small scale.

Offload to a CDN. Configure images.loader to point at Cloudinary, imgix, or your own CDN’s image transformer. Next.js generates URL-encoded requests; the CDN does the actual transform. Better for high traffic.

// next.config.js
module.exports = {
  images: {
    loader: 'cloudinary',
    path: 'https://res.cloudinary.com/yourcloud/image/fetch/',
  },
};

Edge runtime on self-hosted

Vercel runs middleware at the Edge globally. Self-hosted middleware runs on your Node host (single region, no global Edge).

Functionally identical, but the latency benefit of Edge is gone — middleware runs at the same hop as the rest of your app. Not a dealbreaker; just know what you’re giving up.

Decision matrix

Situation Pick
Marketing site, < 100K visits/month Vercel
SaaS app, no existing infra team Vercel
Anything in a regulated industry (HIPAA, PCI) Self-hosted (or Vercel Enterprise with BAA)
Already running production services on K8s Self-hosted
Per-PR preview environments are critical Vercel (much easier)
Bandwidth/invocation costs > $1500/month Self-hosted (probably)
Need specific data residency Self-hosted
Need < 100 ms response globally Vercel (Edge network) or multi-region self-hosted

What our two apps do

This blog (Hugo, not Next.js — but the principle holds): Vercel. Zero-config. Cheap. The build is fast, the team is small, the value of not thinking about infrastructure is high.

Internal admin (Next.js + auth + per-user data): self-hosted in our K8s cluster. Lives next to the services it talks to. Authenticates against our internal SSO. Logs flow through our Loki stack. No external bandwidth bills. The setup cost was real but one-time.

Different apps, different right answers.

Common Pitfalls

Choosing Vercel “because everyone does.” It’s good. It’s also a vendor. Evaluate against your needs, not the default.

Choosing self-hosted to save $100/month while paying an engineer $200/day to maintain it. Math. Vercel is cheap compared to engineer time at small scale.

Self-hosting without a CDN. Static assets served from your Node host are slow and expensive. Always front with a CDN.

ISR on multi-instance without thinking through consistency. Per-instance regen is usually fine; pathological cases (revalidation race conditions) need attention.

Forgetting output: 'standalone'. Without it, your self-hosted Docker image is 4× larger than it needs to be.

Mixing Vercel-specific APIs into a self-hosted app. Things like geo.country from middleware work on Vercel but not self-hosted (no geo enrichment). Be explicit about platform dependencies.

Wrapping Up

Vercel vs self-hosted is a real trade-off, not a foregone conclusion. Vercel wins on velocity and developer experience; self-hosted wins on cost predictability and infra integration past a scale threshold. Both are valid. Friday: the April retro wrapping up the month and the quarter on frontend modernization.