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...

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:
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-appCopy 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#
- Lower TTLs to 60 to 300 seconds, at least one full old TTL before cutover.
- Add the real domains, plus the
wwwredirect. - 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.
- Test with a host override before you change DNS:
curl --resolve YOUR_DOMAIN:443:SERVER_IP YOUR_DOMAIN_URL- Switch the A and AAAA records. Write down the old values first.
- Watch logs and metrics from outside your network.
- 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#
- Migrating a Next.js app from Vercel
- Domains, TLS and ingress
- Feature status, with the evidence behind each claim
- New to Levelrail? Start with the five minute install
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.
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.