Skip to content

How to move a Next.js app off Vercel

Nick Potts, who builds Ployz. Published .

TL;DR: Moving a Next.js app off Vercel takes one line of config and a server that costs about $7 a month. Next.js runs under next start with ISR, image optimization and Proxy (the new name for Middleware) working out of the box. The real work is the Vercel services around the app: Cron Jobs, Blob, a Marketplace database and the DNS switch.

  • Move the app first, the database later. Keep it pointed at Neon until DNS has switched, so a rollback is one DNS change.
  • Cron Jobs are ordinary GET routes. Call them from a crontab, an external scheduler or GitHub Actions, behind a secret header.
  • Blob is the only step that changes code. Copy the files to an S3-compatible bucket like Cloudflare R2 and swap the SDK calls.
  • Start with one instance. Two or more need a shared ISR cache and a fixed Server Actions key.
  • Keep Vercel live for a few weeks after the switch. One more month of Pro buys a one-step rollback.
  • Use Ployz, which I build, for the deploy part, especially if a coding agent will do the move. It gives you push-to-deploy, a preview environment per pull request and HTTPS on the server you rent, and deletes wait for you.

Leaving Vercel means running next start on a server you rent and replacing the Vercel-only services your app uses. For scale: in my Vercel pricing breakdown, a Next.js app at 1 TB of traffic costs $120 a month on Vercel Pro and about $7 on a rented server on Ployz's Free plan, custom domains included. That gap is why people leave, and this guide is how to leave without breaking anything.

What do you need to replace when you leave Vercel?

Fewer things than you'd think. Next.js itself handles ISR, image optimization, Proxy and streaming. What needs a real replacement is Cron Jobs, Blob storage, any Marketplace database, Web Analytics, preview deployments and the global CDN.

Vercel featureWhat replaces itEffort
Functions (Route Handlers, Server Actions, SSR)next start on your server. Same code.Low
Edge Middlewareproxy.ts, which runs on Node.js by default in Next.js 16Low
Edge runtime routesDelete export const runtime = 'edge'; it's deprecatedLow
ISRBuilt in. One server: nothing to do. Several: a shared cache handler (Redis)Low to medium
Image optimizationBuilt into next start, using sharpLow
Cron JobsAn external scheduler, GitHub Actions, or a crontab that calls your routeMedium
BlobAny S3-compatible bucket (Cloudflare R2, Backblaze B2, AWS S3) plus a code changeMedium to high
Postgres (Neon via Marketplace)Keep Neon, or pg_dump into your own PostgresMedium
KV / Redis (Upstash via Marketplace)Keep Upstash, or run Redis next to the appLow
Environment variablesExport with vercel env pull, import on the new hostLow
Preview deploymentsA host with per-PR previews, or a staging serverDepends on host
Web Analytics / Speed InsightsA self-hosted tool like Umami or PlausibleLow
Global CDNCloudflare in front of your server, or nothingLow
Domains and DNSLower TTLs, switch records, keep the old deployment runningLow, but plan it

Step 1: Find out what you use on Vercel

