meSEO Migration Runbook
Vercel to Self-Hosted (Gitea + Coolify)
Prepared for the meSEO team. Two-phase plan. Phase 1 stands the app up on staging and is done before the Wednesday demo. Phase 2 moves production traffic, including the tracking tag, off Vercel, and happens after the demo.

Ground Rules (read first)
? Do not touch app.meseoapp.com during Phase 1. That hostname serves the tracking tag to every client site and receives their event traffic. Phase 1 uses a separate staging hostname, app2.meseoapp.com, so no client is affected.
? Keep the database on Neon. We move the app only and point it at the same database. There is no data migration in this project.
? Secrets are not printed in this document. Copy each secret value directly from the Vercel project (Settings, Environment Variables) into Coolify. Do not paste live keys into shared docs or chat.
? Do not click any /api/admin/backfill endpoint during setup or the demo. Those write to production data. Normal dashboard browsing is read-only and safe.
? Leave the Vercel deployment intact until Phase 2 is proven. It is our instant rollback.
What We Are Building
meSEO is a Next.js app using Prisma, Neon Postgres, and Clerk auth. It runs on Vercel today. We are deploying the same app as a Docker container on our own server through Coolify, with the code hosted in Gitea. Phase 1 stands it up on staging, pointed at the real database, so it can be demoed independently of Vercel. Phase 2 moves production traffic, including the client tracking tag, off Vercel.
Phase 1  -  Stand Up meSEO on Staging (before Wednesday)
Prerequisites
? Server with Coolify running (already in place) and at least 8 GB RAM free for the build. This is a large Next build and fails on undersized boxes.
? Gitea running (already in place).
? Access to the Vercel project to copy environment variable values.
? DNS control for meseoapp.com.
Step 1  -  Push the repo to Gitea
From the local meseo clone, add Gitea as a remote and push main:
 
git remote add gitea <your-gitea-meseo-repo-url>
git push gitea main
 
Confirm in Gitea that the meseo repo shows the latest commit and that the Dockerfile is present at the repo root.
Step 2  -  Confirm the container files are in the repo
The repo needs a Dockerfile at the root and output: 'standalone' in the Next config. If they are not there yet, add the files below, set the config option, commit on main, and push to Gitea.
Dockerfile
 
