Docusaurus — Internal Documentation Site
Project: Docusaurus
Host: docusaurus / docusaurus.home.lab
IP: 10.69.20.77 (VMID 277, VLAN 20)
OS: Rocky Linux 10 (LXC, unprivileged)
Public URL: https://docs.home.helix9.org
Source repo: Docusaurus on OneDev
Overview
This documentation site itself. Docusaurus generates static HTML from Markdown source in OneDev. The output is served by a stripped-down nginx:alpine container on the docusaurus LXC. A push to the source repo triggers an OneDev webhook that rebuilds and redeploys the site within ~60 seconds.
Architecture
push
laptop ──────────────────────────────────► OneDev (10.69.20.76:6611)
│
│ webhook POST
│ http://10.69.20.77:9000/hooks/docusaurus-rebuild
│ (X-OneDev-Signature: <secret>)
▼
┌───────────────────────── docusaurus LXC ───────────────────────────┐
│ │
│ webhook (systemd, port 9000) │
│ └─► /usr/local/bin/docusaurus-rebuild │
│ ├─ git fetch / git reset --hard origin/main │
│ ├─ podman run --rm node:20-alpine │
│ │ npm ci && npm run build → /srv/docusaurus/build │
│ └─ systemctl restart docusaurus.service │
│ │
│ docusaurus.service (Quadlet, nginx:alpine, port 8080) │
│ Volume: /srv/docusaurus/build → /usr/share/nginx/html (ro) │
│ │
└────────────────────────────────────────────────────────────────────┘
▲
HTTPS │
browser ─────────────────────────► Traefik (10.69.20.40:443)
docs.home.helix9.org ↳ http://10.69.20.77:8080
Components
| Component | Where | Role |
|---|---|---|
nginx:alpine (Quadlet) | /etc/containers/systemd/docusaurus.container | Serves /srv/docusaurus/build on port 8080 |
node:20-alpine (ephemeral) | run on demand by rebuild script | Builds Docusaurus → static HTML |
webhook binary | /usr/local/bin/webhook (adnanh/webhook v2.8.2) | HTTP listener, validates token, runs script |
docusaurus-rebuild script | /usr/local/bin/docusaurus-rebuild | git fetch + build + restart |
docusaurus-webhook.service | /etc/systemd/system/ | Runs the webhook listener on :9000 |
| Hooks config | /etc/webhook/hooks.json | Trigger rule + secret + execute-command |
| Shared secret | /etc/webhook/secret (mode 0600, 64 hex chars) | Auth token, generated on first deploy |
Source layout
docs/
├── intro.md # landing (slug: /)
├── networking/ # VyOS routers
│ └── _category_.json # category index + slug
├── automation/ # Terraform + Ansible
│ └── _category_.json
└── services/ # apps on PVE02 (this file lives here)
└── _category_.json
docusaurus.config.js — site title, navbar, footer, theme, search plugin.
sidebars.js — auto-generated from filesystem; ordering via sidebar_position in frontmatter and position in _category_.json.
Adding a new doc
- Create
docs/<category>/my-page.mdwith frontmatter:---sidebar_label: "Short Label"sidebar_position: 5---# My Page TitleContent... - Optional: cross-doc links use relative paths (
./other-doc.mdor../category/doc.md). - Test locally:
cd ~/code/docusaurusnpm start # live preview at http://localhost:3000
- Commit and push:
git add .git commit -m "docs: add my page"git push
- Wait ~60 s for the webhook to rebuild. Refresh
https://docs.home.helix9.org.
Local development
cd ~/code/docusaurus
npm install # one-time
npm start # http://localhost:3000 with hot reload
npm run build # produces /build (use to verify before push)
npm run build fails on broken markdown links (config: onBrokenLinks: 'warn' — change to 'throw' to enforce in CI later).
Webhook configuration
OneDev project Docusaurus → Settings → Web Hooks:
| Field | Value |
|---|---|
| URL | http://10.69.20.77:9000/hooks/docusaurus-rebuild |
| Secret | content of /etc/webhook/secret on docusaurus LXC |
| Event Types | Code push only |
OneDev sends the secret literally in the X-OneDev-Signature header — adnanh/webhook validates with a value match (not HMAC).
Manual operations
| Task | Command (on pve02 unless noted) |
|---|---|
| Force rebuild | ansible-playbook playbooks/docusaurus.yml -e docusaurus_force_rebuild=true |
| Trigger rebuild via webhook (test) | curl -X POST -H "X-OneDev-Signature: $(ssh 10.69.20.77 cat /etc/webhook/secret)" http://10.69.20.77:9000/hooks/docusaurus-rebuild |
| Tail webhook log | ssh 10.69.20.77 'journalctl -fu docusaurus-webhook' |
| Tail nginx (docusaurus) log | ssh 10.69.20.77 'journalctl -fu docusaurus.service' |
| Restart nginx | ssh 10.69.20.77 'systemctl restart docusaurus.service' |
| Restart webhook listener | ssh 10.69.20.77 'systemctl restart docusaurus-webhook.service' |
| Inspect build output | ssh 10.69.20.77 'ls -la /srv/docusaurus/build' |
Ansible role
Source: roles/docusaurus/ in the Ansible repo. Key files:
tasks/main.yml— install git, podman pulls, deploy SSH key, clone repo, build, deploy Quadlet, install webhooktemplates/docusaurus.container.j2— nginx Quadlettemplates/docusaurus-rebuild.sh.j2— idempotent rebuild script (used by playbook and webhook)templates/hooks.json.j2— webhook config with token validationtemplates/docusaurus-webhook.service.j2— systemd unit for the listenerdefaults/main.yml— versions, ports, paths (override ininventory/host_vars/docusaurus/vars.yml)
Deploy from pve02:
cd ~/ansible
ansible-playbook playbooks/docusaurus.yml
Troubleshooting
Push doesn't trigger rebuild
ssh 10.69.20.77 'journalctl -u docusaurus-webhook --since "5 min ago"'
hook rule was not satisfied→ secret mismatch between OneDev hook config and/etc/webhook/secret. Re-paste; no trailing whitespace.- No incoming request at all → check OneDev hook → recent deliveries. Verify URL reachable:
curl -I http://10.69.20.77:9000/hooks/docusaurus-rebuild(expect 400 without payload).
Build fails
ssh 10.69.20.77 'journalctl -u docusaurus-webhook --since "10 min ago" | grep -E "(error|Error|FAIL)"'
Common: broken markdown links (with onBrokenLinks: 'throw'). Run npm run build locally to reproduce.
nginx serves stale content
ssh 10.69.20.77 'ls -la /srv/docusaurus/build/index.html' # check mtime
ssh 10.69.20.77 'systemctl restart docusaurus.service'
If mtime is recent but browser still old → hard refresh (Ctrl-Shift-R) or check Traefik cache (none configured by default).
Connection refused on git push to OneDev
ssh: connect to host onedev.home.helix9.org port 6611: Connection refused
DNS resolves OneDev hostname to Traefik IP, which only handles HTTPS (443) — not SSH (6611). Use the OneDev LXC IP directly:
git remote set-url origin ssh://git@10.69.20.76:6611/Docusaurus.git
Deploy SSH key broken
Re-generate by removing the keypair and re-running the playbook (it auto-regenerates):
ssh 10.69.20.77 'rm /root/.ssh/docusaurus_deploy*'
# from pve02
ansible-playbook playbooks/docusaurus.yml
The playbook prints the new public key. Add it to the OneDev account / project deploy keys.
Search
Local client-side search via @easyops-cn/docusaurus-search-local. Index is built at compile time and bundled with the static output — no external service, queries never leave the browser.
Related
- Ansible Setup — how the Ansible repo deploys this
- Terraform Setup — how the LXC was provisioned
- Adding a New Host — general onboarding pattern