MajorDocs Setup & Publishing Pipeline¶
A walkthrough of how MajorDocs is built, configured, and kept in sync — from writing in Obsidian to publishing via MkDocs.
Architecture Overview¶
| Component | Role |
|---|---|
| Obsidian | Writing and editing markdown notes |
| Forgejo | Self-hosted git remote at git.majorshouse.com |
| Webhook | Small Flask app, non-root, 127.0.0.1:9091 only; verifies the HMAC signature and drops a trigger file |
| Build | majwiki-build.path → majwiki-build.service (root oneshot): git pull --ff-only, one-off mkdocs build in Docker (no network), atomic symlink swap |
| Caddy | Serves the built site as static files at docs.majorshouse.com; the old notes.majorshouse.com address answers with a permanent (301) redirect to the same page, except /webhook, which both addresses forward to the local webhook |
There is no long-running wiki container. Until 2026-11-02 the site was
served by mkdocs serve, MkDocs' live-preview server, which MkDocs says is
not meant for production. See "Problems Encountered" below.
File Layout¶
/opt/majwiki/ # Git repo (content) — also holds mkdocs.yml
/opt/majwiki-config/ # Outside git
compose.yml # build-only profile, network_mode: none
Dockerfile # FROM squidfunk/mkdocs-material:<pinned>
webhook.py # signature check + trigger only
/srv/majwiki/
builds/<timestamp>-<rev> # last 3 kept
current -> builds/... # what Caddy serves (SELinux: httpd_sys_content_t)
/etc/majwiki-webhook/secret # root 600, passed to the webhook via LoadCredential=
Publishing Flow¶
- Write or edit a note in Obsidian
- Obsidian syncs the vault to Gitea via git (using Obsidian Git plugin)
- Forgejo POSTs to
https://notes.majorshouse.com/webhook; Caddy forwards it to127.0.0.1:9091 - The webhook (user
majwiki-hook, no Docker access) verifies the HMAC signature and touches a trigger file — nothing else majwiki-build.pathsees the trigger and startsmajwiki-build.service: pull, build into a newbuilds/directory, relabel, swapcurrent, prune old builds- Caddy serves the new files immediately — about 10–14 s from push to live
Key Configuration¶
mkdocs.yml (in the content repo, /opt/majwiki/mkdocs.yml)¶
mkdocs.yml lives in the content repo and is mounted read-only into the
build container. The critical setting is docs_dir: docs — this tells MkDocs
to look for markdown files in the docs/ subdirectory of its working
directory, which maps to /opt/majwiki/ via the Docker volume mount.
compose.yml volume mounts¶
volumes:
- /opt/majwiki/mkdocs.yml:/docs/mkdocs.yml:ro
- /opt/majwiki:/docs/docs:ro
- /srv/majwiki:/out
Nav tabs require index.md files¶
Each section needs an index.md to make the top nav tabs clickable.
Without it, MkDocs renders the tab as a non-linked dropdown header.
These index files must live in the git repo root, not in any Obsidian
sync subfolder.
Index files are generated — SUMMARY.md is not¶
A .githooks/pre-commit hook does two things automatically on every commit
that touches an article (any *.md that isn't README.md, index.md,
SUMMARY.md, MajorWiki-Deploy-Status.md, or a */index.md):
- Regenerates the indexes. It runs
.githooks/generate_index.pyand stages the result — the rootindex.mdand every domain index (0*/index.md). Article listings are sorted and the "recent entries" block on the home page is rebuilt from scratch. - Refuses the commit if a new article isn't in
SUMMARY.md. This check fires only for articles being added or renamed (--diff-filter=AR), and prints the offending paths.
The practical rule when adding an article:
| File | Who updates it |
|---|---|
05-troubleshooting/index.md (and any 0*/index.md) |
The hook — do not hand-edit, your entry gets overwritten |
Root index.md |
The hook |
SUMMARY.md |
You — manually, or the commit is rejected |
Hand-editing a domain index is harmless but wasted effort: the generator
overwrites the whole listing between <!-- BEGIN GENERATED INDEX --> markers.
Only SUMMARY.md needs a human.
git commit --no-verify bypasses both behaviors. Avoid it — skipping the hook
lands an article that is unreachable from the nav and absent from the indexes,
which is invisible until someone goes looking for the page.
The hook lives in
.githooks/, which git does not use by default. It works only if the repo is configured withgit config core.hooksPath .githooks. On a fresh clone, set that before the first commit or neither behavior runs.
Problems Encountered & Solutions¶
index.md files not found by MkDocs¶
The git repo at /opt/majwiki/ contains a docs/ subdirectory used by
Obsidian sync. MkDocs was seeing index files at docs/01-linux/index.md
instead of 01-linux/index.md. Fix: removed the stale copies from
/opt/majwiki/docs/ and kept the authoritative ones in the repo root.
mkdocs.yml wiped by git reset¶
Running git reset --hard origin/main on /opt/majwiki/ deleted
mkdocs.yml, compose.yml, and webhook.py since they weren't tracked
in the repo. Fix: moved all config files to /opt/majwiki-config/ outside
the git repo.
docs_dir: . not allowed¶
MkDocs refuses docs_dir: . when mkdocs.yml is in the same directory.
Fix: mount mkdocs.yml separately at /docs/mkdocs.yml and the content
at /docs/docs/ via Docker volumes, then set docs_dir: docs.
Home page wikilinks not clickable¶
The original index.md used Obsidian-style [[wikilinks]] which MkDocs
renders as plain text. Fix: rewrote index.md using standard Markdown
links.
502 from a bad SUMMARY.md (2026-03-11)¶
The site returned 502 Bad Gateway. The majwiki container was in a restart
loop because SUMMARY.md used invalid globbing syntax and the literate-nav
plugin failed with a LiterateNavParseError. Fix: rewrote SUMMARY.md with
explicit links. The same outage left majwiki-webhook.service broken; it was
repaired by killing an orphaned webhook process and correcting Caddy's
reverse-proxy routing for /webhook.
Running mkdocs serve in production¶
The original setup served the public site with mkdocs serve
--dev-addr=0.0.0.0:8092 — the live-preview server — and rebuilt by
restarting the container. It worked for months, but it is a development tool:
it listens on every address, announces itself as a Python dev server, and the
rebuild-by-restart meant the webhook (running as root) needed Docker access.
It went unnoticed because a CDN proxy in front of the site hid the server
banner; removing the proxy exposed it. Fix: mkdocs build to static files,
serve them with the reverse proxy you already run, and let the webhook do
nothing but verify and signal.
Managing the Services¶
# Rebuild now (same path a push takes)
systemctl start majwiki-build.service
journalctl -u majwiki-build.service -n 50
# Webhook
systemctl status majwiki-webhook
# Rebuild the image (e.g. after a CVE in the base)
cd /opt/majwiki-config && docker compose --profile build build --no-cache wiki
Never print the webhook secret. To give it to Forgejo, fetch it in your own
terminal (ssh majorlab sudo cat /etc/majwiki-webhook/secret), not in a
shared session or chat.
Updated 2026-03-13: Obsidian Git plugin dropped. See canonical workflow below.
Canonical Publishing Workflow¶
The Obsidian Git plugin was evaluated but dropped — too convoluted for a simple push. Manual git from the terminal is the canonical workflow.
cd ~/MajorVault # MajorRig: cd /mnt/c/Users/majorlinux/Documents/MajorVault
git add 30-Areas/MajorWiki/
git commit -m "wiki: describe your changes"
git push
From there: Forgejo receives the push → fires webhook → majorlab pulls → MkDocs rebuilds → docs.majorshouse.com updates.