Skip to content

Building and publishing

The homepage and contact page use static HTML and a self-contained stylesheet. The manual uses MkDocs Material with system fonts and assets served from the same origin.

Build

From your local copy of the source in an isolated Python environment:

python -m pip install -r docs/requirements.txt
python -m pytest tests/test_public_site.py -q
python scripts/build_site.py --output build/review

Choose a fresh output directory for each build. The builder refuses to overwrite an existing directory. A successful build includes the homepage, contact page, /docs/, search assets, robots file and sitemap.

Publishing boundary

mkdocs.yml navigation is the public page allowlist. docs/hooks.py copies only those pages and the explicit generic example assets listed in scripts/public_site.py. Archives, requirements records, operational evidence, dotenv files and application source are excluded even if they exist below the documentation directory. Automatic docstring and source rendering are disabled. Every selected source and all generated output, including the search index and compressed sitemap, undergo the public-content audit. The output inventory must match the selected pages, generic assets and four pinned theme bundles exactly. Theme source maps and unused language bundles are excluded.

The audit rejects personal paths, private hosts/IPs, credential-shaped values, unknown URL hosts, unexpected binaries and symlinks. Environment credential values are checked in memory and never printed. For installation-specific names, pass a private denylist:

python scripts/build_site.py --output build/private-review --denylist "$PUBLIC_SITE_DENYLIST"

The denylist is a private operator-supplied file, one forbidden value per line; do not commit it or place it in the site output. Automated checks supplement content review; unknown secrets and obfuscated values are not universally detectable.

Browser verification

Install the test extra and its Chromium browser in an isolated environment, then run:

python scripts/check_public_site_browser.py --site build/review --evidence build/browser

This checks desktop/mobile light and dark themes, keyboard access, contact navigation, documentation search and outbound requests. It serves files through browser interception without starting an application server or contacting a running deployment.

Hosting

Serve only the successful combined output as static files. The target layout is:

/
  index.html
  contact.html
  style.css
  favicon.svg
  robots.txt
  docs/

Public destinations come from public-links.toml. Review domain/DNS ownership, TLS, redirects and the existing host configuration before switching traffic. Keep the previous static release for rollback. Website hosting requires no access to application tokens, worker processes, production storage or the operational dashboard.

Release workflow

Automated checks build and audit proposed changes without deploying them. Production deployment is operator-managed: publish only reviewed and approved revisions. Build the approved revision, and copy only its audited output into a new static release directory. Never mount the source tree as the web root.

deploy/website/compose.yml supplies two digest-pinned containers: a non-root, read-only static server on an internal Docker network, and a dedicated outbound tunnel connector. Neither container publishes host ports or uses host networking. The connector joins the private static network and a separate egress network; the website has no egress network. Configure tunnel ingress to HTTP port 8080 of the compose service named website within this private network. This service name is an example from the supplied compose file, not an address from a running installation. Neither container mounts the Docker socket, application data, or application configuration. Both have CPU, memory and process limits. The tunnel credential is supplied as a restricted file outside the public web root and is never embedded in source, environment variables or arguments.

Place the compose file and nginx.conf in the operator-selected private deployment directory. A current symlink there selects an immutable, audited release. Provision the matching tunnel's credential as tunnel.token with ownership readable by container user 65532 only (and root). Start with docker compose up -d, then inspect container health, the connector readiness endpoint and actual homepage/documentation responses. Switching the symlink alone does not retarget an existing Docker bind mount: recreate the website container through the approved deployment procedure when changing releases. Retain the previous directory for rollback. No repository secret or registrar setting is shipped.

After any authorized switch, verify homepage, documentation, assets, redirects and the absence of unintended outbound requests. DNS activation and registrar delegation are separate from a healthy container; do not report public availability from local checks.