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): port8090, serves the immutable HTML tree baked into its image.content-viewer-comrades(Comrades catalogue): port8091, 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:
- imports candidate volumes from
~/comrades-candidates; - generates collected-works indexes (including changelogs);
- renders and validates the Comrades HTML tree on the Mac;
- transfers the staged Comrades docs, the validated
build/comrades-siteHTML tree, and the collected-works document trees to Harvey; - optionally runs
sync-and-rebuild.shon Harvey when--with-library-rebuildis requested, rebuilding the full-library image from its server-owned sources; - stages the transferred HTML into a separate image context and builds the copy-only Comrades image on Harvey; no MkDocs render runs there;
- 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) andmecw/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.