Navigation
Getting StartedUpdated July 3, 2026

How MEGADOC works

architecturemegadocmkdocssubmodulesdeploymentexplanation

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 own docs/ folder with its own mkdocs.yml. The submodule list is tracked in .gitmodules; submodules/ itself is gitignored and populated on demand with git 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/html and drops in the repo's nginx.conf and mime.types.
  • The finished image is tagged epic-nginx:v<run-number> (plus latest) and pushed back to JFrog at epic-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:

  1. Ansible renders HCL. The pb_render_hcl.yml playbook renders the deployment configuration from Jinja templates (epic-nginx.hcl.j2) for each data center (ctc, elr).
  2. 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.com API. The canonical target is the Epic workspace (aide-0085665-ohemr-epic-megadoc).
  3. 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.yml gates the change: Stage 1 validates changed files under docs/ (naming, frontmatter, single H1, fenced-code languages), Stage 2 does a full submodule checkout and mkdocs build and fails on warnings originating in first-party docs/, and Stage 3 is the final pass/fail summary. Super-Linter runs separately, enforced through branch protection.
  • After merge to main, tfe-builder.yml fires (via workflow_run on a successful quality check) and walks Stages 2–4 above.
  • Submodule content refreshes on a schedule. tfe-builder.yml also runs every 30 minutes, and its build step runs git 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

PieceWhat it isWhere it lives
MkDocs + MaterialStatic-site generator and theme that render Markdown into the portalmkdocs.yml, overrides/
mkdocs-monorepo-pluginMerges the 74 submodule sites into one nav via !includemkdocs.yml plugins + nav
Submodules74 sibling repos pinned under submodules/, each contributing its own docs/.gitmodules
.megadoc/ fallback systemGenerates placeholder docs and opens issues for submodules missing docs.megadoc/scripts/
PackerBakes the built site/ into an Nginx container imageepic.pkr.hcl
JFrog ArtifactoryRegistry the image is pushed to and pulled fromcentraluhg.jfrog.io
AnsibleRenders the TFE deployment HCL from Jinja templatespb_render_hcl.yml, *.j2
Terraform EnterpriseRuns the HCL that schedules the containerterraform.uhg.com
ARC runtimeNomad + Consul + HAProxy platform that runs and fronts the containerdata centers ctc, elr

Where to go next