How MEGADOC works
How MEGADOC works
This page is the single, end-to-end explanation of how the Epic on Azure MEGADOC documentation portal is built and shipped. If you have ever wondered "where does this site actually come from, and what happens when I merge a docs change?" — this is the answer, in one place.
M.E.G.A.D.O.C. stands for Masterfully Engineered Guide — All Documentation Organized & Centralized. The core idea is simple: instead of ~75 separate documentation sites scattered across the optum-tech-compute organization, one build stitches this repository plus 74 sibling repositories into a single, searchable portal at epic.optum.com.
The big picture
Content flows left-to-right through four stages: it is gathered from many repos, aggregated and built into a static site, packaged into a container image, and deployed to the runtime that serves it.
flowchart TB
subgraph sources["Content sources"]
fp["First-party docs/ in this repo"]
sm["74 git submodules<br/>each with its own docs/ and mkdocs.yml"]
end
subgraph build["Aggregate and build"]
mp["mkdocs-monorepo-plugin<br/>includes each submodule's mkdocs.yml"]
mb["mkdocs build produces site/"]
end
subgraph pkg["Package"]
pk["Packer bakes site/ into a Chainguard Nginx image"]
jf["Push image to JFrog as epic-nginx:vN"]
end
subgraph dep["Deploy"]
an["Ansible renders HCL via pb_render_hcl.yml"]
tfe["TFE triggers a run on terraform.uhg.com"]
arc["ARC runtime: Nomad, Consul, HAProxy"]
end
site(["Live site at epic.optum.com"])
fp --> mp
sm --> mp
mp --> mb --> pk --> jf --> an --> tfe --> arc --> site
The rest of this page walks each stage.
Stage 1 — Content sources
MEGADOC has two kinds of content, and understanding the split explains almost everything else.
- First-party content lives in this repository under
docs/. This is the material the platform team owns directly: onboarding, operations runbooks, standards, and this page. It is Diataxis-organized (tutorial / how-to / reference / explanation) and every page carries YAML frontmatter. - Submodule content comes from 74 sibling repositories (Ansible roles, Terraform modules, the architecture hub, the action library, and more), pinned as git submodules under
submodules/. Each submodule keeps its docs next to its code in its owndocs/folder with its ownmkdocs.yml. The submodule list is tracked in.gitmodules;submodules/itself is gitignored and populated on demand withgit submodule update.
This is the "mega" in MEGADOC: docs stay with the teams that own the code, but readers get one site.
Stage 2 — Aggregate and build
Aggregation is done by the mkdocs-monorepo-plugin. In mkdocs.yml, nav entries use !include directives that pull each submodule's own mkdocs.yml into the parent navigation, for example:
- Architecture Hub: '!include submodules/ohemr-arch-hub/mkdocs.yml'
- OHEMR Ansible Roles: '*include submodules/ohemr-ansible-role-*/mkdocs.yml'
At build time, mkdocs build resolves every include, merges the trees into one navigation, indexes the whole corpus for search, and emits a fully static site into site/. The theme is Material for MkDocs (with the local overrides/ and Mermaid rendering via pymdownx.superfences), and the site uses use_directory_urls, which is why internal links use the trailing-slash form (../guides/) rather than .md.
The result of this stage is just a folder of HTML, CSS, and JS — no server, no database.
Stage 3 — Package
The static site/ folder is baked into a container image by HashiCorp Packer (epic.pkr.hcl):
- The base image is a hardened Chainguard Nginx image, pulled from JFrog (
centraluhg.jfrog.io). - Packer copies
site/into/usr/share/nginx/htmland drops in the repo'snginx.confandmime.types. - The finished image is tagged
epic-nginx:v<run-number>(pluslatest) and pushed back to JFrog atepic-on-azure-docker-vir/epic-nginx.
After this stage the entire portal is a single, self-contained, immutable Nginx image.
Stage 4 — Deploy and serve
Deployment turns that image into a running service:
- Ansible renders HCL. The
pb_render_hcl.ymlplaybook renders the deployment configuration from Jinja templates (epic-nginx.hcl.j2) for each data center (ctc,elr). - TFE runs the config. The rendered HCL is packaged and uploaded as a Terraform Enterprise configuration version, then a run is triggered via the
terraform.uhg.comAPI. The canonical target is the Epic workspace (aide-0085665-ohemr-epic-megadoc). - ARC serves it. TFE schedules the Nginx container onto the ARC platform — Nomad for orchestration, Consul for service discovery, HAProxy for routing — bi-homed across data centers for high availability. That is what answers requests at epic.optum.com.
How a change reaches production
The two workflows that matter are docs-quality-check.yml (the PR gate) and tfe-builder.yml (the deploy). Here is the lifecycle of a single edit:
flowchart LR
a["Edit docs on a feature branch"] --> b["Open a pull request"]
b --> c["docs-quality-check.yml<br/>Stage 1 changed content<br/>Stage 2 full build<br/>Stage 3 quality gate"]
c --> d["Merge to main"]
d --> e["tfe-builder.yml runs<br/>on merge and every 30 min"]
e --> f["Build, push to JFrog,<br/>render HCL, trigger TFE"]
f --> g(["Live at epic.optum.com"])
- On the pull request,
docs-quality-check.ymlgates the change: Stage 1 validates changed files underdocs/(naming, frontmatter, single H1, fenced-code languages), Stage 2 does a full submodule checkout andmkdocs buildand fails on warnings originating in first-partydocs/, and Stage 3 is the final pass/fail summary. Super-Linter runs separately, enforced through branch protection. - After merge to
main,tfe-builder.ymlfires (viaworkflow_runon a successful quality check) and walks Stages 2–4 above. - Submodule content refreshes on a schedule.
tfe-builder.ymlalso runs every 30 minutes, and its build step runsgit submodule update --remote --recursive— so each rebuild pulls the latest commit from every submodule's tracked branch, and sibling-repo docs reach the site without a MEGADOC PR.
The moving parts
| Piece | What it is | Where it lives |
|---|---|---|
| MkDocs + Material | Static-site generator and theme that render Markdown into the portal | mkdocs.yml, overrides/ |
| mkdocs-monorepo-plugin | Merges the 74 submodule sites into one nav via !include | mkdocs.yml plugins + nav |
| Submodules | 74 sibling repos pinned under submodules/, each contributing its own docs/ | .gitmodules |
.megadoc/ fallback system | Generates placeholder docs and opens issues for submodules missing docs | .megadoc/scripts/ |
| Packer | Bakes the built site/ into an Nginx container image | epic.pkr.hcl |
| JFrog Artifactory | Registry the image is pushed to and pulled from | centraluhg.jfrog.io |
| Ansible | Renders the TFE deployment HCL from Jinja templates | pb_render_hcl.yml, *.j2 |
| Terraform Enterprise | Runs the HCL that schedules the container | terraform.uhg.com |
| ARC runtime | Nomad + Consul + HAProxy platform that runs and fronts the container | data centers ctc, elr |
Where to go next
- Getting Started hub — the hands-on onboarding path (setup, workflows, first contribution).
- Local development setup — run the site on your workstation.
- Submodules guide and submodule adder — the aggregation mechanism in practice.
- Site design & architecture — the visual and style system (colors, typography, navigation conventions).
- Publishing process — the release and deploy details.