This repo now ships a standalone renderer for its own content/ at
site/ (Fumadocs + Next.js — see site/README.md).
The infra side (container + reverse proxy) lives in the custom-domains repo,
matching its existing single-box Caddy + Docker Compose pattern exactly. That
repo is private, so the pull request that introduced it is not linkable from
here; what it added is described in full below.
docs.customdomain.ai is live: it returns 200 and serves the Fumadocs build
from site/, and app.customdomain.ai/docs* permanently redirects to it. This
doc covers what changed operationally, what has already shipped, and the two
follow-ups still open.
| Piece | Repo | Path |
|---|---|---|
| Content (MDX) | docs (this repo) |
content/ |
| Standalone renderer | docs (this repo) |
site/ |
| Production container definition | custom-domains |
infra/docker-compose.prod.yml (docs service) |
| Reverse proxy + TLS | custom-domains |
infra/Caddyfile (docs.customdomain.ai block) |
/docs in the old product app |
custom-domains |
apps/app — permanently redirects (308) to docs.customdomain.ai (see below) |
The docs service in infra/docker-compose.prod.yml builds from a sibling
checkout of this repo on the production host:
docs:
build:
context: ../../docs # sibling of the custom-domains checkout
dockerfile: site/DockerfileThis mirrors how every other service in that file is built (context: ../apps/app, context: ../services/edge, ...) from source on the host at
deploy time — nothing is pulled from a container registry, matching the
existing all-built-on-host pattern for this single-box deployment. The
difference is that this source lives in a different repo, so the host
needs a checkout of it as a sibling directory of the product checkout — i.e.
<custom-domains checkout>/../docs.
CUSTOM-DOMAIN-APP/docs is a public repo, so this is a plain
unauthenticated clone — no deploy key, no new secret, no new credential
surface:
# one-time, on the host
git clone --depth 1 https://github.com/CUSTOM-DOMAIN-APP/docs.git /home/ubuntu/docs
# before every deploy (until this is automated into deploy-host.sh)
git -C /home/ubuntu/docs fetch --depth 1 origin main
git -C /home/ubuntu/docs reset --hard origin/mainThis is intentionally not wired into infra/scripts/deploy-host.sh /
deploy.sh by this change. Those scripts drive the live production
deploy (backups, DB migrations, health-checked rollback for
control-plane/edge) and editing them was judged out of the safe, isolated
scope of adding one new container + one new vhost — a mistake there has a much
bigger blast radius than a docs container failing to update. Wiring the two
snippets above into deploy-host.sh (so the docs container always rebuilds
against the latest docs@main on every deploy) is a small, well-scoped
follow-up for a human to make deliberately, not something this change forces
through as a side effect.
Until that's wired in, the docs container simply keeps serving whatever
content/ was checked out last time someone ran the clone/pull above (or
docker compose build docs locally on the host) — it does not silently break;
it just doesn't auto-update yet.
The record below has been added and docs.customdomain.ai resolves and serves.
It is recorded here so the setup is reproducible, and because it is the one
piece that lives outside version control — in the Cloudflare dashboard for the
customdomain.ai zone:
Type: CNAME Name:
docsTarget:app.customdomain.aiProxy status: Proxied (orange cloud) — same as the existingapp,www, and apex records.
docs.customdomain.ai needs to reach the exact same origin host that already
serves app.customdomain.ai (one EC2 box running every service behind one
Caddy instance — see infra/docker-compose.prod.yml /
infra/Caddyfile). Caddy dispatches to the right container purely by the
incoming Host/SNI header (that's what the new docs.customdomain.ai { ... }
block in the Caddyfile matches on), so it does not matter how traffic
arrives at the origin, only that it arrives at the same origin app already
does.
We could not find the origin's raw IP/Elastic IP anywhere in this organization's version-controlled infrastructure to hand you a literal IP instead. Checked specifically:
terraform/edge-apexincustom-domains— this provisions a separate pair of Elastic IPs + NLB for the customer-facing edge (end-user custom domains connecting directly, bypassing Cloudflare). It is not the target used byapp/www/apex/docs, which all go through Cloudflare.- The rest of
custom-domains/terraform/**(aws-adopt,bootstrap,grafana,sentry,posthog,backups) — none of it defines acloudflare_record(no Cloudflare Terraform provider is used anywhere in the repo) or anaws_eip/aws_instancefor the primary Caddy host. infra/Caddyfile/infra/docker-compose.prod.yml— confirm the pattern (Cloudflare zone on SSL mode "Full", Caddy's internal CA on the origin) but contain no IP literals..github/workflows/deploy.ymlhas an illustrative example IP in a comment (e.g. 34.234.249.128) documenting the shape of theDEPLOY_HOSTrepo secret, not a real, current value — it is not something we can respond responsibly with as "the" target.
The customdomain.ai zone's actual DNS records are managed by hand in the
Cloudflare dashboard, outside version control (consistent with why that record had to
be added by hand — we do not have, and did not ask for, Cloudflare
credentials). CNAME-to-app.customdomain.ai sidesteps needing that IP at
all: whatever the app A/AAAA record currently points to, docs will too,
automatically, and stays correct if that origin IP ever changes.
If your Cloudflare setup for any reason cannot proxy a CNAME to another
proxied hostname in the same zone, the equivalent alternative is: open the
existing app DNS record in the dashboard, copy its A/AAAA value, and create
an identical docs A/AAAA record (Proxied) with that same value.
No further action needed. This zone runs Cloudflare SSL mode "Full"
(non-strict): Cloudflare terminates public TLS for visitors, and only requires
some certificate on the origin, which it does not validate the trust chain
of. Caddy already provisions that origin certificate automatically and
identically for every hostname listed in its config (tls internal — Caddy's
own internal CA, no ACME/Let's Encrypt call-out, no extra Cloudflare token
permissions) — see the comment at the top of infra/Caddyfile. The
docs.customdomain.ai block added in that PR uses the exact same tls internal directive as app/api/mcp, so once the CNAME above resolves,
Caddy issues that block's certificate the same automatic way it already does
for the other four hostnames. There is no public ACME certificate involved for
any of these origin hostnames today; upgrading to a real Cloudflare Origin CA
cert + "Full (strict)" is an existing, separate follow-up noted in
docker-compose.prod.yml, unrelated to this change.
apps/app redirects /docs and /docs/* to the matching path on
docs.customdomain.ai. This landed with custom-domains#78. The redirect started
temporary (307/302) on purpose, so that browsers and CDNs could not hard-cache
it before the new host was proven, and was flipped to permanent (308) once
docs.customdomain.ai had been stable: apps/app/next.config.ts now sets
permanent: true. Check it with curl -sI https://app.customdomain.ai/docs,
which returns 308 with location: https://docs.customdomain.ai/docs.
Two follow-ups are still open. Each is a deliberate human decision, and neither blocks anything:
sync-to-product.yml(this repo's workflow that rsyncscontent/intoapps/app/src/content/docs/and opens adocs-syncPR) still runs as-is. Nothing renders that synced copy any more (the redirect fires before any page match), so the workflow is redundant — but it's still harmless, and deciding whether and when to retire it is a separate call (e.g. some teams keep a synced fallback during a transition window).- A few hardcoded internal
/docslinks insideapps/app(src/components/app/Sidebar.tsx,CommandPalette.tsx,src/components/legal/Footer.tsx,src/app/sitemap.ts, and others) still point at the in-app path. They keep working (the redirect catches them), just with one extra hop. Repointing them straight athttps://docs.customdomain.aiis a cosmetic follow-up, not required for correctness.
site/openapi-v1.yaml is a vendored copy of apps/app/openapi-v1.yaml, kept
so the standalone docs site can render content/api-reference/** without
depending on the product repo at build time. Keeping the two copies in sync
going forward (e.g. a small CI check, or a follow-up decision to have one
repo fetch from the other at build time) is unsolved by this change and left
as a deliberate follow-up.