ADR-0014: Deploy mgh-docs to Cloudflare Pages, serving at mgh-docs.pages.dev (no custom domain)¶
- Status: Accepted — implemented 2026-06-06 (Pages project mgh-docs live; tunnel CNAME for docs.marnissi-holdings.com removed; no custom-domain binding — site permanently served at mgh-docs.pages.dev)
- Date: 2026-06-06
- Deciders: cloud-architect, cloudflare-expert, oci-expert, finops-agent, secops-agent, marnissi.investments
Revision history¶
- 2026-06-06 — Original ADR proposed CF Pages + custom-domain binding to
docs.marnissi-holdings.comvia atomic swap (Apply B). Operator reviewed the livemgh-docs.pages.devURL and revised the decision: keep the pages.dev URL as the canonical docs hostname permanently. No custom domain binding. The tunnel CNAME fordocs.marnissi-holdings.comwas removed in the same revision (Tofu state-/+0 add / 1 destroy formodule.cloudflare_dns.cloudflare_dns_record.records["docs_cname"]). Apply B and Apply C are obsolete; Apply C is a no-op because the cloudflared template never carried a docs ingress rule. The Pages projectcloudflare_pages_domainresource stays gated (enable_custom_domain = false) permanently.
Context¶
mgh-docs (formerly docs/) is a MkDocs Material site — static HTML, CSS, and JS generated from Markdown source. It has no server-side compute: no API calls, no database reads, no dynamic content. Today it is deployed via Cloudflare Tunnel to an MkDocs container on the ARM A1 VPS (ADR-0007), identical in topology to the backend services. This is workload-topology mismatch: a static documentation site occupies a container slot on the VPS, consumes ARM A1 memory and CPU headroom that competes with actual server-side workloads (mgh-admin-api, future database), requires a cloudflared ingress rule in config.yml, and adds a container lifecycle (build image, push to OCIR, pull on VPS, run, health check, restart on failure) to every docs release.
ADR-0011 established the bucket-plus-Worker pattern for static frontends and explicitly rejected Cloudflare Pages for frontends because the operator's confirmed direction is to keep asset storage on OCI. Docs is a different workload from frontends and warrants a separate evaluation:
- Docs content is pure Markdown; the MkDocs build output is self-contained and carries no environment secrets.
- Docs change frequency is low compared to application frontends — typically a few PRs per sprint.
- The operator does not need per-PR preview environments for the frontends (which are tested locally). For docs, PR previews are directly useful: reviewers can read rendered docs before merge without pulling the branch locally and running
mkdocs serve. docs.marnissi-holdings.comis intentionally public (no CF Access, per ADR-0008 rationale). There is no authentication layer interaction to reason about.- Cloudflare Pages integrates natively with GitHub repositories: a push to
maintriggers a build-and-deploy webhook without CI scripting, OCI credentials, oraws s3 sync. The Pages free tier (500 builds/month, unlimited bandwidth, unlimited static requests) is more than sufficient for a documentation site.
This ADR reverses the Cloudflare Pages rejection in ADR-0011 for the docs workload specifically. The bucket-plus-Worker decision in ADR-0011 remains authoritative for mgh-website, mgh-admin-ui, and quantmods-website.
Decision¶
Deploy mgh-docs to Cloudflare Pages, binding docs.marnissi-holdings.com as the Pages Custom Domain. The Pages project pulls from the MARNISSI-GROUP-HOLDINGS/mgh-docs GitHub repository, builds on main branch push, and issues PR preview deployments at auto-generated <deployment-id>.mgh-docs.pages.dev URLs. The docs.marnissi-holdings.com hostname is served by Pages; the tunnel CNAME for that hostname is removed.
Tofu module. infra-agent creates infra/modules/cloudflare-pages-docs/ containing a cloudflare_pages_project resource wired to the mgh-docs GitHub repo and a cloudflare_pages_domain resource binding the Custom Domain docs.marnissi-holdings.com. This is a new module, not a modification of the existing cloudflare-dns or cloudflare-tunnel modules. The Pages project name is mgh-docs (stable — changing it changes all *.pages.dev preview URLs).
Build configuration. The Pages project build command is uv run mkdocs build and the output directory is site/. The build environment must have Python and uv available. As of CF Pages support, Python builds are supported via a runtime.txt or pyproject.toml presence detection. infra-agent confirms the build environment with cloudflare-expert before declaring the Pages project build configuration in the Tofu resource. If CF Pages does not support uv natively at apply time, the build command falls back to pip install mkdocs-material && mkdocs build and a requirements.txt generated from pyproject.toml is committed to the mgh-docs repo. docs-agent makes that change.
DNS cutover. The docs.marnissi-holdings.com CNAME currently points at the CF Tunnel (<tunnel-uuid>.cfargotunnel.com). When the Pages Custom Domain binding is applied, Cloudflare Pages automatically creates and manages a CNAME at docs.marnissi-holdings.com pointing at the Pages routing endpoint. The existing Tofu-managed CNAME in modules/cloudflare-dns/ for docs.marnissi-holdings.com must be removed (or marked as deleted in state) before the Pages Custom Domain resource is applied — both cannot coexist as managed resources targeting the same hostname. infra-agent handles this with a targeted tofu state rm of the CNAME record and a single tofu apply that creates the Pages project, the Pages domain binding, and leaves the CNAME record under Pages management.
Cutover sequence. The following sequence is recorded for the operator and infra-agent to execute; this ADR does not prescribe the timing:
- Create Pages project via
cloudflare_pages_projectTofu resource wired tomgh-docsrepomainbranch. Verify the auto-generatedmgh-docs.pages.devURL serves the MkDocs Material build correctly before touching DNS. - Confirm MkDocs build output is correct: check navigation, internal links, diagrams (Mermaid), and search index on the Pages-hosted preview.
- Remove the
docs.marnissi-holdings.comCNAME Tofu resource frommodules/cloudflare-dns/(tofu state rmthe record, then remove from HCL). This does not immediately affect the live CNAME — it only de-registers it from Tofu state. - Apply
cloudflare_pages_domainresource binding Pages todocs.marnissi-holdings.com. Pages issues a certificate for the hostname and creates the CNAME. DNS TTL propagation is fast (CF-managed, ~1 minute). - Remove the docs ingress rule from
roles/cloudflared/templates/config.yml.j2ininfra/(infra-agentedit). Re-apply Ansiblesite.ymlon the VPS to restartcloudflaredwith the updated config. - Stop and remove the
mgh-docscontainer on the ARM A1 VPS. Reclaim the container slot, memory allocation, and OCIR image lifecycle for other workloads. - Remove the
mgh-docsOCIR repository from Tofu (or retain it empty for potential future use — operator decision). If removed,tofu state rmthe OCIR resource before deleting from HCL.
PR previews as the docs dev environment. Per ADR-0013, mgh-docs has no docs.dev.marnissi-holdings.com hostname. PR preview deployments on *.mgh-docs.pages.dev serve this purpose. Every PR opened against the mgh-docs repo automatically gets a preview build. Reviewers access docs changes rendered in context without any local mkdocs serve step. This is a net improvement over the current VPS-only workflow where docs changes must be reviewed locally.
main-only deployment. Pages deploys from main. The mgh-docs repo uses a simple main-only branch model (no develop branch, per ADR-0015 single-env repos section). Feature branches targeting main get PR previews; merge to main triggers the prod deployment to docs.marnissi-holdings.com.
SSL and zone settings. CF terminates TLS for docs.marnissi-holdings.com as it does today. The zone-wide SSL full_strict, HSTS, and minimum TLS 1.2 settings (ADR-0006) apply to all hostnames in the zone, including those served by Pages. No zone setting change is required. Pages issues its own origin certificate to CF; the connection from CF to the Pages edge origin is CF-internal and does not traverse the public internet.
Consequences¶
-
Cost (delta vs free tier): $0 delta. CF Pages Free tier: 500 builds/month, unlimited bandwidth, unlimited static requests. A docs site pushing one to five deploys per sprint will not approach 500 builds/month. The VPS container slot and OCIR repo released by this change are a cost reduction in operational overhead, not in dollars (OCI Always Free covers both). No new paid surface.
-
Operational surface:
- New Tofu module
infra/modules/cloudflare-pages-docs/owned byinfra-agent. Two resources:cloudflare_pages_project,cloudflare_pages_domain. Module is called once fromenvs/dev/(andenvs/prod/once promoted — same module, different branch/domain inputs). - Docs deploys are triggered by GitHub push events, not CI-initiated
aws s3 syncor Docker builds. No GitHub Actions workflow is needed inmgh-docsfor deploy (build and deploy are handled by the Pages webhook). A CI workflow for lint/strict-build quality gate remains inmgh-docs(callsreusable-python-uv.ymlfromdevops/, per ADR-0016). - The
mgh-docsOCIR repository is no longer used for the docs container image.infra-agentdecides whether to delete the OCIR repo or retain it empty (operator call at cutover time). - CF Pages build logs are visible in the CF dashboard under the
mgh-docsPages project. No separate monitoring infrastructure required at launch. -
PR preview URLs are ephemeral: they are available while the PR is open and for a short retention period after merge. Reviewers should note that
*.pages.devURLs require a browser (CF WAF blockscurlUA, consistent with the behaviour documented for the website Worker in workspace CLAUDE.md). -
Security posture:
- MkDocs build output is static HTML/CSS/JS.
secops-agentchecklist for everymgh-docsPR: (a)mkdocs.ymldoes not embed secret environment variables; (b) no.envfiles are committed to the repo; (c) no API keys, tokens, or credentials appear in any Markdown source file or in the MkDocs build output (checksite/if built locally). - Pages serves world-readable content. Same public posture as the website bucket (ADR-0011). No new exposure.
- The Pages build environment has access to GitHub repository contents at build time. No OCI credentials are passed to the build; no Cloudflare account credentials are embedded in the build. The Pages project's GitHub app integration uses a scoped installation token, not the
mgh-iac-accountCF API token. - CF still applies zone-wide WAF rules, HSTS, and TLS enforcement to
docs.marnissi-holdings.comafter the Pages binding. No WAF gap introduced. secops-agentconfirms on the cutover PR: (a) old CNAME Tofu resource is removed from state before Pages domain binding is applied (no dual-ownership of the record); (b) cloudflared ingress rule for docs is removed; (c) no residual container or OCIR credential references remain in active CI workflows.-
CF Pages vendor lock-in for asset hosting: docs content is plain Markdown; the
mgh-docsrepo is the source of truth. Migrating off Pages means pointing the build command at any static host (OCI bucket, another Pages project, Netlify). The Pages Custom Domain binding in Tofu is one resource to delete and replace. Lock-in is bounded and the exit path is documented below. -
Migration path if we revisit: Re-add
docs.marnissi-holdings.comCNAME → tunnel inmodules/cloudflare-dns/, re-add ingress rule toconfig.yml.j2, redeploy the MkDocs container to the VPS (image in OCIR or rebuilt from source), delete the Pages project and domain binding in Tofu. Estimated effort: 1 hour. Fully bidirectional. No application code changes required.
Alternatives considered¶
| Option | Why rejected |
|---|---|
| Tunnel → VPS container (status quo, ADR-0007) | Correct for server-side workloads. For a static MkDocs site it wastes VPS headroom, requires container lifecycle management (OCIR push/pull, Compose service, health check), and provides no PR preview capability. The marginal complexity is not justified for content that has no server-side compute requirement. |
| OCI Object Storage bucket + CF Worker (ADR-0011 pattern) | Technically valid — same pattern as the website frontends. However, this pattern requires a CI-driven aws s3 sync deploy step and an OCI Customer Secret Key in GitHub Actions secrets. For docs (low change frequency, simple static content), the CI scripting adds complexity that the Pages webhook eliminates. PR previews require either per-PR buckets (expensive) or a separate preview Worker (more Tofu). Pages delivers both deploy simplicity and free PR previews with zero CI scripting. |
| GitHub Pages | GitHub Pages on a free private org repo requires GitHub Pro or Team plan to enable. MGH uses GitHub Free; Pages is unavailable for private repos on the Free plan. |
Self-hosted docs via mkdocs serve or a static file server (nginx) on VPS |
Replaces the container problem with a different container or process management problem. Does not eliminate the VPS operational surface; does not add PR previews. Same coupling to VPS uptime. Strictly worse than Pages for this workload. |
| Netlify / Vercel | Both support MkDocs static builds and PR previews. However, they add a third vendor to the edge layer (alongside CF and OCI) with separate account management and billing surfaces. CF Pages is already inside the CF account that MGH uses for DNS, WAF, Tunnel, and Access — no new vendor, no new credentials, no new billing dashboard. Prefer collapsing moving parts. |