# How to move a Next.js app off Vercel > Migrate a Next.js app from Vercel to your own server in 8 steps: next start on a $7 server, replace Cron, Blob and Postgres, then switch DNS. Published 2026-10-29, by Nick Potts. For agents: [llms.txt](https://ployz.dev/llms.txt) indexes the Ployz docs as Markdown. **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](https://ployz.dev/blog/vercel-pricing), 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 feature | What replaces it | Effort | |---|---|---| | Functions (Route Handlers, Server Actions, SSR) | `next start` on your server. Same code. | Low | | Edge Middleware | `proxy.ts`, which runs on Node.js by default in Next.js 16 | Low | | Edge runtime routes | Delete `export const runtime = 'edge'`; it's deprecated | Low | | ISR | Built in. One server: nothing to do. Several: a shared cache handler (Redis) | Low to medium | | Image optimization | Built into `next start`, using `sharp` | Low | | Cron Jobs | An external scheduler, GitHub Actions, or a crontab that calls your route | Medium | | Blob | Any S3-compatible bucket (Cloudflare R2, Backblaze B2, AWS S3) plus a code change | Medium to high | | Postgres (Neon via Marketplace) | Keep Neon, or `pg_dump` into your own Postgres | Medium | | KV / Redis (Upstash via Marketplace) | Keep Upstash, or run Redis next to the app | Low | | Environment variables | Export with `vercel env pull`, import on the new host | Low | | Preview deployments | A host with per-PR previews, or a staging server | Depends on host | | Web Analytics / Speed Insights | A self-hosted tool like Umami or Plausible | Low | | Global CDN | Cloudflare in front of your server, or nothing | Low | | Domains and DNS | Lower TTLs, switch records, keep the old deployment running | Low, 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. ```bash 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](https://vercel.com/docs/cron-jobs)), 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](https://vercel.com/docs/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](https://vercel.com/docs/analytics)). ## Step 2: Make the app run with next start The [Next.js self-hosting guide](https://nextjs.org/docs/app/guides/self-hosting) 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](https://nextjs.org/docs/app/api-reference/config/next-config-js/output) warn that it "does not copy the `public` or `.next/static` folders by default", so copy them yourself or your CSS and images 404: ```bash 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](https://nextjs.org/docs/app/api-reference/file-conventions/proxy)). 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](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config/runtime) 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](https://nextjs.org/docs/messages/sharp-missing-in-production) 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](https://vercel.com/docs/incremental-static-regeneration)). The [self-hosting guide](https://nextjs.org/docs/app/guides/self-hosting#multi-server-deployments) lists what to set once you run more than one copy: | Setting | Why | When | |---|---|---| | `cacheHandler` + `cacheMaxMemorySize: 0` | Shared ISR cache in Redis or S3; Next.js links a [Redis example](https://github.com/vercel/next.js/tree/canary/examples/cache-handler-redis) | 2+ instances | | `refreshTags()` in the cache handler | So `revalidateTag()` reaches every instance | 2+ instances | | `NEXT_SERVER_ACTIONS_ENCRYPTION_KEY` | Otherwise you get "Failed to find Server Action" errors | 2+ instances | | `deploymentId` | Version skew protection during rolling deploys | Any zero-downtime deploy | | `generateBuildId` | Same build ID if you build once per environment | Multi-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. | Option | Example | Catch | |---|---|---| | A crontab on the server | `*/15 * * * * curl -fsS -H "Authorization: Bearer $CRON_SECRET" https://example.com/api/cron/sync` | Free and exact, but if that machine is down, the job doesn't run | | An external scheduler | Calls a URL on a schedule | Survives server changes | | GitHub Actions `schedule` | A scheduled workflow that runs `curl` | Shortest 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](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#schedule)) | Vercel's crons always run in UTC ([Vercel Cron docs](https://vercel.com/docs/cron-jobs)). 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](https://vercel.com/docs/cli/env)). 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](https://vercel.com/docs/cli/blob) lists 1,000 blobs per page with `--cursor` for the next page, and `vercel blob get --output ` 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](https://developers.cloudflare.com/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](https://ployz.dev/blog/ephemeral-environment) 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](https://vercel.com/docs/domains/managing-dns-records)), 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](https://developers.cloudflare.com/dns/manage-dns-records/reference/ttl/)). 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](https://neon.com/docs/manage/backup-pg-dump)): ```bash pg_dump -Fc -v -d "" -f app.dump pg_restore -v -d "" 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](https://www.cloudflare.com/plans/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](https://developers.cloudflare.com/ssl/origin-configuration/ssl-modes/full-strict/)), 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](https://ployz.dev/blog/vercel-pricing). | Server | Specs | Price | Source | |---|---|---|---| | Netcup VPS 500 | 2 vCPU, 4 GB RAM, 64 GB SSD, traffic included | €8.26/month incl. VAT, 12-month term | [netcup VPS](https://www.netcup.com/en/server/vps) | | Hetzner CX23 (Germany, Finland only) | 2 vCPU, 4 GB RAM, 40 GB | $6.49 + $0.60 IPv4/month | [Hetzner price adjustment](https://docs.hetzner.com/general/infrastructure-and-availability/price-adjustment/) | Netcup has a US datacenter in Manassas for about €1 more a month. Still choosing? [Vercel alternatives](https://ployz.dev/blog/vercel-alternatives) and [PaaS pricing](https://ployz.dev/blog/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](https://devcenter.heroku.com/articles/procfile)). 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](https://devcenter.heroku.com/articles/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](https://devcenter.heroku.com/articles/heroku-postgres-import-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](https://devcenter.heroku.com/articles/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](https://docs.railway.com/reference/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](https://ployz.dev/blog/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](https://ployz.dev/blog/heroku-pricing) and [Railway pricing](https://ployz.dev/blog/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](https://ployz.dev/alternatives/self-hosted-vercel), 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. ```sh 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: | Step | With Ployz | |---|---| | Build and deploy | Push to GitHub; Ployz builds Next.js on your server without a Dockerfile, or uses yours. No container registry. | | HTTPS | Every service gets a free https address; custom domains are free too | | Preview deployments | A preview environment for every pull request, with its own https address | | Postgres / Redis | One-click Postgres and Redis on your server, with Railway-style variables like `DATABASE_URL` | | Deploys | Zero-downtime: with a health check set, the new version has to pass it before the old one stops | | Migrations | A pre-deploy command, like Heroku's `release:` line | | Backups | A restore point for every database; restore it or move the database to another server | | Monitoring | Metrics 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](https://opennext.js.org/cloudflare)). Cloudflare's own guide warns that "Vercel specific features are not supported by Cloudflare Workers" ([Migrate from Vercel to Workers](https://developers.cloudflare.com/workers/static-assets/migration-guides/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](https://ployz.dev/blog/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](https://ployz.dev/auth/github).