📄

DevOps Guide — Docs-Site

Operating and maintaining the docs-site services

Sign in to see your personalized configuration examples Sign In

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

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
ServiceContainer namePortCompose file
VitePressregistry-docs-vitepress5173compose/docs.vitepress.yml
Next.jsregistry-docs-nextjs3000compose/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 check on the host to identify broken symlinks
  • Check podman logs registry-docs-vitepress for the specific file that failed
  • Re-run uv run scripts/docs-sync.py sync and restart the container

Next.js fails to load a guide:

  • The MDX loader in docs-site-nextjs/src/lib/mdx.ts reads from CONTENT_DIR
  • If a .mdx symlink points to a missing .md file, the route returns 404
  • Run uv run scripts/docs-sync.py check and uv 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_:

VariableDefaultPurpose
DOCS_SYNC_CONFIG_FILEdocs-sync.ymlPath to config file
DOCS_SYNC_DOCS_ROOTauto-detectedExplicit docs root path
LOG_LEVELWARNINGLogging 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

VariableDefaultPurpose
CONTENT_DIR/app/content/.service-specific/nextjsDirectory 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:

  1. Update the canonical .md file on the host.
  2. Re-run the sync tool to update .service-specific/:
    uv run scripts/docs-sync.py sync
  3. Restart the VitePress container to trigger a rebuild:
    podman restart registry-docs-vitepress
  4. 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:

  1. Check podman logs <container-name> for the root cause.
  2. For VitePress: a failing build (broken symlink, malformed Markdown) will cause the container to remain in starting state past the health check deadline.
  3. Run uv run scripts/docs-sync.py check to identify broken symlinks.
  4. 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.