FROM node:22-slim AS deps
WORKDIR /app
RUN apt-get update && apt-get install -y openssl ca-certificates && rm -rf /var/lib/apt/lists/*
COPY package.json package-lock.json ./
COPY prisma ./prisma
RUN npm ci
 
FROM node:22-slim AS build
WORKDIR /app
RUN apt-get update && apt-get install -y openssl ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npx prisma generate
RUN npm run build
 
FROM node:22-slim AS runner
WORKDIR /app
RUN apt-get update && apt-get install -y openssl ca-certificates && rm -rf /var/lib/apt/lists/*
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
COPY --from=build /app/public ./public
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/.next/static ./.next/static
COPY --from=build /app/prisma ./prisma
COPY --from=build /app/node_modules/.prisma ./node_modules/.prisma
COPY --from=build /app/node_modules/@prisma/client ./node_modules/@prisma/client
EXPOSE 3000
CMD ["node", "server.js"]
 
.dockerignore
 
node_modules
.next
.git
.env*
Dockerfile
.dockerignore
npm-debug.log
 
In the Next config, add:  output: 'standalone'
Step 3  -  Create the deploy key (Gitea authorization)
Coolify pulls the private repo using an SSH deploy key. Do not reuse the Korrith key unless its public half is already added to the meseo repo. Cleanest path:
? In Coolify, generate a new private key named meseo-deploy. Copy its public key.
? In Gitea: open the meseo repo, Settings, Deploy Keys, Add Deploy Key, paste the public key, read access is enough, save.
? Back in Coolify, select meseo-deploy.
Step 4  -  Create the application in Coolify
? New Resource, Applications, Git Based, Private Repository (with Deploy Key). Clear the search box first, or Coolify shows service templates instead of deploy options.
? Select the meseo-deploy key.
? Repository: the Gitea meseo repo. Branch: main.
? Build Pack: Dockerfile. Path: /Dockerfile.
? Port: 3000.
? Do not deploy yet. Add environment variables and the domain first.
Step 5  -  Environment variables
Add every variable from the Vercel project, copying secret values directly from Vercel. Apply these rules:
? Change for staging: set NEXT_PUBLIC_API_URL and NEXT_PUBLIC_APP_URL to https://app2.meseoapp.com.
? Leave the two tracker URLs as they are for Phase 1 (NEXT_PUBLIC_TRACKER_SCRIPT_URL, NEXT_PUBLIC_COLLECT_ENDPOINT). Do not repoint live client tracking yet.
? Every NEXT_PUBLIC_ variable is build-time. It must be set as a build variable before the image builds, or it ships blank.
? Everything else carries over unchanged, same values as Vercel, including DATABASE_URL and DIRECT_URL (same Neon endpoint).
The full variable table is at the end of this document.
Step 6  -  Domain and DNS
? Add a DNS A record: app2.meseoapp.com to the server's public IP. Keep it DNS-only (no proxy) for now so Coolify can issue its TLS certificate cleanly.
? In Coolify, set the application domain to https://app2.meseoapp.com.
Step 7  -  Provider allowlists (or login fails)
? Clerk: add app2.meseoapp.com to the allowed origins for the live instance.
? Google OAuth (Google Cloud Console): add app2's redirect URI (https://app2.meseoapp.com plus the app's Google callback path) to the authorized redirect URIs.
Step 8  -  Deploy and verify
? Trigger the deploy. Watch the build logs. The build produces .next/standalone/server.js.
? Open https://app2.meseoapp.com, sign in.
? Load Site Tag Analytics and AI Visibility. Real data should render because it is the same Neon database.
? Do not click admin backfill endpoints.
? If the dashboards render, Phase 1 is complete. This is the Wednesday demo environment.
Phase 2  -  Production Cutover (after Wednesday, not before)
Do not start this until the demo is done. Each step below carries client-visible risk.
? Cloudflare in front. Put Cloudflare in front of the production app hostname and cache the tag script at the edge, so ingestion volume is absorbed before it reaches the server.
? Repoint the tracking tag. Today NEXT_PUBLIC_TRACKER_SCRIPT_URL and NEXT_PUBLIC_COLLECT_ENDPOINT point at meseo-staging.vercel.app. That is a live dependency on Vercel. Move them to a domain we control, rebuild, and confirm clients pick up the new tag. This is the highest-risk item, do it deliberately.
? Re-point cron and background jobs. Recreate each Vercel Cron as a Coolify scheduled task hitting the same route on the same schedule, and verify each one fires. If Inngest is used, re-point its serve URL and keys.
? Move Blob storage. BLOB_READ_WRITE_TOKEN is a Vercel Blob token. Anything using it must move to a storage backend we control, or those features break after leaving Vercel.
? Confirm Redis. Check where REDIS_URL points. If it is a Vercel-linked store, move or re-point it.
? Load test. Test the tag and collect endpoints under real concurrency against the new box before cutover.
? Cut over DNS. Lower the app.meseoapp.com TTL to 60 seconds a day ahead. Cut over during a low-traffic window. Watch events land in Neon live. Keep Vercel intact as instant rollback.
? Decommission Vercel only after days of stability. Note: decommissioning does not clear the outstanding Vercel balance, that is settled separately.
Watch-Outs Specific to This App
? Tracker tag points at Vercel. The production tag currently loads from meseo-staging.vercel.app. When Vercel is paused or removed, client tracking breaks. Repointing this is the core Phase 2 task.
? Vercel Blob. The blob token only works while Vercel Blob is the store. Move upload storage in Phase 2.
? Redis host. Confirm the Redis URL points to a store the new server can reach.
? Cron. The nightly rollup and any other scheduled jobs must be recreated as Coolify scheduled tasks. They do not come across automatically.
? Build memory. The Next build needs real RAM (8 GB comfortable). Undersized servers fail the build.
? Build-time publics. All NEXT_PUBLIC_ vars must be set as build variables, or the bundle ships with blank values.
Troubleshooting Quick Reference
? Coolify cannot pull the repo: the deploy key's public half is not on the meseo repo in Gitea. Add it under repo Settings, Deploy Keys.
? Build fails, out of memory: increase server RAM or build resources.
? Login redirect error or loop: app2 not added to Clerk allowed origins, or NEXT_PUBLIC_APP_URL not set to app2 at build time.
? Google sign-in redirect_uri_mismatch: app2 redirect URI not added in Google Cloud console.
? Dashboards load but show no data: DATABASE_URL not carried over correctly. Confirm it matches Vercel exactly.
? Blank NEXT_PUBLIC values in the browser: those vars were set as runtime only. Set them as build variables and redeploy.
Do NOT Add These (Vercel-specific)
Some variables only exist because the app ran on Vercel. Do not copy these into Coolify. Some are injected by the platform automatically, others tie a feature to a Vercel-hosted service that has to move before the variable is valid on our own server.
Skip entirely (Vercel-injected, do not recreate)
? Any variable beginning with VERCEL_ (for example VERCEL_URL, VERCEL_ENV, VERCEL_REGION, VERCEL_GIT_*). Vercel sets these at runtime. They will not exist on our server, and recreating them by hand is wrong. If any code reads VERCEL_URL to build a link, that code should use NEXT_PUBLIC_APP_URL instead. Flag it if a build error points at a missing VERCEL variable.
? Any NEXT_RUNTIME or edge-runtime hints that were set for Vercel's serverless split. We run one long-lived Node server, they do not apply.
Do not carry over as-is (Vercel-hosted services, migrate first)
? BLOB_READ_WRITE_TOKEN. This is a Vercel Blob token and only works while Vercel Blob is the store. For Phase 1 staging it can stay so uploads do not error, but it is not a keep-forever value. Move the storage in Phase 2, then replace this.
? REDIS_URL and DIRECT_URL, only if the Redis or database URL points at a Vercel-provided integration (a Vercel-managed Upstash or Postgres). Our DATABASE_URL is Neon and stays. Confirm the Redis host is one our server can reach before trusting it; if it is a Vercel-linked store, it has to move or be re-pointed.
Everything not listed here carries over normally, per the table below.
Environment Variable Reference
Copy secret values directly from the Vercel project. Do not paste live secret values into this document or any shared copy.
Variable
Scope
Action
NEXT_PUBLIC_API_URL
All
CHANGE to https://app2.meseoapp.com. Build-time.
NEXT_PUBLIC_APP_URL
All
CHANGE to https://app2.meseoapp.com. Build-time.
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
All
Keep. Build-time.
NEXT_PUBLIC_GOOGLE_PAGESPEED_API_KEY
All
Keep. Build-time.
NEXT_PUBLIC_TRACKER_SCRIPT_URL
All
Keep for Phase 1. Repoint in Phase 2. Build-time.
NEXT_PUBLIC_COLLECT_ENDPOINT
All
Keep for Phase 1. Repoint in Phase 2. Build-time.
NEXT_PUBLIC_CLERK_SIGN_IN_URL
All
Keep. Build-time.
NEXT_PUBLIC_CLERK_SIGN_UP_URL
All
Keep. Build-time.
DATABASE_URL
All
Keep. Same Neon endpoint. Copy from Vercel.
DIRECT_URL
Prod/Preview
Keep. Copy from Vercel.
REDIS_URL
Prod/Preview
Keep. Confirm the host is reachable from the server (Phase 2).
CLERK_SECRET_KEY
Prod
Keep. Also add app2 to Clerk allowed origins.
OPENAI_API_KEY
Prod
Keep.
PERPLEXITY_API_KEY
Prod/Preview
Keep.
GOOGLE_AI_STUDIO_API_KEY
Prod/Preview
Keep.
BING_SEARCH_API_KEY
Prod/Preview
Keep.
BRAVE_SEARCH_API_KEY
Prod/Preview
Keep.
BROWSERLESS_API_KEY
Prod/Preview
Keep.
FIRECRAWL_API_KEY
Prod/Preview
Keep.
DATAFORSEO_LOGIN
All
Keep.
DATAFORSEO_PASSWORD
Prod
Keep.
GOOGLE_CLIENT_ID
All
Keep. Add app2 redirect URI in Google Cloud console.
GOOGLE_CLIENT_SECRET
Prod
Keep.
GOOGLE_PAGESPEED_API_KEY
Prod
Keep.
INTEGRATION_ENCRYPTION_KEY
Prod
Keep. Must match the value that encrypted stored tokens.
BLOB_READ_WRITE_TOKEN
All
Keep for now. Vercel-bound storage, move in Phase 2.
ADMIN_SECRET
Prod
Keep.
ADMIN_EMAILS
All
Keep.
CRON_SECRET
Prod
Keep.
SMTP_HOST / PORT / USER / PASS / FROM
Prod/Preview
Keep.
BYPASS_PUBLIC_AUDIT_VERIFICATION
Prod/Preview
Keep. Confirm intended value.
SITE_TAG_SIDE_EFFECTS_ENABLED
Prod/Preview
Keep. Confirm it reads true on the new box.
AI_MODEL_ID
All
Keep.
MESEO_ENV
All
Keep. Currently 'staging'; note this is the production project.
FEATURE_AI_AVATAR / FEATURE_SITE_TAG / FEATURE_BILLING
All
Keep.
DEBUG_MODE
All
Keep (false).

Sign-Off Checklist (Phase 1)
? [  ]  Repo pushed to Gitea, Dockerfile present at root
? [  ]  Deploy key added to the meseo repo in Gitea
? [  ]  Coolify app created, build pack Dockerfile, port 3000
? [  ]  All env vars entered; app2 URLs changed; NEXT_PUBLIC set as build vars
? [  ]  app2.meseoapp.com DNS record live
? [  ]  Clerk origin and Google redirect URI added for app2
? [  ]  Deploy green; dashboards render real data
? [  ]  Vercel left intact as rollback
