buildmymcpserver/DEPLOY.md
Marco Sadjadi a54f6218a7 docs(deploy): flag buildmymcp.com vs buildmymcpserver.com domain mismatch
The request said buildmymcp.com; the GoDaddy tab and the repo are named
buildmymcpserver.com. Added a top-of-file callout so the domain is resolved
before any DNS/nameserver change rather than baked in wrong.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 00:37:59 +02:00

12 KiB
Raw Blame History

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.combuildmymcpserver.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 41004999.

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 https://dash.cloudflare.com (your account — do this yourself).
  2. Add a sitebuildmymcp.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 https://dcc.godaddy.com (your account — do this yourself).
  2. Find buildmymcp.comDomain SettingsNameserversChange.
  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]

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

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]

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?

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.comhttp://127.0.0.1:3001
    • api.buildmymcp.comhttp://127.0.0.1:4000
  • Nothing listens on 80/443: use the optional Traefik in infra/traefik/:

    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:<port> 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 https://console.cloud.google.com (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:
    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

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:

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