Look in four places: vercel.json (crons, rewrites, headers, redirects), package.json (any @vercel/* package), the project's environment variables, and the Storage tab. The Usage tab tells you how big the new server needs to be.

grep -rn "@vercel/" package.json src app lib 2>/dev/null
cat vercel.json
vercel env ls production

What usually turns up:

  • crons in vercel.json. Each entry is a path and a schedule. Vercel triggers them by making "an HTTP GET request to your project's production deployment URL" (Vercel Cron docs), so they're ordinary routes.
  • @vercel/blob. The one that needs code changes.
  • Variables like DATABASE_URL, POSTGRES_URL, KV_URL. Vercel's Marketplace connects databases from Neon, Upstash and Supabase, and "Vercel injects provisioned resource credentials as environment variables" (Vercel Storage). The data lives at Neon or Upstash, so it doesn't have to move on day one.
  • @vercel/analytics and @vercel/speed-insights. These stop being useful once you leave. Export what you want first: each analytics panel exports up to 250 entries as CSV (Vercel Web Analytics).

Step 2: Make the app run with next start

The Next.js self-hosting guide says image optimization and Proxy work "with zero configuration when deploying using next start". ISR works automatically "for a single self-hosted next start instance with persistent local disk". If your host runs next build and next start for you, most apps need no changes beyond the ones below.

If you build your own Docker image, use output: 'standalone'. It creates .next/standalone with a minimal server.js and only the files your app needs. The output docs warn that it "does not copy the public or .next/static folders by default", so copy them yourself or your CSS and images 404:

next build
cp -r public .next/standalone/ && cp -r .next/static .next/standalone/.next/
PORT=3000 HOSTNAME=0.0.0.0 node .next/standalone/server.js

Rename Middleware to Proxy. Next.js 16 renamed middleware.ts to proxy.ts, and "Proxy defaults to using the Node.js runtime" (Proxy docs). The codemod is npx @next/codemod@canary middleware-to-proxy .. Off Vercel it runs on your server, not at the edge, which is fine for auth checks and redirects.

Remove edge runtime exports. The runtime docs list 'edge' as deprecated and say to remove the runtime export from route files.

Install sharp if images fail. next/image keeps working, but Next.js's error page for a missing sharp says standalone builds must have it installed. If you see that error, run npm i sharp.

Give the build its NEXT_PUBLIC_ variables. The self-hosting guide says they "will be inlined into the JavaScript bundle during next build", so your new host needs them during the build, not only at runtime. This is the most common reason a migrated app shows undefined in the browser.

Don't buffer streaming. If you put nginx in front, the guide suggests sending X-Accel-Buffering: no.

What about ISR on more than one server?

On one server, ISR works with no changes because the cache sits on that server's disk. On two or more, each instance has its own cache, so they can serve different versions of a page, and revalidateTag() "only invalidates the cache on that instance". On Vercel, by contrast, "all caches across all regions update within 300ms" on revalidation (Vercel ISR docs).

The self-hosting guide lists what to set once you run more than one copy:

SettingWhyWhen
cacheHandler + cacheMaxMemorySize: 0Shared ISR cache in Redis or S3; Next.js links a Redis example2+ instances
refreshTags() in the cache handlerSo revalidateTag() reaches every instance2+ instances
NEXT_SERVER_ACTIONS_ENCRYPTION_KEYOtherwise you get "Failed to find Server Action" errors2+ instances
deploymentIdVersion skew protection during rolling deploysAny zero-downtime deploy
generateBuildIdSame build ID if you build once per environmentMulti-stage builds

Start with one instance. I think a 4 GB server runs a typical Next.js app comfortably, and you skip the whole table except deploymentId. Add replicas when you have a reason.

Step 3: Replace Vercel Cron Jobs

Your cron routes are ordinary GET endpoints, so the code stays. Point something that sends an HTTP request on a schedule at them, and protect each route with a secret header, because it's now a public URL anyone can call.

OptionExampleCatch
A crontab on the server*/15 * * * * curl -fsS -H "Authorization: Bearer $CRON_SECRET" https://example.com/api/cron/syncFree and exact, but if that machine is down, the job doesn't run
An external schedulerCalls a URL on a scheduleSurvives server changes
GitHub Actions scheduleA scheduled workflow that runs curlShortest interval is 5 minutes, runs "can be delayed during periods of high loads", and public repos disable schedules after 60 days without activity (GitHub docs)

Vercel's crons always run in UTC (Vercel Cron docs). A server crontab uses the server's timezone, so check it before copying schedules.

Step 4: Move environment variables

Pull production variables with vercel env pull --environment=production, check them, and load them into the new host. Vercel stores each variable as Config or Secret, and Production and Preview default to Secret. For Secret variables "whose values cannot be read from Vercel", the pull keeps the value already in your local file, or "writes a placeholder" if there isn't one (vercel env docs). Search the pulled file for placeholders and re-fetch those keys from Stripe and the like. Skip Vercel's own variables, like VERCEL_OIDC_TOKEN.

Step 5: Move files out of Vercel Blob

Copy every file to an S3-compatible bucket, then swap the @vercel/blob calls for an S3 client. Vercel Blob is built on Amazon S3 but has its own API, which is why this step changes application code. The Vercel Blob CLI lists 1,000 blobs per page with --cursor for the next page, and vercel blob get <pathname> --output <file> downloads one, so a short script loops through both.

  • Pick a bucket. Cloudflare R2 is a good default for public files: $0.015 per GB-month, 10 GB-month free, and free egress (R2 pricing).
  • Keep the same pathnames so you can rewrite stored URLs with one UPDATE ... SET url = replace(url, old_host, new_host).
  • Replace put, del and list with S3 SDK calls. Private blobs become signed URLs.
  • Keep the Vercel store until the migration has been live for a while.

Step 6: Set up previews and analytics

Preview deployments need a host that builds per pull request, or a staging environment; ephemeral environments covers both. For analytics, Umami is a small Node app with Postgres that covers what Vercel Analytics does.

Step 7: Switch DNS without downtime

Run the new host on its own address until it passes your checks, then point the A record at it, with the Vercel deployment still running. Visitors who still resolve the old address get a working site either way.

  1. Check who runs your DNS: dig NS example.com +short. If it says vercel-dns.com, your records live in Vercel.
  2. If you're on Vercel's nameservers, move DNS first, as its own step. Copy every record to the new DNS provider, including MX, SPF, DKIM and verification TXT records. Nameserver changes "can take up to 48 hours to propagate" (Vercel DNS docs), so do this a week before the app move.
  3. Lower the TTL to 60 seconds a day or two before. Vercel DNS defaults to 60 seconds. On Cloudflare, unproxied records go down to 60 seconds on non-Enterprise plans, and proxied ones are fixed at 300 (Cloudflare TTL docs). Wait at least one old TTL before the switch.
  4. Test the new host on its own hostname. Log in, submit a form, upload a file and trigger each cron route.
  5. Switch the A record (and AAAA, or remove it if your server has no IPv6) to the new server. Check with dig A example.com +short.
  6. Remove the domain from the Vercel project only after a few days of clean logs.

What's the rollback plan?

Point the DNS record back at Vercel. That works only if you keep the Vercel project deployed with its environment variables intact, and haven't moved the database yet. Write down beforehand:

  • The old DNS values (Vercel's A record or CNAME target), so going back is a paste.
  • What "broken" means: error rate, a failed checkout, a cron that didn't fire.
  • How long you'll keep Vercel live. One more month on Pro is $20 per seat plus usage. I think a one-step rollback is worth that.

Step 8: Move Postgres off Neon or Vercel Postgres

Do this last, after DNS has switched, in a short maintenance window. Move both at once and a rollback has to undo both. Move the app first and there's one database the whole time, so going back to Vercel stays a DNS change. The cost is some latency in between, so pick a server region close to your Neon region.

Neon's documented path is pg_dump -Fc over the unpooled connection string, then pg_restore into the new database (Neon pg_dump guide):

pg_dump -Fc -v -d "<neon_unpooled_connection_string>" -f app.dump
pg_restore -v -d "<new_database_connection_string>" app.dump

The same page says to "Avoid using pg_dump over a pooled connection string", and to use a client version that matches your Neon project's Postgres version. Make the new database the same major version or newer.

The window: put the app in maintenance mode (or stop writes), dump, restore, change DATABASE_URL, deploy, check, reopen. For a database of a few GB that's usually minutes. Keep the app.dump file and the Neon project until the new database has run for a while; once both databases have taken writes, rolling back means merging data. Still deciding where Postgres should live? See Postgres hosting.

Redis on Upstash is usually cache or session data. Start the new Redis empty and let it fill, unless you store something there you can't regenerate.

Do you lose the CDN when you leave Vercel?

Yes: your app is served from one city instead of Vercel's network. For most apps with users in one region, nobody notices. If your visitors are spread worldwide or your pages are mostly static, put Cloudflare's free plan in front, which includes "free SSL, CDN, DDoS protection" (Cloudflare Free).

Get the site working on HTTPS with plain DNS first, then turn the proxy on and set the SSL mode to Full (strict). That mode needs a valid, publicly trusted certificate on your server matching the hostname (Cloudflare docs), which you already have at that point. Dynamic pages send Cache-Control: private, so Cloudflare only caches what Next.js marks public.

What does the server cost after you leave?

About $7 to €8 a month for a 2 vCPU, 4 GB server, which runs a typical Next.js app plus its Postgres and Redis. For Vercel's bill at different traffic levels, see Vercel pricing.

ServerSpecsPriceSource
Netcup VPS 5002 vCPU, 4 GB RAM, 64 GB SSD, traffic included€8.26/month incl. VAT, 12-month termnetcup VPS
Hetzner CX23 (Germany, Finland only)2 vCPU, 4 GB RAM, 40 GB$6.49 + $0.60 IPv4/monthHetzner price adjustment

Netcup has a US datacenter in Manassas for about €1 more a month. Still choosing? Vercel alternatives and PaaS pricing compare the hosted options.

I've made this move from the other platforms. My first month on Laravel Cloud cost about $600, and Railway ran me $500+ a month. Today one €80 dedicated server runs everything.

Moving from Heroku or Railway instead?

The shape is the same: run the app on the new server under a temporary hostname, copy the variables, move the database, switch DNS, and keep the old platform running for a rollback. That's also the answer to "how do I move an app to another server" in general. What differs is where each platform keeps its config and data.

From Heroku to a VPS:

  • Procfile. Each line is a process type: web is the only one Heroku routes HTTP to, and worker lines are background processes (Procfile docs). On a server, each line becomes its own service running that command. A release: line, which Heroku runs "before a new release is deployed", is your migrations step, and on Ployz it becomes a pre-deploy command.
  • Buildpacks. Heroku builds without a Dockerfile, and so does Railpack, which Ployz uses for Node, Python, Ruby, PHP, Go, Rust, Java and Elixir. Most Heroku apps build unchanged.
  • Config vars. heroku config prints them all (config vars). Drop the ones add-ons set for you, like DATABASE_URL and REDIS_URL, because the new database gives you new ones.
  • Add-ons. Heroku Postgres exports with heroku pg:backups:capture then heroku pg:backups:download, and restores with pg_restore --no-acl --no-owner (import and export). Heroku says PGBackups is meant for databases up to 20 GB. Heroku Redis usually holds cache and queues you can let drain rather than copy. Replace Heroku Scheduler with a crontab or GitHub Actions.
  • Files. There usually aren't any. Dynos have an ephemeral filesystem where files "get discarded the moment the dyno stops or restarts" (dynos), so uploads already live in S3 or similar and can stay there.

From Railway to a VPS:

  • Variables. railway variable list prints a service's variables. Reference variables like ${{Postgres.DATABASE_URL}} only resolve inside Railway, so write out real values, and swap Railway's private hostnames for the new server's.
  • Volumes. Unlike Heroku, Railway services can have files on disk. Its docs list railway volume files, or sftp and scp through railway ssh on the attached service (volumes). Copy them before you switch DNS, and again after you stop writes.
  • Postgres. pg_dump over the public URL, which costs a little egress; Railway Postgres has the commands.
  • Builds. Railway builds with Railpack too, so an app that built there needs no Dockerfile on Ployz.

For what staying costs, see Heroku pricing and Railway pricing.

Where Ployz fits

Ployz covers the deploy half of this guide on the server you rent: build, deploy, previews, HTTPS and the databases. I built it to give me the Vercel workflow without the Vercel bill, and it's built for coding agents on your own servers. Paste one line into Claude Code, Codex or Cursor and it installs Ployz, adds your server and ships the repo. Deleting a service, a volume or a domain waits for you, so the agent can do the move without wiping the database you just restored.

ployz up --server root@your-server

That installs Ployz on the server over SSH, builds the current directory and gives it an https address. Connect the GitHub repo and the rest of the table above maps like this:

StepWith Ployz
Build and deployPush to GitHub; Ployz builds Next.js on your server without a Dockerfile, or uses yours. No container registry.
HTTPSEvery service gets a free https address; custom domains are free too
Preview deploymentsA preview environment for every pull request, with its own https address
Postgres / RedisOne-click Postgres and Redis on your server, with Railway-style variables like DATABASE_URL
DeploysZero-downtime: with a health check set, the new version has to pass it before the old one stops
MigrationsA pre-deploy command, like Heroku's release: line
BackupsA restore point for every database; restore it or move the database to another server
MonitoringMetrics for every service and server, searchable logs, uptime checks and email alerts

A database the preview copies belongs to it and starts empty, and a setup command like pnpm db:seed fills it, so a preview never touches production data.

Check that the Postgres you restore into has a restore point before you delete the old platform's copy. With a health check set, deploys have no downtime, so old and new versions overlap for a moment; set deploymentId as in step 2. The Free plan covers one server.

FAQ

Should I leave Vercel?

Leave if your bill tracks your traffic, you pay for several seats, or you already run Postgres, Redis or workers somewhere else. Stay if your bill is under about $40 a month, your visitors are spread around the world, or you lean on ISR across regions. I'd budget a weekend for the move.

Does Next.js work without Vercel?

Yes. ISR, image optimization, Proxy and streaming all work under next start, according to the Next.js self-hosting guide. What you lose is Vercel's own services: Cron, Blob, Analytics and the global CDN.

How long does a Vercel migration take?

Most of it is a weekend, but spread the risky parts out. Move DNS off Vercel's nameservers a week ahead, move the app, keep Vercel live for a few weeks, then move the database in a window of minutes for a few GB.

Do I have to move my database when I leave Vercel?

No. A Marketplace database lives at Neon or Upstash, so your new server can keep using it. Move it later, once the app has run on the new host for a while.

Can I migrate from Vercel to Cloudflare instead?

Yes, with the OpenNext adapter, which runs Next.js on Cloudflare Workers "using the Node.js 'runtime' from Next.js" (OpenNext Cloudflare). Cloudflare's own guide warns that "Vercel specific features are not supported by Cloudflare Workers" (Migrate from Vercel to Workers), so Cron, Blob and env vars still need steps 3 to 5. Workers bills per request and CPU time; see Cloudflare Workers pricing. A server bills a flat monthly price instead.

Do I need Docker to self-host Next.js?

No. next build and next start run on any server with Node.js. Docker helps if you want the same image everywhere, and output: 'standalone' keeps that image small (step 2).

How I checked

I checked every fact on 2026-10-01 and rechecked them on 2026-10-16 against the Next.js 16.4 docs and Vercel's, Neon's, Cloudflare's, GitHub's, Heroku's and Railway's own pages, linked next to each claim. This guide is built from those docs, not a log of a migration I ran. Server prices come from Netcup's and Hetzner's own pages. I build Ployz, so weigh the "Where Ployz fits" section with that in mind; the steps work on any server.

Move your app off Vercel to your own server with Ployz.

Run your apps and databases on your own servers with Ployz.

Deploy your app