fix(deploy): rework prod artifacts to match the actual Hetzner box
Server recon (read-only SSH) showed the box already runs ~8 apps behind a host-level nginx, with Gitea + an Actions runner. The host-networking design collided with contentra on port 3001. - docker-compose.prod.yml: bridge networking + per-app network, house style; api/web/postgres/redis publish to 127.0.0.1 on verified-free ports (4000/4001/5440/6390); only the generator keeps host networking (no listening port, needs the host namespace for runner-port probing). - Drop the Traefik config; the box uses a host nginx. Add a ready nginx vhost in infra/nginx/buildmymcpserver.conf (listen 80, Cloudflare TLS). - Add .gitea/workflows/deploy.yml mirroring the buildmydiscord pipeline. - Narrow the generated-MCP port range to 4400-4900 (clear of screencraft on 4321). - .env.production.example + DEPLOY.md rewritten for buildmymcpserver.com and the real topology. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a54f6218a7
commit
c7e6537c64
@ -1,5 +1,5 @@
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Production environment for buildmymcp.com
|
# Production environment for buildmymcpserver.com
|
||||||
# Copy to .env.production on the server and fill every value marked CHANGE-ME.
|
# Copy to .env.production on the server and fill every value marked CHANGE-ME.
|
||||||
# Never commit the filled file — .env.production is gitignored.
|
# Never commit the filled file — .env.production is gitignored.
|
||||||
#
|
#
|
||||||
@ -15,26 +15,30 @@ NODE_ENV=production
|
|||||||
POSTGRES_USER=bmm
|
POSTGRES_USER=bmm
|
||||||
POSTGRES_PASSWORD=CHANGE-ME-strong-db-password
|
POSTGRES_PASSWORD=CHANGE-ME-strong-db-password
|
||||||
POSTGRES_DB=bmm
|
POSTGRES_DB=bmm
|
||||||
|
|
||||||
|
# ---- Host ports (loopback only — picked free on the shared box) ----
|
||||||
POSTGRES_PORT=5440
|
POSTGRES_PORT=5440
|
||||||
|
|
||||||
# ---- Redis ----
|
|
||||||
REDIS_PORT=6390
|
REDIS_PORT=6390
|
||||||
|
API_PORT=4000
|
||||||
|
WEB_PORT=4001
|
||||||
|
|
||||||
# ---- Connection strings (host-networked services reach the DBs on loopback) ----
|
# ---- Connection strings ----
|
||||||
DATABASE_URL=postgresql://bmm:CHANGE-ME-strong-db-password@127.0.0.1:5440/bmm
|
# api + web reach the DBs over the compose network (service names).
|
||||||
REDIS_URL=redis://127.0.0.1:6390
|
# The generator overrides these to 127.0.0.1 (it uses host networking).
|
||||||
|
DATABASE_URL=postgresql://bmm:CHANGE-ME-strong-db-password@postgres:5432/bmm
|
||||||
|
REDIS_URL=redis://redis:6379
|
||||||
|
|
||||||
# ---- API ----
|
# ---- API ----
|
||||||
PORT=4000
|
PORT=4000
|
||||||
|
|
||||||
# ---- Public URLs (must match the Cloudflare DNS records) ----
|
# ---- Public URLs (must match the Cloudflare DNS records) ----
|
||||||
NEXT_PUBLIC_APP_URL=https://buildmymcp.com
|
NEXT_PUBLIC_APP_URL=https://buildmymcpserver.com
|
||||||
NEXT_PUBLIC_API_URL=https://api.buildmymcp.com
|
NEXT_PUBLIC_API_URL=https://api.buildmymcpserver.com
|
||||||
# Used to build the Google OAuth redirect URI and as the JWKS origin.
|
# Used to build the Google OAuth redirect URI and as the JWKS origin.
|
||||||
CONTROL_PLANE_PUBLIC_URL=https://api.buildmymcp.com
|
CONTROL_PLANE_PUBLIC_URL=https://api.buildmymcpserver.com
|
||||||
# Reachable by generated MCP containers — must be public so they can resolve it.
|
# Reachable by generated MCP containers — must be public so they can resolve it.
|
||||||
CONTROL_PLANE_URL=https://api.buildmymcp.com
|
CONTROL_PLANE_URL=https://api.buildmymcpserver.com
|
||||||
OAUTH_ISSUER=https://api.buildmymcp.com
|
OAUTH_ISSUER=https://api.buildmymcpserver.com
|
||||||
|
|
||||||
# ---- Crypto ----
|
# ---- Crypto ----
|
||||||
# REQUIRED in production. The API refuses to boot on the all-zero placeholder.
|
# REQUIRED in production. The API refuses to boot on the all-zero placeholder.
|
||||||
@ -52,7 +56,7 @@ ANTHROPIC_API_KEY=
|
|||||||
# ---- Google OAuth ("Continue with Google") ----
|
# ---- Google OAuth ("Continue with Google") ----
|
||||||
# Google Cloud Console -> APIs & Services -> Credentials -> OAuth client (Web).
|
# Google Cloud Console -> APIs & Services -> Credentials -> OAuth client (Web).
|
||||||
# Authorized redirect URI must be EXACTLY:
|
# Authorized redirect URI must be EXACTLY:
|
||||||
# https://api.buildmymcp.com/v1/auth/google/callback
|
# https://api.buildmymcpserver.com/v1/auth/google/callback
|
||||||
GOOGLE_OAUTH_ID=
|
GOOGLE_OAUTH_ID=
|
||||||
GOOGLE_OAUTH_SECRET=
|
GOOGLE_OAUTH_SECRET=
|
||||||
|
|
||||||
@ -62,13 +66,14 @@ OAUTH_KEY_DIR=./keys
|
|||||||
|
|
||||||
# ---- Runner / Generator ----
|
# ---- Runner / Generator ----
|
||||||
# Host used in a generated server's public URL (http://RUNNER_HOST:<port>).
|
# Host used in a generated server's public URL (http://RUNNER_HOST:<port>).
|
||||||
# Generated MCP containers bind host ports in RUNNER_PORT_RANGE_*.
|
# Generated MCP containers bind host ports in RUNNER_PORT_RANGE_* — this range
|
||||||
# NOTE: per-server subdomain routing through the proxy is not wired yet — a
|
# is kept clear of every other app already running on the box.
|
||||||
|
# NOTE: per-server subdomain routing through nginx is not wired yet — a
|
||||||
# generated server is currently reachable at the host port directly. Treat
|
# generated server is currently reachable at the host port directly. Treat
|
||||||
# public exposure of generated servers as a follow-up before GA. See DEPLOY.md.
|
# public exposure of generated servers as a follow-up before GA. See DEPLOY.md.
|
||||||
RUNNER_HOST=buildmymcp.com
|
RUNNER_HOST=buildmymcpserver.com
|
||||||
RUNNER_PORT_RANGE_START=4100
|
RUNNER_PORT_RANGE_START=4400
|
||||||
RUNNER_PORT_RANGE_END=4999
|
RUNNER_PORT_RANGE_END=4900
|
||||||
|
|
||||||
# ---- Observability (optional) ----
|
# ---- Observability (optional) ----
|
||||||
SENTRY_DSN=
|
SENTRY_DSN=
|
||||||
|
|||||||
36
.gitea/workflows/deploy.yml
Normal file
36
.gitea/workflows/deploy.yml
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
name: Deploy to Production
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: bmm-deploy
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: hetzner
|
||||||
|
steps:
|
||||||
|
- name: Pull from Gitea + rebuild containers
|
||||||
|
run: |
|
||||||
|
set -eo pipefail
|
||||||
|
: "${HOME:=/root}"
|
||||||
|
export HOME
|
||||||
|
cd /opt/buildmymcpserver
|
||||||
|
git fetch gitea main
|
||||||
|
git reset --hard gitea/main
|
||||||
|
docker compose --env-file .env.production -f docker-compose.prod.yml up -d --build
|
||||||
|
docker system prune -f
|
||||||
|
|
||||||
|
- name: Health check
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
code=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:4000/health 2>/dev/null || echo 000)
|
||||||
|
if [ "$code" = "200" ]; then echo "API healthy after $i attempts"; exit 0; fi
|
||||||
|
echo "wait $i/30 (got $code)"
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
docker logs bmm-api --tail 60 || true
|
||||||
|
exit 1
|
||||||
338
DEPLOY.md
338
DEPLOY.md
@ -1,78 +1,51 @@
|
|||||||
# Deploying the app
|
# Deploying buildmymcpserver.com
|
||||||
|
|
||||||
End-to-end runbook: domain → DNS → server → live, plus Google login.
|
End-to-end runbook for the production deploy on the shared Hetzner box.
|
||||||
|
|
||||||
This document is written to be executed in order. Steps marked **[you]** require
|
Steps marked **[you]** require logging into a third-party account (Cloudflare,
|
||||||
logging into a third-party account (Cloudflare, GoDaddy, Google) — those must be
|
GoDaddy, Google) — those must be done by a human. Steps marked **[server]** run
|
||||||
done by a human; they are not automated here. Steps marked **[server]** run on
|
on the box over SSH.
|
||||||
the host over SSH.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ⚠️ FIRST: confirm the domain name
|
## 0. The target box — what is already there
|
||||||
|
|
||||||
The request said **`buildmymcp.com`**. But the open GoDaddy tab and this
|
`213.239.213.217` — Debian 12, Docker 29 + Compose v5, 62 GB RAM, 151 GB free.
|
||||||
repository are both named **`buildmymcpserver.com`** — that is the domain you
|
It is a **shared box running ~8 other production apps** (buildmydiscord,
|
||||||
appear to actually own.
|
savesphere, ava, contentra, screencraft, helixmind, prishtina-bot, …).
|
||||||
|
|
||||||
**These are two different domains.** Decide which one before doing anything:
|
Verified house pattern — this deploy follows it exactly:
|
||||||
|
|
||||||
- If the live domain is **`buildmymcpserver.com`** — do a find-and-replace of
|
- Each app lives in `/opt/<app>` and runs via `docker compose` on a **bridge
|
||||||
`buildmymcp.com` → `buildmymcpserver.com` across this file and
|
network**, publishing ports to `127.0.0.1`.
|
||||||
`.env.production.example` before you start. `api.buildmymcp.com` becomes
|
- A **host-level nginx** owns `:80` / `:443`. Each app has a vhost in
|
||||||
`api.buildmymcpserver.com`, etc.
|
`/etc/nginx/sites-enabled/` that proxies its domain to its loopback port.
|
||||||
- If you intend to register and use the shorter **`buildmymcp.com`** — register
|
- TLS is terminated by **Cloudflare** (proxied DNS); origins serve plain HTTP.
|
||||||
it at GoDaddy first, then this file is correct as written.
|
- **Gitea** runs on the box (`gitea-gitea-1`, web on `127.0.0.1:3020`, SSH on
|
||||||
|
`:2222`) with an Actions runner labelled `hetzner`. Apps deploy via a
|
||||||
|
`.gitea/workflows/deploy.yml` that does `git fetch` + `docker compose up`.
|
||||||
|
|
||||||
Every hostname below uses `buildmymcp.com` as a placeholder. It must match the
|
**Do not** start anything that binds `:80`/`:443` — the host nginx owns them.
|
||||||
domain you put into Cloudflare in step 1.
|
|
||||||
|
|
||||||
---
|
### Ports this deploy uses (all verified free on the box)
|
||||||
|
|
||||||
## 0. What you are deploying
|
| Service | Host bind | Notes |
|
||||||
|
|-----------|----------------------|----------------------------------------|
|
||||||
Five services, defined in `docker-compose.prod.yml`:
|
| web | `127.0.0.1:4001` | nginx → buildmymcpserver.com |
|
||||||
|
| api | `127.0.0.1:4000` | nginx → api.buildmymcpserver.com |
|
||||||
| Service | Role | Network |
|
| postgres | `127.0.0.1:5440` | loopback only |
|
||||||
|-------------|---------------------------------------------------|----------------|
|
| redis | `127.0.0.1:6390` | loopback only |
|
||||||
| `postgres` | Primary database | bridge → loopback |
|
| generated | `4400–4900` | MCP runner containers (host ports) |
|
||||||
| `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. Cloudflare — create the zone **[you]**
|
||||||
|
|
||||||
1. Log in to <https://dash.cloudflare.com> (your account — do this yourself).
|
1. Log in to <https://dash.cloudflare.com>.
|
||||||
2. **Add a site** → `buildmymcp.com` → pick the **Free** plan.
|
2. **Add a site** → `buildmymcpserver.com` → **Free** plan.
|
||||||
3. Cloudflare scans existing DNS. **Before changing anything, write down every
|
3. Cloudflare scans existing DNS. **Write down every record it finds first** —
|
||||||
record it finds.** If `buildmymcp.com` currently points anywhere, those
|
anything not recreated in Cloudflare stops resolving after step 3.
|
||||||
records must be recreated here or that service goes dark.
|
4. Note the **two nameservers** Cloudflare assigns.
|
||||||
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
|
### DNS records to create in Cloudflare
|
||||||
|
|
||||||
@ -82,26 +55,18 @@ treat ports 80/443 and existing services as occupied until proven otherwise.**
|
|||||||
| A | `api` | `213.239.213.217` | Proxied (🟠) |
|
| A | `api` | `213.239.213.217` | Proxied (🟠) |
|
||||||
| A | `www` | `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
|
**SSL/TLS mode:** **Full**. (The origin serves HTTP on :80, like the other apps
|
||||||
> Traefik), set the records to **DNS only (grey cloud)** first, issue the cert,
|
on this box. Never use **Flexible**.)
|
||||||
> 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
|
## 2. ⚠️ Order of operations
|
||||||
|
|
||||||
The single way this deploy can take a site offline:
|
|
||||||
|
|
||||||
> **Recreate ALL existing DNS records in Cloudflare (step 1) BEFORE changing the
|
> **Recreate ALL existing DNS records in Cloudflare (step 1) BEFORE changing the
|
||||||
> nameservers at GoDaddy (step 3).**
|
> nameservers at GoDaddy (step 3).**
|
||||||
|
|
||||||
Once GoDaddy points at Cloudflare, Cloudflare's zone becomes authoritative.
|
Once GoDaddy points at Cloudflare, Cloudflare's zone is authoritative. Anything
|
||||||
Anything not copied into it stops resolving — email (MX), other subdomains,
|
not copied into it — MX, TXT, other subdomains — stops resolving. Copy first.
|
||||||
verification TXT records, everything. Copy first, switch second.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -109,175 +74,127 @@ verification TXT records, everything. Copy first, switch second.
|
|||||||
|
|
||||||
Only after step 1's records exist in Cloudflare:
|
Only after step 1's records exist in Cloudflare:
|
||||||
|
|
||||||
1. Log in to <https://dcc.godaddy.com> (your account — do this yourself).
|
1. Log in to <https://dcc.godaddy.com>.
|
||||||
2. Find `buildmymcp.com` → **Domain Settings** → **Nameservers** → **Change**.
|
2. `buildmymcpserver.com` → **Domain Settings → Nameservers → Change**.
|
||||||
3. Choose **Enter my own nameservers (custom)**.
|
3. **Enter my own nameservers (custom)** → the two from Cloudflare.
|
||||||
4. Replace GoDaddy's nameservers with the two from Cloudflare (step 1.4).
|
4. Save. Propagation: minutes, up to 24 h. Cloudflare shows the zone **Active**
|
||||||
5. Save. Propagation is usually minutes, up to 24 h.
|
when it has taken over.
|
||||||
6. Cloudflare's dashboard shows the zone as **Active** when it has taken over.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Server prep **[server]**
|
## 4. Deploy the stack **[server]**
|
||||||
|
|
||||||
|
The app is installed at `/opt/buildmymcpserver`. To deploy or redeploy by hand:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ssh root@213.239.213.217
|
cd /opt/buildmymcpserver
|
||||||
|
|
||||||
# Docker + compose plugin (skip any that are already present)
|
# First time only: create the env file and fill every CHANGE-ME value
|
||||||
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
|
cp .env.production.example .env.production
|
||||||
openssl rand -hex 32 # paste into SECRETS_ENCRYPTION_KEY
|
openssl rand -hex 32 # -> SECRETS_ENCRYPTION_KEY
|
||||||
nano .env.production # fill every CHANGE-ME value
|
nano .env.production
|
||||||
|
|
||||||
# 2. Build and start
|
# Build + start (this is exactly what the Gitea pipeline runs)
|
||||||
docker compose --env-file .env.production -f docker-compose.prod.yml up -d --build
|
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)
|
# First time only: create the database schema
|
||||||
docker compose --env-file .env.production -f docker-compose.prod.yml \
|
docker compose --env-file .env.production -f docker-compose.prod.yml \
|
||||||
exec api pnpm --filter @bmm/db push
|
exec -T api pnpm --filter @bmm/db push
|
||||||
|
|
||||||
# 4. Watch it come up
|
# Status / logs
|
||||||
docker compose --env-file .env.production -f docker-compose.prod.yml ps
|
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
|
docker compose --env-file .env.production -f docker-compose.prod.yml logs -f api
|
||||||
```
|
```
|
||||||
|
|
||||||
`.env.production` values that must be correct before first boot:
|
`.env.production` essentials:
|
||||||
|
|
||||||
- `SECRETS_ENCRYPTION_KEY` — real 32-byte hex. The API **refuses to boot** in
|
- `SECRETS_ENCRYPTION_KEY` — real 32-byte hex. The API **refuses to boot** in
|
||||||
production on the all-zero placeholder.
|
production on the all-zero placeholder.
|
||||||
- `DATABASE_URL` password must match `POSTGRES_PASSWORD`.
|
- `DATABASE_URL` password must equal `POSTGRES_PASSWORD`.
|
||||||
- `NEXT_PUBLIC_API_URL` is compiled into the web bundle — rebuild `web` if you
|
- `NEXT_PUBLIC_API_URL` is compiled into the web bundle — after changing it,
|
||||||
change it (`up -d --build web`).
|
rebuild web: `... 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,...}`.
|
Health check: `curl http://127.0.0.1:4000/health` → `{"ok":true,...}`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Reverse proxy + TLS **[server]**
|
## 5. nginx vhost **[server]**
|
||||||
|
|
||||||
**First: is there already a proxy?**
|
`infra/nginx/buildmymcpserver.conf` is ready. Install it on the host nginx:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ss -ltnp '( sport = :80 or sport = :443 )'
|
cp /opt/buildmymcpserver/infra/nginx/buildmymcpserver.conf \
|
||||||
|
/etc/nginx/sites-available/buildmymcpserver
|
||||||
|
ln -sf /etc/nginx/sites-available/buildmymcpserver \
|
||||||
|
/etc/nginx/sites-enabled/buildmymcpserver
|
||||||
|
nginx -t && systemctl reload nginx
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Something already listens** (the other live apps' proxy): add two vhosts to
|
`nginx -t` must pass before the reload — a reload of a bad config is rejected,
|
||||||
that proxy and **skip the Traefik option**:
|
so the other live sites are never at risk. The vhost is `listen 80` only;
|
||||||
- `buildmymcp.com`, `www.buildmymcp.com` → `http://127.0.0.1:3001`
|
Cloudflare provides TLS.
|
||||||
- `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:<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]**
|
## 6. Google login — Google Cloud Console **[you]**
|
||||||
|
|
||||||
1. Log in to <https://console.cloud.google.com> (your account — do this yourself).
|
1. Log in to <https://console.cloud.google.com>.
|
||||||
2. **Create a project** — e.g. `buildmymcp`.
|
2. **Create a project** — e.g. `buildmymcpserver`.
|
||||||
3. **APIs & Services → OAuth consent screen:**
|
3. **APIs & Services → OAuth consent screen:** External; app name
|
||||||
- User type: **External**.
|
`BuildMyMCPServer`; scopes `openid`, `userinfo.email`, `userinfo.profile`;
|
||||||
- App name `BuildMyMCP`, support email, developer contact.
|
add yourself as a test user or **Publish**.
|
||||||
- Scopes: `openid`, `.../auth/userinfo.email`, `.../auth/userinfo.profile`.
|
4. **Credentials → Create credentials → OAuth client ID → Web application.**
|
||||||
- Add yourself as a **test user**, or **Publish** the app for public sign-in.
|
**Authorized redirect URI** — exactly:
|
||||||
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
|
https://api.buildmymcpserver.com/v1/auth/google/callback
|
||||||
```
|
```
|
||||||
- (For local testing also add `http://localhost:4000/v1/auth/google/callback`.)
|
5. Put the Client ID + secret into `.env.production`:
|
||||||
5. Copy the **Client ID** and **Client secret** into `.env.production`:
|
|
||||||
```
|
```
|
||||||
GOOGLE_OAUTH_ID=...apps.googleusercontent.com
|
GOOGLE_OAUTH_ID=...apps.googleusercontent.com
|
||||||
GOOGLE_OAUTH_SECRET=...
|
GOOGLE_OAUTH_SECRET=...
|
||||||
```
|
```
|
||||||
6. Restart the API so it picks them up:
|
6. Apply: `docker compose --env-file .env.production -f docker-compose.prod.yml up -d api`.
|
||||||
|
|
||||||
|
When `GOOGLE_OAUTH_ID`/`SECRET` are set the **Continue with Google** button
|
||||||
|
appears automatically; when unset it stays hidden and magic-link login is used.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Verify live
|
||||||
|
|
||||||
|
- `https://buildmymcpserver.com` — landing page over HTTPS.
|
||||||
|
- `https://api.buildmymcpserver.com/health` — `{"ok":true,...}`.
|
||||||
|
- `/login` — magic link, plus Continue with Google once step 6 is done.
|
||||||
|
- `/admin/login` — admin via `ADMIN_EMAIL` / `ADMIN_PASSWORD`.
|
||||||
|
- Wizard → create a server → build reaches `live`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Gitea pipeline (continuous deploy)
|
||||||
|
|
||||||
|
`.gitea/workflows/deploy.yml` is in the repo and mirrors the buildmydiscord
|
||||||
|
pattern (`runs-on: hetzner`, `git fetch` + `docker compose up -d --build` +
|
||||||
|
health check). To activate it:
|
||||||
|
|
||||||
|
1. Create a repo on the box's Gitea (`https://<gitea-host>`), e.g.
|
||||||
|
`DancingTedDanson/buildmymcpserver`.
|
||||||
|
2. On the box, add it as a remote and push:
|
||||||
```bash
|
```bash
|
||||||
docker compose --env-file .env.production -f docker-compose.prod.yml up -d api
|
cd /opt/buildmymcpserver
|
||||||
|
git remote add gitea <gitea-ssh-url>
|
||||||
|
git push gitea main
|
||||||
```
|
```
|
||||||
|
3. From then on, every push to `main` rebuilds and redeploys automatically.
|
||||||
|
|
||||||
The redirect URI is derived from `CONTROL_PLANE_PUBLIC_URL`. If that is not
|
Until then, deploy by hand with the step 4 command — it is byte-identical to
|
||||||
`https://api.buildmymcp.com`, the URI registered in Google must match whatever
|
what the pipeline runs.
|
||||||
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
|
## 9. Operations
|
||||||
|
|
||||||
- `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
|
```bash
|
||||||
cd /opt/buildmymcp
|
cd /opt/buildmymcpserver
|
||||||
C="docker compose --env-file .env.production -f docker-compose.prod.yml"
|
C="docker compose --env-file .env.production -f docker-compose.prod.yml"
|
||||||
|
|
||||||
$C ps # status
|
$C ps # status
|
||||||
@ -285,26 +202,27 @@ $C logs -f generator # tail a service
|
|||||||
$C up -d --build # redeploy after a code change
|
$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 up -d --build web # rebuild only web (e.g. NEXT_PUBLIC_API_URL changed)
|
||||||
$C restart api # restart one service
|
$C restart api # restart one service
|
||||||
$C down # stop the stack (volumes/data preserved)
|
$C down # stop the stack — named volumes (data) survive
|
||||||
```
|
```
|
||||||
|
|
||||||
**Rollback:** `$C down` then redeploy the previous commit. Named volumes
|
**Rollback:** `$C down`, check out the previous commit, redeploy. Volumes
|
||||||
(`bmm_pg`, `bmm_redis`, `bmm_keys`, `bmm_build_context`) survive `down`, so data
|
`bmm_pg / bmm_redis / bmm_keys / bmm_build_context` survive `down`. `down -v`
|
||||||
and OAuth signing keys persist. `down -v` would destroy them — do not use it.
|
destroys them — never use it.
|
||||||
|
|
||||||
**Back up the database before any schema change:**
|
**Back up the DB before a schema change:**
|
||||||
|
`$C exec -T postgres pg_dump -U bmm bmm > backup-$(date +%F).sql`
|
||||||
```bash
|
|
||||||
$C exec postgres pg_dump -U bmm bmm > backup-$(date +%F).sql
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Known follow-ups (not blockers, but track them)
|
## Known follow-ups
|
||||||
|
|
||||||
1. Per-server subdomain routing (`*.mcp.buildmymcp.com`) for generated MCP
|
1. **Generated-server routing.** Generated MCP servers get a
|
||||||
servers — not yet wired (step 7).
|
`http://buildmymcpserver.com:<port>` URL on ports 4400–4900. Those ports are
|
||||||
2. Magic-link email is printed to the API log in all environments — wire a real
|
not opened on the firewall and not proxied by subdomain — wire
|
||||||
transport (Resend / SES) before relying on email sign-in in production.
|
`*.mcp.buildmymcpserver.com` through nginx before exposing generated servers
|
||||||
3. CI/CD: if a Gitea pipeline is adopted, the deploy step is exactly the step 6
|
publicly.
|
||||||
commands.
|
2. **Magic-link email** is printed to the API log, not sent. Wire a real
|
||||||
|
transport (Resend / SES) before relying on email sign-in.
|
||||||
|
3. **Cloudflare SSL** — once confirmed working on **Full**, an optional
|
||||||
|
hardening step is a Cloudflare Origin Certificate + nginx `listen 443 ssl`
|
||||||
|
for **Full (strict)**.
|
||||||
|
|||||||
@ -1,25 +1,27 @@
|
|||||||
# Production stack for buildmymcp.com — Linux host only.
|
# Production stack for buildmymcpserver.com — Linux host only.
|
||||||
#
|
#
|
||||||
# Run with:
|
# Run with:
|
||||||
# docker compose --env-file .env.production -f docker-compose.prod.yml up -d --build
|
# docker compose --env-file .env.production -f docker-compose.prod.yml up -d --build
|
||||||
#
|
#
|
||||||
# Topology notes:
|
# Topology — matches the house pattern on the shared Hetzner box:
|
||||||
# - api / web / generator use host networking. The generator allocates host
|
# - Bridge networking + per-app network, like every other app on the box.
|
||||||
# ports (4100-4999) for generated MCP containers and probes them with a local
|
# - api / web / postgres / redis publish to 127.0.0.1 only. The host nginx
|
||||||
# socket bind — that probe is only correct in the host network namespace.
|
# reverse-proxies the public domains to these loopback ports. Nothing here
|
||||||
# Host networking also keeps every service on one address space (127.0.0.1).
|
# binds 0.0.0.0:80/443 — the box's existing nginx owns those.
|
||||||
# - postgres / redis stay on the compose bridge network and publish to loopback
|
# - generator uses host networking: it has no listening port of its own (no
|
||||||
# only, so the host-networked services reach them at 127.0.0.1.
|
# collision risk) and it must allocate + probe host ports for the MCP
|
||||||
# - api and generator mount the Docker socket: the API removes containers, the
|
# containers it spawns, which is only correct in the host namespace.
|
||||||
# generator builds + runs them. Generated MCP containers are host siblings.
|
# - api + generator mount the Docker socket: the API removes generated
|
||||||
# - Nothing here binds 0.0.0.0:80/443. Front this with the box's existing
|
# containers, the generator builds + runs them as host siblings.
|
||||||
# reverse proxy, or the optional one in infra/traefik/. See DEPLOY.md.
|
#
|
||||||
|
# Ports are picked to not collide with the other apps already on this box.
|
||||||
|
|
||||||
name: buildmymcp
|
name: buildmymcpserver
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
|
container_name: bmm-postgres
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: ${POSTGRES_USER:-bmm}
|
POSTGRES_USER: ${POSTGRES_USER:-bmm}
|
||||||
@ -29,6 +31,7 @@ services:
|
|||||||
- "127.0.0.1:${POSTGRES_PORT:-5440}:5432"
|
- "127.0.0.1:${POSTGRES_PORT:-5440}:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- bmm_pg:/var/lib/postgresql/data
|
- bmm_pg:/var/lib/postgresql/data
|
||||||
|
networks: [bmm-network]
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-bmm} -d ${POSTGRES_DB:-bmm}"]
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-bmm} -d ${POSTGRES_DB:-bmm}"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
@ -37,12 +40,14 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
|
container_name: bmm-redis
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: ["redis-server", "--appendonly", "yes"]
|
command: ["redis-server", "--appendonly", "yes"]
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${REDIS_PORT:-6390}:6379"
|
- "127.0.0.1:${REDIS_PORT:-6390}:6379"
|
||||||
volumes:
|
volumes:
|
||||||
- bmm_redis:/data
|
- bmm_redis:/data
|
||||||
|
networks: [bmm-network]
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
@ -53,28 +58,15 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: apps/api/Dockerfile
|
dockerfile: apps/api/Dockerfile
|
||||||
|
container_name: bmm-api
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
network_mode: host
|
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:${API_PORT:-4000}:4000"
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
- bmm_keys:/app/apps/api/keys
|
- bmm_keys:/app/apps/api/keys
|
||||||
depends_on:
|
networks: [bmm-network]
|
||||||
postgres:
|
|
||||||
condition: service_healthy
|
|
||||||
redis:
|
|
||||||
condition: service_healthy
|
|
||||||
|
|
||||||
generator:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: apps/generator/Dockerfile
|
|
||||||
restart: unless-stopped
|
|
||||||
network_mode: host
|
|
||||||
env_file: .env.production
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
- bmm_build_context:/app/build-context
|
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@ -87,11 +79,40 @@ services:
|
|||||||
dockerfile: apps/web/Dockerfile
|
dockerfile: apps/web/Dockerfile
|
||||||
args:
|
args:
|
||||||
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:?set NEXT_PUBLIC_API_URL in .env.production}
|
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:?set NEXT_PUBLIC_API_URL in .env.production}
|
||||||
|
container_name: bmm-web
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file: .env.production
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:${WEB_PORT:-4001}:3001"
|
||||||
|
networks: [bmm-network]
|
||||||
|
depends_on:
|
||||||
|
- api
|
||||||
|
|
||||||
|
generator:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: apps/generator/Dockerfile
|
||||||
|
container_name: bmm-generator
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
network_mode: host
|
network_mode: host
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
|
environment:
|
||||||
|
# Host networking — reach the DBs via their published loopback ports
|
||||||
|
# instead of the compose-network service names.
|
||||||
|
DATABASE_URL: postgresql://${POSTGRES_USER:-bmm}:${POSTGRES_PASSWORD}@127.0.0.1:${POSTGRES_PORT:-5440}/${POSTGRES_DB:-bmm}
|
||||||
|
REDIS_URL: redis://127.0.0.1:${REDIS_PORT:-6390}
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- bmm_build_context:/app/build-context
|
||||||
depends_on:
|
depends_on:
|
||||||
- api
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
networks:
|
||||||
|
bmm-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
bmm_pg:
|
bmm_pg:
|
||||||
|
|||||||
68
infra/nginx/buildmymcpserver.conf
Normal file
68
infra/nginx/buildmymcpserver.conf
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# nginx vhost for buildmymcpserver.com — install on the host nginx:
|
||||||
|
# scp this to /etc/nginx/sites-available/buildmymcpserver
|
||||||
|
# ln -s /etc/nginx/sites-available/buildmymcpserver /etc/nginx/sites-enabled/
|
||||||
|
# nginx -t && systemctl reload nginx
|
||||||
|
#
|
||||||
|
# TLS is terminated by Cloudflare (proxied DNS records). The origin serves
|
||||||
|
# plain HTTP on :80 — same pattern as the other Cloudflare-fronted apps here.
|
||||||
|
# Set the Cloudflare SSL/TLS mode to "Full" for this zone.
|
||||||
|
|
||||||
|
# --- Web app: buildmymcpserver.com ---
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name buildmymcpserver.com www.buildmymcpserver.com;
|
||||||
|
|
||||||
|
client_max_body_size 12M;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:4001;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
proxy_read_timeout 120s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Control plane API: api.buildmymcpserver.com ---
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name api.buildmymcpserver.com;
|
||||||
|
|
||||||
|
client_max_body_size 12M;
|
||||||
|
|
||||||
|
# Build-log WebSocket stream (/v1/builds/:id/stream) — needs the upgrade
|
||||||
|
# headers and a long read timeout; buffering off so frames are not held.
|
||||||
|
location /v1/builds/ {
|
||||||
|
proxy_pass http://127.0.0.1:4000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_cache off;
|
||||||
|
proxy_read_timeout 600s;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:4000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
proxy_read_timeout 120s;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +0,0 @@
|
|||||||
# Copy to infra/traefik/.env — used only by docker-compose.traefik.yml.
|
|
||||||
# Email Let's Encrypt uses for expiry notices.
|
|
||||||
ACME_EMAIL=marco.frangiskatos@gmail.com
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
# OPTIONAL reverse proxy — use ONLY if the server has no existing proxy.
|
|
||||||
#
|
|
||||||
# !! DANGER: this binds host ports 80 and 443. If another reverse proxy
|
|
||||||
# !! (nginx / Caddy / another Traefik) is already serving the other live apps
|
|
||||||
# !! on this box, starting this WILL conflict and can take those apps offline.
|
|
||||||
# !! Check first: sudo ss -ltnp '( sport = :80 or sport = :443 )'
|
|
||||||
# !! If something already listens there, DO NOT run this. Instead add a vhost
|
|
||||||
# !! to the existing proxy pointing at 127.0.0.1:3001 (web) and 127.0.0.1:4000
|
|
||||||
# !! (api). See DEPLOY.md.
|
|
||||||
#
|
|
||||||
# Run with:
|
|
||||||
# docker compose --env-file .env -f docker-compose.traefik.yml up -d
|
|
||||||
|
|
||||||
name: buildmymcp-traefik
|
|
||||||
|
|
||||||
services:
|
|
||||||
traefik:
|
|
||||||
image: traefik:v3.2
|
|
||||||
restart: unless-stopped
|
|
||||||
network_mode: host
|
|
||||||
command:
|
|
||||||
- --providers.file.filename=/etc/traefik/dynamic.yml
|
|
||||||
- --providers.file.watch=true
|
|
||||||
- --entrypoints.web.address=:80
|
|
||||||
- --entrypoints.websecure.address=:443
|
|
||||||
- --entrypoints.web.http.redirections.entrypoint.to=websecure
|
|
||||||
- --entrypoints.web.http.redirections.entrypoint.scheme=https
|
|
||||||
- --certificatesresolvers.le.acme.httpchallenge=true
|
|
||||||
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=web
|
|
||||||
- --certificatesresolvers.le.acme.email=${ACME_EMAIL:?set ACME_EMAIL in infra/traefik/.env}
|
|
||||||
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
|
|
||||||
volumes:
|
|
||||||
- ./dynamic.yml:/etc/traefik/dynamic.yml:ro
|
|
||||||
- bmm_letsencrypt:/letsencrypt
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
bmm_letsencrypt:
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
# Traefik file-provider routes. The app stack uses host networking, so it has
|
|
||||||
# no Docker labels for Traefik to discover — routes are declared statically here.
|
|
||||||
# Targets are loopback ports owned by docker-compose.prod.yml.
|
|
||||||
|
|
||||||
http:
|
|
||||||
routers:
|
|
||||||
bmm-web:
|
|
||||||
rule: "Host(`buildmymcp.com`) || Host(`www.buildmymcp.com`)"
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
service: bmm-web
|
|
||||||
tls:
|
|
||||||
certResolver: le
|
|
||||||
|
|
||||||
bmm-api:
|
|
||||||
rule: "Host(`api.buildmymcp.com`)"
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
service: bmm-api
|
|
||||||
tls:
|
|
||||||
certResolver: le
|
|
||||||
|
|
||||||
services:
|
|
||||||
bmm-web:
|
|
||||||
loadBalancer:
|
|
||||||
servers:
|
|
||||||
- url: "http://127.0.0.1:3001"
|
|
||||||
|
|
||||||
bmm-api:
|
|
||||||
loadBalancer:
|
|
||||||
servers:
|
|
||||||
- url: "http://127.0.0.1:4000"
|
|
||||||
Loading…
Reference in New Issue
Block a user