DevOps Guide — Docs-Site
Operating and maintaining the docs-site services
DevOps Guide — Docs-Site
This guide covers the operational aspects of the docs-site: container architecture, volume mounts, startup procedures, troubleshooting, and CI integration.
Table of Contents
- Container Architecture
- Volume Mounts
- Service Startup
- VitePress Build at Startup
- Troubleshooting Containers
- docs-sync.py check in CI
- Environment Variables
- Updating Docs Without a Container Rebuild
- Adding a New Docs-Site Service
- Homepage Dashboard Monitoring
Container Architecture
Two containers serve documentation for the Registry Platform:
graph TD
subgraph host ["Host Filesystem"]
canonical["docs/docs-site/\n(canonical .md files)"]
specific[".service-specific/\n(symlinks + real files)"]
canonical --> specific
end
subgraph vp_container ["registry-docs-vitepress"]
vp_mount["/app/docs (ro)\nvol: ./docs/docs-site"]
vp_build["vitepress build\nsrcDir: docs/.service-specific/vitepress"]
vp_preview["vitepress preview\n:5173"]
vp_mount --> vp_build --> vp_preview
end
subgraph nx_container ["registry-docs-nextjs"]
nx_mount["/app/content (ro)\nvol: ./docs/docs-site"]
nx_serve["Next.js runtime\n:3000"]
nx_mount --> nx_serve
end
specific -- "volume mount" --> vp_mount
specific -- "volume mount" --> nx_mount
| Service | Container name | Port | Compose file |
|---|---|---|---|
| VitePress | registry-docs-vitepress | 5173 | compose/docs.vitepress.yml |
| Next.js | registry-docs-nextjs | 3000 | compose/docs.nextjs.yml |
Volume Mounts
Both containers receive the docs directory as a read-only volume mount:
# In compose/docs.vitepress.yml
volumes:
- ./docs/docs-site:/app/docs:ro
# In compose/docs.nextjs.yml
volumes:
- ./docs/docs-site:/app/content:ro
Why read-only? The containers never need to write to the docs directory. Making the mount read-only is a defence-in-depth measure: a container compromise or a bug in either documentation framework cannot corrupt the canonical source.
How symlinks work inside a read-only mount:
The .service-specific/ symlinks use relative paths (e.g., ../../../guides/auth.md).
Because both the symlink and its target are within the same mounted directory tree,
Node.js and VitePress can follow the symlinks successfully even though the mount is
read-only. An absolute symlink pointing outside the mount would fail.
Service Startup
# Bring up both docs services
task docs:up
# Bring up only VitePress
podman-compose -f compose/docs.vitepress.yml up -d registry-docs-vitepress
# Bring up only Next.js
podman-compose -f compose/docs.nextjs.yml up -d registry-docs-nextjs
# Tear down
task docs:down
The task docs:up target is defined in Taskfile.yml and wraps the individual
podman-compose invocations.
VitePress Build at Startup
VitePress generates a static site and cannot serve content dynamically. The
container's entrypoint.sh runs vitepress build before starting the preview server:
sequenceDiagram
participant Docker as Container runtime
participant EP as entrypoint.sh
participant VP as vitepress CLI
participant Host as Host filesystem
Docker->>EP: container start
Host->>EP: /app/docs mounted (ro)
EP->>VP: vitepress build /app/docs/.service-specific/vitepress
Note over VP: Reads all .md files via symlinks<br/>Writes static HTML to /app/dist/
VP-->>EP: build complete
EP->>VP: vitepress preview --host 0.0.0.0 --port 5173
Note over VP: Serves /app/dist/ (no more file reads)
Consequence: a content change on the host requires a container restart to be reflected in VitePress. See Updating Docs Without a Container Rebuild.
Health check: the compose file defines a health check that polls
http://localhost:5173/. The container transitions from starting to healthy
only after the VitePress build completes and the preview server is up. This is
why the container may take 15–30 seconds to become healthy on first start.
Troubleshooting Containers
Check container status and health
podman ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
View logs
# VitePress logs (includes build output and errors)
podman logs registry-docs-vitepress
# Follow logs in real time
podman logs -f registry-docs-vitepress
# Next.js logs
podman logs registry-docs-nextjs
Common failure modes
VitePress build fails at startup:
- Usually caused by a broken symlink in
.service-specific/vitepress/ - Run
uv run scripts/docs-sync.py checkon the host to identify broken symlinks - Check
podman logs registry-docs-vitepressfor the specific file that failed - Re-run
uv run scripts/docs-sync.py syncand restart the container
Next.js fails to load a guide:
- The MDX loader in
docs-site-nextjs/src/lib/mdx.tsreads fromCONTENT_DIR - If a
.mdxsymlink points to a missing.mdfile, the route returns 404 - Run
uv run scripts/docs-sync.py checkanduv run scripts/docs-sync.py status --service nextjs
Container unhealthy after 2 minutes:
- VitePress build is taking longer than the health check timeout — check
podman logs - The volume mount may not be accessible — verify the host path exists and has content
Manual health check
curl -sf http://localhost:5173/ > /dev/null && echo "VitePress OK" || echo "VitePress DOWN"
curl -sf http://localhost:3000/ > /dev/null && echo "Next.js OK" || echo "Next.js DOWN"
docs-sync.py check in CI
The check command is designed for CI pipelines. It exits 0 on success, 1 on failure:
# In CI (e.g., GitHub Actions)
uv run scripts/docs-sync.py check
A typical CI job:
- name: Validate docs symlinks
run: |
cd /opt/services/registries
uv run scripts/docs-sync.py check
What it validates: every file in .service-specific/*/ that is a symlink
resolves to an existing file. It does not check whether the sync is up-to-date
(i.e., it will not catch a missing sync after adding a new canonical file). For
full validation, run sync --dry-run and check for non-zero created/deleted counts.
Environment Variables
docs-sync.py
The docs-sync.py script reads environment variables prefixed DOCS_SYNC_:
| Variable | Default | Purpose |
|---|---|---|
DOCS_SYNC_CONFIG_FILE | docs-sync.yml | Path to config file |
DOCS_SYNC_DOCS_ROOT | auto-detected | Explicit docs root path |
LOG_LEVEL | WARNING | Logging verbosity (DEBUG, INFO, WARNING) |
# Use a custom config file
DOCS_SYNC_CONFIG_FILE=custom-sync.yml uv run scripts/docs-sync.py sync
# Explicit docs root (useful in CI when cwd may vary)
DOCS_SYNC_DOCS_ROOT=/opt/services/registries/docs/docs-site uv run scripts/docs-sync.py check
# Debug output
LOG_LEVEL=DEBUG uv run scripts/docs-sync.py sync
Next.js container
| Variable | Default | Purpose |
|---|---|---|
CONTENT_DIR | /app/content/.service-specific/nextjs | Directory from which the MDX loader reads guides |
Configured in env/docs.nextjs.env. Changing this variable allows the Next.js
container to point at a different content root without modifying the image.
Updating Docs Without a Container Rebuild
Content changes do not require rebuilding the container image. They do require:
- Update the canonical
.mdfile on the host. - Re-run the sync tool to update
.service-specific/:uv run scripts/docs-sync.py sync - Restart the VitePress container to trigger a rebuild:
podman restart registry-docs-vitepress - Next.js picks up changes automatically on the next request (no restart needed).
flowchart TD
edit["Edit canonical .md\non host"] --> sync["uv run docs-sync.py sync"]
sync --> restart["podman restart\nregistry-docs-vitepress"]
restart --> build["VitePress build runs\ninside container"]
build --> serve["Updated content\nserved at :5173"]
sync --> nx["Next.js reads updated\nsymlink on next request\n(:3000 — no restart)"]
Adding a New Docs-Site Service
To add a third documentation renderer (e.g., MkDocs, Docusaurus):
flowchart TD
A["1. Add service entry\nto docs-sync.yml"] --> B["2. Run docs-sync.py sync\nto generate .service-specific/new-service/"]
B --> C["3. Write Dockerfile\n+ compose file"]
C --> D["4. Mount docs volume\n./docs/docs-site:/app/docs:ro"]
D --> E["5. Point service config\nat .service-specific/new-service/"]
E --> F["6. Build and start container"]
F --> G["7. Verify routes 200 OK"]
Step 1 — Add to docs-sync.yml:
services:
mkdocs:
output_dir: .service-specific/mkdocs
include:
- "**/*.md"
exclude:
- ".service-specific/**"
- "README.md"
extension_override: null
frontmatter_transforms:
remove_fields:
- title
- description
Step 2 — Generate the content root:
uv run scripts/docs-sync.py sync --service mkdocs
Step 3 — Compose file:
# compose/docs.mkdocs.yml
services:
registry-docs-mkdocs:
build: ./docs-site-mkdocs
volumes:
- ./docs/docs-site:/docs-src:ro
environment:
DOCS_DIR: /docs-src/.service-specific/mkdocs
ports:
- "8000:8000"
Step 4 — Verify:
podman-compose -f compose/docs.mkdocs.yml up -d registry-docs-mkdocs
uv run scripts/docs-sync.py check
curl -sf http://localhost:8000/ > /dev/null && echo "OK"
Homepage Dashboard Monitoring
Both docs containers are registered in the Homepage dashboard. The health status is derived from each container's health check endpoint.
If a container shows as unhealthy:
- Check
podman logs <container-name>for the root cause. - For VitePress: a failing build (broken symlink, malformed Markdown) will cause
the container to remain in
startingstate past the health check deadline. - Run
uv run scripts/docs-sync.py checkto identify broken symlinks. - Fix the issue, re-sync, and restart the container.
The Homepage widget provides a fast visual indicator that documentation is serving correctly — treat an unhealthy docs container with the same urgency as any other service health degradation.