Skip to content

Content-viewer deployment runbook

Overview

The Comrades catalogue is rendered and validated on the Mac, then served as a copy-only nginx image on Harvey. Comrades candidates and its generated HTML tree are transferred over SSH. The normal Hovercraft library is always rebuilt on Harvey from its canonical Observatory and vault checkouts; a Mac build must never replace that server-owned tree.

Containers and ports:

  • content-viewer (full Hovercraft library): port 8090, serves the immutable HTML tree baked into its image.
  • content-viewer-comrades (Comrades catalogue): port 8091, serves its image-baked HTML tree behind Basic Auth.

Fast path: content change, immutable Comrades image rebuild

This is the normal iteration for new MECW/LCW/Wellred volumes, changelogs, or other content:

cd /Users/maarten-mac/Projects/content-viewer
./scripts/deploy-comrades.sh

deploy-comrades.sh:

  1. imports candidate volumes from ~/comrades-candidates;
  2. generates collected-works indexes (including changelogs);
  3. renders and validates the Comrades HTML tree on the Mac;
  4. transfers the staged Comrades docs, the validated build/comrades-site HTML tree, and the collected-works document trees to Harvey;
  5. optionally runs sync-and-rebuild.sh on Harvey when --with-library-rebuild is requested, rebuilding the full-library image from its server-owned sources;
  6. stages the transferred HTML into a separate image context and builds the copy-only Comrades image on Harvey; no MkDocs render runs there;
  7. recreates the Comrades container and verifies both Harvey endpoints.

The full-library Docker rebuild is deliberate. It prevents a Mac checkout, whose synced content is gitignored, from replacing current Harvey content with empty essay sections or old briefings.

Full path: code/pipeline change, image rebuild

Only rebuild images when nginx config, MkDocs config, themes, or build scripts change:

cd /Users/maarten-mac/Projects/content-viewer
docker compose build content-viewer
./scripts/build-comrades.sh --image
docker compose up -d --force-recreate content-viewer
docker compose --profile comrades up -d --force-recreate content-viewer-comrades

The Comrades serve image only copies the Mac-validated HTML tree. A pipeline or theme change therefore still requires a local render/validation, followed by the same image rebuild and container recreation on Harvey.

Local review before deploying

cd /Users/maarten-mac/Projects/content-viewer
./scripts/build-comrades.sh
mkdocs build --clean

Open:

file:///Users/maarten-mac/Projects/content-viewer/build/comrades-site/index.html
file:///Users/maarten-mac/Projects/content-viewer/site/index.html

Candidate producers

  • MECW: mecw/pipeline/publish_candidates.py (gate) and mecw/pipeline/sync_for_review.py (review/deploy chain).
  • Wellred: scripts/batch/publish_comrades_candidates.py.
  • LCW: producer still to be built; the shared adapter contract is documented in mecw/pipeline/README.md.

The candidate boundary and manifest contract are normative in comrades-library.md; do not bypass the producer gate by copying files directly into the candidate root.

Remote verification

After a deploy, check the Harvey endpoints:

curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8090/
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8091/

Comrades returns 401 without credentials and 200 with the shared comrades account.

Nightly image authority

Both Compose services serve the HTML tree baked into their image; neither bind-mounts a host-generated site directory. The normal Dockerfile validates the full rendered profile during its builder stage. sync-and-rebuild.sh then force-recreates the full-library container, verifies that it is running the Compose image, checks Music/Film source counts against files inside that container, and requests the newest Observatory briefing over localhost.

deploy-comrades.sh transfers both the staged Comrades source docs and the Mac-rendered HTML tree. Harvey stages that HTML into a dedicated context and builds a copy-only nginx image; it never runs MkDocs for Comrades. If a deploy appears successful, verify the served content rather than a host output directory: curl the latest briefing path on 127.0.0.1:8090 and compare it with /volume1/data/observatory/briefing_log/.

nginx config changes need a container recreate, not a reload

Both Compose services bind-mount their nginx config as a single file:

- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro   # library
- ./nginx/comrades.conf:/etc/nginx/conf.d/default.conf:ro  # comrades

A file bind-mount resolves to an inode at container start. git pull does not edit that file in place — it writes a replacement and renames over it, which allocates a new inode. The running container keeps the old one, so the host file and the container's view diverge silently:

grep -c library_directory nginx/default.conf                              # 3
docker exec content-viewer grep -c library_directory /etc/nginx/conf.d/default.conf  # 0

nginx -s reload re-reads the stale inode, so it reports success and changes nothing. nginx -t is equally misleading: run on the host path it validates the new file, run in the container it validates the old one.

This is why the sidebar redirect was reported fixed twice without ever taking effect. The config on disk was correct both times; the container was never recreated, so it kept serving the pre-fix config.

After any change under nginx/:

docker compose up -d --force-recreate --no-build content-viewer
docker exec content-viewer grep -c <a-token-from-your-change> /etc/nginx/conf.d/default.conf

Confirm the container sees the change before believing it shipped, then test the behaviour over HTTP rather than reading the config back.

Directory URLs and the Sabnzbd redirect

MkDocs emits relative navigation links, so a directory page must be served at its canonical trailing-slash URL. Requested without the slash, the browser keeps the slash-less URL and every sidebar link resolves one level too high — at /corpus/mecw, ../podcasts/index.html becomes /podcasts/index.html, which 404s.

Both configs now redirect directory paths before serving the index, and both set absolute_redirect off. That directive is load-bearing: the containers listen on 8080 internally, so an absolute redirect would advertise the host's port 8080 — which is Sabnzbd, not this site. Keep absolute_redirect off in any new server block.

The redirect uses a regex location, which outranks prefix locations. Anything that must not be swallowed by it needs an exact match — location = /health, not location /health. Adding the regex without that change silently broke /health, which is what scripts/deploy.sh health-checks.

Verify both containers after an nginx change:

for p in 8090 8091; do
  curl -s -o /dev/null -D- "http://192.168.1.247:$p/corpus/mecw" | head -1
  curl -s -o /dev/null -w "health: %{http_code}\n" "http://192.168.1.247:$p/health"
done

Expect 301 with a relative Location: /corpus/mecw/, and health: 200.