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