# Deploying the app End-to-end runbook: domain → DNS → server → live, plus Google login. This document is written to be executed in order. Steps marked **[you]** require logging into a third-party account (Cloudflare, GoDaddy, Google) — those must be done by a human; they are not automated here. Steps marked **[server]** run on the host over SSH. --- ## ⚠️ FIRST: confirm the domain name The request said **`buildmymcp.com`**. But the open GoDaddy tab and this repository are both named **`buildmymcpserver.com`** — that is the domain you appear to actually own. **These are two different domains.** Decide which one before doing anything: - If the live domain is **`buildmymcpserver.com`** — do a find-and-replace of `buildmymcp.com` → `buildmymcpserver.com` across this file and `.env.production.example` before you start. `api.buildmymcp.com` becomes `api.buildmymcpserver.com`, etc. - If you intend to register and use the shorter **`buildmymcp.com`** — register it at GoDaddy first, then this file is correct as written. Every hostname below uses `buildmymcp.com` as a placeholder. It must match the domain you put into Cloudflare in step 1. --- ## 0. What you are deploying Five services, defined in `docker-compose.prod.yml`: | Service | Role | Network | |-------------|---------------------------------------------------|----------------| | `postgres` | Primary database | bridge → loopback | | `redis` | Queue + cache | bridge → loopback | | `api` | Fastify control plane + OAuth server (port 4000) | host | | `generator` | BullMQ worker — builds & runs generated MCP images| host | | `web` | Next.js front end (port 3001) | host | `api` and `generator` mount the Docker socket: the API removes generated containers, the generator builds and runs them as host siblings on ports 4100–4999. **The stack binds nothing on ports 80/443.** It is safe to run alongside the other apps already on the box. A reverse proxy terminates TLS in front of it (step 7). ### Server Per `~/Desktop/DayZ/server_remote/README.md`, the box is a Hetzner machine at `213.239.213.217`, Linux, root SSH. **Other production apps already run on it — treat ports 80/443 and existing services as occupied until proven otherwise.** > **Gitea / pipeline — unverified.** The request mentioned deploying "via the > Gitea pipeline like the other projects." Nothing in the repo or the DayZ > server repo confirms Gitea runs on this box, and the DayZ deploy workflow > there is a manual `scp` + `systemctl` flow with no CI. Step 6 below gives a > Gitea path **and** a plain `git`/`rsync` path — use whichever matches what is > actually installed. Confirm with `ssh root@213.239.213.217 "docker ps | grep -i gitea"`. --- ## 1. Cloudflare — create the zone **[you]** 1. Log in to (your account — do this yourself). 2. **Add a site** → `buildmymcp.com` → pick the **Free** plan. 3. Cloudflare scans existing DNS. **Before changing anything, write down every record it finds.** If `buildmymcp.com` currently points anywhere, those records must be recreated here or that service goes dark. 4. Note the **two nameservers** Cloudflare assigns (e.g. `xxx.ns.cloudflare.com`). You need them in step 3. ### DNS records to create in Cloudflare | Type | Name | Content | Proxy | |------|-------|----------------------|--------------| | A | `@` | `213.239.213.217` | Proxied (🟠) | | A | `api` | `213.239.213.217` | Proxied (🟠) | | A | `www` | `213.239.213.217` | Proxied (🟠) | > If you issue TLS certificates with Let's Encrypt HTTP-01 (step 7, optional > Traefik), set the records to **DNS only (grey cloud)** first, issue the cert, > then switch to **Proxied**. With a Cloudflare Origin Certificate this is not > needed — see step 7. **SSL/TLS mode:** set to **Full (strict)** once the origin has a real certificate. Use **Full** in the interim. Never use **Flexible**. --- ## 2. ⚠️ Order of operations — read before step 3 The single way this deploy can take a site offline: > **Recreate ALL existing DNS records in Cloudflare (step 1) BEFORE changing the > nameservers at GoDaddy (step 3).** Once GoDaddy points at Cloudflare, Cloudflare's zone becomes authoritative. Anything not copied into it stops resolving — email (MX), other subdomains, verification TXT records, everything. Copy first, switch second. --- ## 3. GoDaddy — point the domain at Cloudflare **[you]** Only after step 1's records exist in Cloudflare: 1. Log in to (your account — do this yourself). 2. Find `buildmymcp.com` → **Domain Settings** → **Nameservers** → **Change**. 3. Choose **Enter my own nameservers (custom)**. 4. Replace GoDaddy's nameservers with the two from Cloudflare (step 1.4). 5. Save. Propagation is usually minutes, up to 24 h. 6. Cloudflare's dashboard shows the zone as **Active** when it has taken over. --- ## 4. Server prep **[server]** ```bash ssh root@213.239.213.217 # Docker + compose plugin (skip any that are already present) docker --version || curl -fsSL https://get.docker.com | sh docker compose version # Confirm what already uses 80/443 — do NOT disturb it ss -ltnp '( sport = :80 or sport = :443 )' ``` Firewall: only `22`, `80`, `443` should be open to the internet. The app ports (`3001`, `4000`, `5440`, `6390`) must stay private — they are reachable only over loopback / the proxy. --- ## 5. Get the code onto the server **[server]** Pick the path that matches the box. **Option A — Gitea pipeline** (only if Gitea is actually installed): push this repo to the Gitea instance, then have its Actions runner (or your existing deploy pipeline) check out the repo to `/opt/buildmymcp` and run the commands in step 6. Mirror whatever the other projects on this box already do. **Option B — plain git / rsync** (always works): ```bash mkdir -p /opt/buildmymcp # from your workstation: rsync -az --delete --exclude node_modules --exclude .git \ ~/Desktop/buildmymcpserver.com/ root@213.239.213.217:/opt/buildmymcp/ ``` --- ## 6. Configure and start the stack **[server]** ```bash cd /opt/buildmymcp # 1. Create the production env file from the template cp .env.production.example .env.production openssl rand -hex 32 # paste into SECRETS_ENCRYPTION_KEY nano .env.production # fill every CHANGE-ME value # 2. Build and start docker compose --env-file .env.production -f docker-compose.prod.yml up -d --build # 3. Push the database schema (one-time, and after any schema change) docker compose --env-file .env.production -f docker-compose.prod.yml \ exec api pnpm --filter @bmm/db push # 4. Watch it come up docker compose --env-file .env.production -f docker-compose.prod.yml ps docker compose --env-file .env.production -f docker-compose.prod.yml logs -f api ``` `.env.production` values that must be correct before first boot: - `SECRETS_ENCRYPTION_KEY` — real 32-byte hex. The API **refuses to boot** in production on the all-zero placeholder. - `DATABASE_URL` password must match `POSTGRES_PASSWORD`. - `NEXT_PUBLIC_API_URL` is compiled into the web bundle — rebuild `web` if you change it (`up -d --build web`). - `ANTHROPIC_API_KEY` — empty runs mock generation; set it for real Claude output. Health check: `curl http://127.0.0.1:4000/health` → `{"ok":true,...}`. --- ## 7. Reverse proxy + TLS **[server]** **First: is there already a proxy?** ```bash ss -ltnp '( sport = :80 or sport = :443 )' ``` - **Something already listens** (the other live apps' proxy): add two vhosts to that proxy and **skip the Traefik option**: - `buildmymcp.com`, `www.buildmymcp.com` → `http://127.0.0.1:3001` - `api.buildmymcp.com` → `http://127.0.0.1:4000` - **Nothing listens** on 80/443: use the optional Traefik in `infra/traefik/`: ```bash cd /opt/buildmymcp/infra/traefik cp .env.example .env # set ACME_EMAIL docker compose --env-file .env -f docker-compose.traefik.yml up -d ``` `dynamic.yml` already routes the three hostnames to the loopback ports. > **Let's Encrypt + Cloudflare:** HTTP-01 issuance works most reliably while the > Cloudflare records are **DNS only (grey cloud)**. Issue the cert, confirm > HTTPS, then flip the records to **Proxied (orange)**. > Alternative: generate a **Cloudflare Origin Certificate** (15-year, in the > Cloudflare dashboard → SSL/TLS → Origin Server), drop the cert + key into the > proxy, and skip ACME entirely. Then set Cloudflare SSL to **Full (strict)**. > **Generated MCP servers** currently get a `http://RUNNER_HOST:` URL and > are not yet routed through the proxy by subdomain. Wiring `*.mcp.buildmymcp.com` > to the dynamic runner ports is a follow-up before opening generated servers to > the public internet. --- ## 8. Google login — Google Cloud Console **[you]** 1. Log in to (your account — do this yourself). 2. **Create a project** — e.g. `buildmymcp`. 3. **APIs & Services → OAuth consent screen:** - User type: **External**. - App name `BuildMyMCP`, support email, developer contact. - Scopes: `openid`, `.../auth/userinfo.email`, `.../auth/userinfo.profile`. - Add yourself as a **test user**, or **Publish** the app for public sign-in. 4. **APIs & Services → Credentials → Create credentials → OAuth client ID:** - Application type: **Web application**. - **Authorized redirect URI** — must be exact: ``` https://api.buildmymcp.com/v1/auth/google/callback ``` - (For local testing also add `http://localhost:4000/v1/auth/google/callback`.) 5. Copy the **Client ID** and **Client secret** into `.env.production`: ``` GOOGLE_OAUTH_ID=...apps.googleusercontent.com GOOGLE_OAUTH_SECRET=... ``` 6. Restart the API so it picks them up: ```bash docker compose --env-file .env.production -f docker-compose.prod.yml up -d api ``` The redirect URI is derived from `CONTROL_PLANE_PUBLIC_URL`. If that is not `https://api.buildmymcp.com`, the URI registered in Google must match whatever it is. When `GOOGLE_OAUTH_ID`/`SECRET` are set, the login page shows the **Continue with Google** button automatically; when unset it is hidden. --- ## 9. Verify live - `https://buildmymcp.com` — landing page loads over HTTPS. - `https://api.buildmymcp.com/health` — `{"ok":true,...}`. - `https://buildmymcp.com/login` — magic link **and** Continue with Google. - Sign in with Google → lands on `/dashboard`. - `https://buildmymcp.com/admin/login` — admin login with `ADMIN_EMAIL` / `ADMIN_PASSWORD`. - Create a server in the wizard → build reaches `live`. --- ## 10. Operations ```bash cd /opt/buildmymcp C="docker compose --env-file .env.production -f docker-compose.prod.yml" $C ps # status $C logs -f generator # tail a service $C up -d --build # redeploy after a code change $C up -d --build web # rebuild only web (e.g. NEXT_PUBLIC_API_URL changed) $C restart api # restart one service $C down # stop the stack (volumes/data preserved) ``` **Rollback:** `$C down` then redeploy the previous commit. Named volumes (`bmm_pg`, `bmm_redis`, `bmm_keys`, `bmm_build_context`) survive `down`, so data and OAuth signing keys persist. `down -v` would destroy them — do not use it. **Back up the database before any schema change:** ```bash $C exec postgres pg_dump -U bmm bmm > backup-$(date +%F).sql ``` --- ## Known follow-ups (not blockers, but track them) 1. Per-server subdomain routing (`*.mcp.buildmymcp.com`) for generated MCP servers — not yet wired (step 7). 2. Magic-link email is printed to the API log in all environments — wire a real transport (Resend / SES) before relying on email sign-in in production. 3. CI/CD: if a Gitea pipeline is adopted, the deploy step is exactly the step 6 commands.