Docusaurus Docker Publishing
Use when publishing to the Dockerized Docusaurus docsite (docs, tags, git sync).
This host runs a pinned Docusaurus 3.10.2 instance in Docker: project at /root/Working/Projects/docsite, container name docsite, image docsite:latest, port 3000. Docs live in docsite/docs/ (auto sidebar; subdirectories become categories). Docker-only install is a standing user requirement. Global owner rule: every artifact generated for owner review (reports, briefs, review packets, receipts, deliverables, work records) is published to this site before it is considered delivered — drop it in the right category, rebuild, link it in the review message; the nightly docs-push cron commits and pushes automatically.
Naming convention (user-set, standing)
Reports are generated by date: one file per report named YYYY-MM-DD-slug.md in docs/01-reports/ (front matter id matches the filename). Reports carry no sidebar_position — filename sort makes them chronological, so the date is the primary axis and new reports land in their date slot automatically. tags: from docs/tags.yml are the secondary index. Do not add one tag per report — only stable cross-cutting vocabulary.
Sidebar ordering (owner rule, user-visible order is the spec)
- Top-level categories display alphabetically. Each category's
index.mdcarriessidebar_position: N= its alphabetical slot (Architecture→Catalogs→Compliance→…). To change display order, change that position — never renumber theNN-folder, which is storage-only (stable folder names) and no longer drives display order. A sidebar that reads as neither alphabetical nor numbered is the failure symptom of missing/tied category positions. - Within a category: by filename. Content docs omit
sidebar_position(Docusaurus auto-sort); dated reports therefore sort chronologically. Do not add per-report positions — they fight the date order. - Verify the rendered order, not the folder names. Read the actual menu order out of
build/docs/index.html(the SSR<nav class="…menu…">block,.menu__linkhrefs in DOM order) or the live DOM.menu__list-itemsequence, and assert top-level labels are alphabetical. Folder structure can look right on disk while the sidebar renders out of order.
Publishing a document (most common task)
- Drop the
.md/.mdxintodocsite/docs/(front matterid/titlecontrols URL slug;sidebar_positioncontrols order; follow the date naming convention above for reports). Large external reports: verify the body is MDX-safe first (grep for raw HTML elements and{{/}}expression pairs — none present means safe), then prepend the front matter with a script and assert the body SHA-256 equals the source's so nothing is mangled in transit. - Rebuild inside the running container:
cd /root/Working/Projects/docsite && docker-compose exec -T docsite npm run build
-Tis required — without it compose fails withthe input device is not a TTYand the build silently does not run.docs/is a bind mount, so no image rebuild or container restart is needed; the build takes ~10–30 s. Note the container serves the rebuiltbuild/itself, so the new doc is live after the build with no restart. - Verify:
curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/docs/<category>/<slug>→ 200, and grep the served HTML for a distinctive content fragment. Do not rely on the page's<title>— it can be empty in a shell render even when the body serves correctly; the fragment grep is the real check. - Commit + push — every doc change MUST land in the private
jknash/docsiterepo:It stagespython3 /root/.hermes/scripts/docs-sync.pydocs/+ site config, auto-writes a commit message, and pushes with the token read from~/.hermes/.envin-process (never on disk/argv); it silently no-ops when nothing changed. Verify the remote head via the GitHub Contents API with the jknash token — plaingit ls-remote403s because the global credential helper routes to the read-onlyghOAuth token; that does not mean the push failed.
To delete a doc: remove the file, rebuild, confirm the URL now 404s.
Routes and URLs
- The
/docsroot 404s until a document with idindex(docs/index.md) exists — add it so the docs section has a landing page. - Numbered-prefix category folders get their prefix stripped from routes: Docusaurus'
numberPrefixParserdropsNN-from folder names, sodocs/01-reports/foo.mdserves at/docs/reports/foo, NOT/docs/01-reports/foo. When adding or renaming a category, curl the stripped slug and link with it from the navbar and from cross-doc links — links written with the numeric prefix silently 404.
Rebuilding the image (config/Dockerfile changes)
cd /root/Working/Projects/docsite && docker-compose build && docker-compose up -d
Pitfall: this host runs docker-compose 1.29 against a newer Docker engine, and post-rebuild container recreation fails with KeyError: 'ContainerConfig' — it does not recover with docker rm -f + up -d retry loops. Working path: recreate with plain Docker, reusing the compose network and binding docs/ and src/ mounts: docker rm -f docsite; docker run -d --name docsite --network docsite_docsite_net -p 3000:3000 -v /root/Working/Projects/docsite/docs:/app/docs -v /root/Working/Projects/docsite/src:/app/src docsite:latest. Verify with a healthcheck curl, then npm run build inside the container.
Pitfall: the host's Docker default subnet pools are exhausted (every 172.x/16 and 192.168/20 default range is taken by other networks), so docker-compose up fails with all predefined address pools have been fully subnetted. The compose file must pin its own bridge network with an explicit ipam.config subnet (this project uses 10.40.0.0/24). If a new Docker app on this host hits the same error, list existing network subnets and assign a free private range the same way — never remove or repurpose other projects' networks.
Theme and custom pages
Site-wide theming lives in src/css/custom.css (dark slate base, cyan accent; the /portals page theme is hardcoded in src/pages/portals.js — to match a portals color, grep the hex in that file, not in the generator script, which contains no colors). src/ is bind-mounted, so editing CSS or pages needs only the in-container npm run build (step 2 above) — no image rebuild. Verify a color/style change by grepping the compiled bundle: grep -c <hex> build/assets/*.css (host-side build/ only appears after the in-container build writes it through the mount), then curl the served CSS through the tailnet URL.
The site is tailnet-only. From the host, http://localhost:3000 works; for live external verification use curl -sk --resolve jkdev001.tail6817df.ts.net:8445:100.70.216.68 https://jkdev001.tail6817df.ts.net:8445/<path> (skip cert check — tailscale-serve cert), or just confirm on localhost plus the compiled-bundle grep.
Docusaurus config rules (config validation fails the build)
- Docusaurus rejects unrecognized top-level config fields with a hard error.
onNoImagesand top-levelcolorModeare NOT valid site config fields (color mode lives inthemeConfig.colorMode). onBrokenMarkdownLinksat top level is deprecated; put it undermarkdown.hooks.onBrokenMarkdownLinks.- Keep custom pages (
src/pages/) static: importing@docusaurus/useContextor@docusaurus/corefrom a page fails the client bundle withModule not foundon this setup.@docusaurus/Linkis fine. Hard-code the title instead of reading site config. - The healthcheck in the Dockerfile must probe a URL that reliably returns 200: probe
/, not/docs/(404 when no doc sits at the docs root — a failing probe flips the container tounhealthywhile the site itself is fine).
Tags (supported out of the box, no plugin)
Docusaurus docs tags work in the pinned 3.10.2 install with zero config changes: define tag metadata in docs/tags.yml (key → label/description), then reference keys from any doc's front matter: tags: [key1, key2]. Each tag auto-generates an index page at /docs/tags/<key> listing every doc carrying it; the build emits build/docs/tags/<key>/ (inside the container only — host-side build/ does not exist, see Routes). Keep tags to genuinely reusable cross-cutting buckets the user named; per-report or one-off tags fragment the tag index into singletons — the user prefers a small, stable vocabulary.
Scaffolding a fresh instance
See references/scaffold.md for the full known-good file set (package.json with pinned 3.10.2 versions, docusaurus.config.js, sidebars.js, static index.js, multi-stage Dockerfile on node:22-alpine, docker-compose.yml with pinned subnet, .dockerignore). Copy, pin the same versions, and adjust the subnet if it collides.
Source: jknash/hermes-shared-skills · branch hermes-jkdev001 @ 1d0d545c3970 · skills/docusaurus-docker-publishing/ · view source · Imported 2026-10-04. Supporting files (references, scripts) remain in the source repository.
Published by Muse · 2026-10-04.