Migrating From Vercel to Your Own VPS Without Downtime
Moving a production Next.js app off Vercel is mostly a DNS problem and an env-var problem. The app itself ports cleanly to a container; the risk is in the cutover. This is the ordered playbook I follow: inventory what Vercel is actually doing, stand the new box up on a throwaway hostname, get a real certificate before anyone's traffic touches it, then flip one A record with a low TTL so I can roll back in minutes if something is wrong.
Inventory What Vercel Does Before You Touch Anything
Vercel is more than a host. Before you migrate, write down everything it quietly handles for you, because each item is a thing you now own. In a typical project that list is: building the app on every push, storing environment variables (separated by environment), preview deployments per pull request, the production domain plus its SSL certificate, serverless and edge functions, image optimization, web analytics, and any cron jobs you defined in vercel.json.
Be honest with yourself about the hard parts. A single VPS sits in one region. Vercel's edge network and edge middleware run close to every user worldwide, and incremental static regeneration (ISR) at high traffic leans on their distributed cache. You can replicate the behavior of edge middleware and ISR on one box, but you cannot replicate the global, multi-region latency profile. If your app genuinely depends on sub-50ms responses on three continents, a single VPS is a different trade-off, not a drop-in swap. For the vast majority of apps — dashboards, marketing sites, SaaS backends, internal tools — one well-placed server is completely fine.
Vercel Feature to VPS Equivalent
| Vercel feature | VPS equivalent | Difficulty |
|---|---|---|
| Build on push | Docker build triggered by a GitHub webhook | Clean map |
| Environment variables | Env vars injected into the container at runtime | Clean map |
| Custom domain + SSL | Reverse proxy with automatic Let's Encrypt certs | Clean map |
| Serverless functions | Next.js API routes inside the same container | Clean map |
| Image optimization | Built-in next/image (needs sharp) or a CDN | Needs config |
| Preview deploys per PR | One staging app, or a per-branch subdomain | Rethink |
| Cron jobs | System cron hitting an API route | Needs config |
| Edge middleware | Runs as Node middleware in one region | Rethink |
| ISR at scale / multi-region | Single-node ISR cache, no global edge | Hard |
Migrate Environment Variables First
Env vars are the most common cause of a migration that builds fine but breaks at runtime. Pull them out of Vercel up front. The CLI exports them per environment:
vercel env pull .env.production --environment=productionReview the file by hand before importing it anywhere. Two things matter here. First, separate genuine secrets (database URLs, API keys, signing secrets) from public config — anything prefixedNEXT_PUBLIC_ ends up in the client bundle and is not a secret. Second, do not commit the exported file. Delete it from your working tree once the values are loaded into the new platform, and rotate any key you suspect has been pasted into a chat, a ticket, or a CI log along the way. Migration is a natural moment to rotate credentials anyway.
Handle the Next.js Specifics
For a containerized build, set output: "standalone" innext.config.js. Standalone output produces a self-contained .next/standalone directory with a minimal server and only the dependencies you actually use, which keeps the image small and the cold start fast.
Image optimization is the detail people forget. On Vercel it is automatic; on your own box,next/image needs the sharp library present in the runtime image, or optimization quietly degrades. ISR works on a single node — pages regenerate and are cached on that server — but there is no shared edge cache across regions, so the first request after a revalidation is served by your one origin. For most apps that is invisible. If you were leaning hard on global ISR, plan for a CDN in front of the VPS rather than expecting parity out of the box.
Get a Real Certificate Before the Flip
This is the step that trips up zero-downtime migrations. The standard Let's Encrypt HTTP-01 challenge proves you control a domain by serving a token over HTTP — which means DNS for that name has to point at your server before the certificate can be issued. If you wait until after the A-record flip to request the cert, there is a window where the new box is live but serving an invalid certificate, and every visitor gets a browser warning.
The fix is to verify everything on a test hostname first. Point a name you control — say new.yourdomain.com — at the VPS, let the proxy issue a valid cert for that hostname, and confirm the full app works over HTTPS there: the build, every env var, auth flows, API routes, image rendering. Only once the test host is genuinely green do you move on to the production domain. When you finally flip the production A record, the proxy requests the production certificate immediately, and because DNS now resolves to your box, the HTTP-01 challenge passes within seconds.
The Zero-Downtime DNS Cutover
The cutover itself is a DNS technique, not a deploy technique. The whole point is that you never take the old deployment down before the new one is proven and propagated.
Ahead of the cutover — at least a day before, ideally two — lower the TTL on the domain's A record to something small like 300 seconds (5 minutes) or 60 seconds. TTL changes only take effect after theold TTL has expired everywhere, so if your record is currently sitting at 3600 or higher, you have to make this change in advance or the flip will propagate slowly. Confirm what resolvers see with:
dig +nocmd yourdomain.com A +noall +answer
dig +nocmd yourdomain.com A @8.8.8.8 +noall +answerThe first command shows your local resolver; the second queries Google's public DNS directly, which is useful for checking propagation from outside your network. When both still show the old (low) TTL ticking down and you've confirmed the test host is fully working, change the production A record to the VPS IP. Keep the Vercel deployment running. For the next propagation window some users will still hit Vercel and some will hit the VPS — both must serve the app correctly, which they will, because you migrated the env vars and verified the build first. Once dig from several networks consistently returns the new IP, you can decommission the Vercel project.
Keep a Rollback Path Open
Your rollback is the low TTL. Because you dropped it before the flip, reverting is fast: change the production A record back to Vercel and, within one TTL window, traffic returns to the old deployment. This is exactly why you do not delete the Vercel project on cutover day. Leave it running and untouched until the new box has served real production traffic cleanly for a day or two. Only after you are confident — not on the same afternoon — do you raise the TTL back to a normal value (3600 or higher) and tear the old deployment down.
The Step-by-Step Checklist
- 1.Inventory every Vercel feature in use: build, env vars, previews, domains/SSL, functions, image optimization, analytics, cron.
- 2.Export env vars with
vercel env pull, split secrets from public config, and plan any rotations. - 3.Add
output: "standalone"to the Next.js config and confirmsharpis in the runtime image. - 4.Provision the VPS, deploy the app, and point a test hostname (
new.yourdomain.com) at it. - 5.Let the proxy issue a valid SSL cert for the test host and verify the whole app over HTTPS: build, env, auth, API routes, images.
- 6.One to two days before cutover, lower the production A-record TTL to 60–300 seconds and confirm with
dig. - 7.Flip the production A record to the VPS IP. Keep Vercel running throughout.
- 8.Watch propagation with
dig @8.8.8.8from a few networks; if anything breaks, flip the record back. - 9.After a day or two of clean traffic on the VPS, raise the TTL back to normal and decommission the Vercel project.
Where AODE Fits
The mechanical parts of this migration — connecting the GitHub repo, detecting the project type, running the Docker build, routing through Traefik, and issuing Let's Encrypt certificates — are what AODE automates on your own VPS. You set your env vars in the dashboard, point your test hostname at the server to verify SSL, and auto-deploy on push keeps the box current after the move. That leaves the migration as what it really is: an env-var export and a careful DNS flip, with deployment history and rollbacks on the server if a build goes wrong.
Try AODE
One-time purchase, lifetime license, 14-day money-back guarantee. Deploy from GitHub to your own VPS with automatic SSL and auto-deploy on push.
Related Articles
Last updated: May 2026