Back to writing
Original post by Gagan Deep Singh, canonical on thegdsks.comAlso on Dev.toDiscuss on Dev.to

Leave Vercel for Your Own Server: A Next.js Migration Runbook

Leave Vercel for Your Own Server: A Next.js Migration Runbook Moving off Vercel is hard...

5 min read

Leave Vercel for Your Own Server: A Next.js Migration Runbook

Moving off Vercel is hard because of configuration, not code. Vercel holds configuration in its dashboard that never lived in your repo: env vars per environment, domain redirects, cron schedules, integrations. This runbook inventories that first, then maps each piece to Levelrail, a self-hosted platform I am building.

Vercel has no automatic importer. Everything here is manual. Run the new setup beside Vercel, and keep Vercel as your rollback until the checks at the end pass.

TL;DR#

Phase Goal
1. Inventory Write down everything Vercel holds
2. Map Match each concept to a self-hosted counterpart
3. Build Deploy to a test URL, no real domain yet
4. Cutover Lower DNS TTLs, switch, watch
5. Shadow run Run both until boring problems show up

Phase 1: inventory#

Collect this before you touch anything new.

Item Where in Vercel Why it matters
Env vars per environment Project Settings, Environment Variables You decide how Production, Preview and Development map over
NEXT_PUBLIC_* vars Same list Compiled in at build time, never read at runtime
Domains and redirects Project Settings, Domains You recreate each one
DNS provider and TTLs Your registrar You lower TTLs before cutover
Cron jobs vercel.json and the Cron tab Vercel cron hits an HTTP path
Functions config vercel.json, runtime = "edge" in code Nothing replaces the edge runtime
Storage and add-ons Integrations Managed storage does not move with the app

Phase 2: the concept map#

Vercel Levelrail
Project An app with one or more services
Git deploy on push A git source with push trigger mode
Framework preset build railpack build type, or dockerfile
Static export static build type, served by Caddy with no container
Environments Optional environments in a project, with promote
Sensitive env vars Encrypted, write-only secrets
Preview deployments Per pull request previews, opt in per app
Instant Rollback Rollback by deploy id, pinned by digest
Cron Jobs Scheduled tasks that run a command in the container
Vercel Postgres A managed Postgres you restore a dump into

Two rows hide a difference worth knowing. Vercel cron calls an HTTP route, while a scheduled task runs a command inside your running container, so the command must use a tool your image actually contains. And previews here are per pull request, never per branch.

The build-time trap#

next build bakes NEXT_PUBLIC_* values in. The platform injects env only when a container starts, so it never reaches the build. Commit a .env.production with the public values and every build type picks it up. The deploy guide in this series explains the failure mode in detail.

Phase 3: build it on a test URL#

Leave domains out of the first deploy so nothing competes with Vercel for your real hostname. Create the app from git, load plain env with a dry run first, then secrets:

bash
levelrail-cli apps env import your-app --file prod.env --dry-run
levelrail-cli apps secrets set your-app --env-file secrets.env
levelrail-cli apps apply your-app

Copy values from the Production column in Vercel, not Preview. Add a cheap readiness route that does not depend on outside services, then check pages, API routes, auth callbacks, image requests and streaming responses on the test URL.

If the app uses Vercel Postgres, create a managed database, restore a dump into it and rehearse that restore before cutover.

What has no counterpart#

Plan for these before you commit.

Vercel feature Status
Edge Functions and Middleware None. Node-compatible middleware works, edge-only code must move to the Node runtime
Image Optimization CDN next/image runs in your container on your CPU
Global CDN and edge caching None built in. Caddy serves from your nodes
Web Analytics and Speed Insights No browser-side analytics. Server request metrics exist
Blob, KV, Edge Config No drop-in match. Managed Redis exists but you rewrite the client
Autoscaling and scale to zero Fixed replicas only
Plain HTTP to HTTPS redirect Not done by the ingress today. Add it at DNS or a CDN in front

If any row is a deal breaker, you just saved yourself a migration.

Phase 4: DNS cutover with a way back#

  1. Lower TTLs to 60 to 300 seconds, at least one full old TTL before cutover.
  2. Add the real domains, plus the www redirect.
  3. Have a certificate ready first. Public issuance needs the domain pointing at the server, so expect a short gap, or upload your own certificate beforehand.
  4. Test with a host override before you change DNS:
bash
curl --resolve YOUR_DOMAIN:443:SERVER_IP YOUR_DOMAIN_URL
  1. Switch the A and AAAA records. Write down the old values first.
  2. Watch logs and metrics from outside your network.
  3. To roll back, restore the old records. With a low TTL most clients return in minutes.

Do not remove the domain from Vercel until the shadow run is clean.

Phase 5: the shadow run#

Run both platforms for a full business cycle, including any weekly or monthly cron. Check at least these:

  • A deliberately broken deploy fails while the old container keeps serving
  • A rollback works from the CLI and the dashboard
  • Every Production env key from Vercel exists
  • NEXT_PUBLIC_* values are correct in the deployed bundle
  • OAuth callback URLs updated at each provider
  • Payment and other webhooks point at the new URL
  • Each scheduled task ran on time
  • The certificate arrived and has expiry alerting
  • A database backup ran and you rehearsed a restore
  • Error rate and latency compare acceptably with Vercel

What to check before trusting it#

Levelrail has no stable release, and real public certificate issuance has had less field testing than the rest of the ingress. Treat this as a parallel run, not a leap.

Which Vercel feature is the one you would find hardest to replace?

Go deeper#

Try it, and tell me what breaks#

Levelrail is open source under Apache 2.0. It is young, so every bug report changes what gets built next.

glincker/levelrail

If this post saved you time, a star on the repo helps other self-hosters find it. Bugs and feature requests go in the issue tracker.


GDS K S · thegdsks.com · building Glincker · follow on X @thegdsks

Leaving a platform gets easy once you write down everything it quietly did for you.

This article is published here first and cross-posted to Dev.to. This page is the canonical version.
Contact