diff --git a/hosts/piha/services.yaml b/hosts/piha/services.yaml index b5abaed..81f6b53 100644 --- a/hosts/piha/services.yaml +++ b/hosts/piha/services.yaml @@ -145,6 +145,26 @@ services: # .env (KB_DSN, LAN_BIND_IP) lives alongside the compose file; stateless, no data path config_path: services/kb-query + narty27: + role: static-html-host # single self-contained viz.html (narty 2027 / Saalbach KB export) + deployment_model: docker-compose + exposure: private # LAN only; no npm vhost, no public ingress + offline_required: false + depends_on: + local: [] + external: [] + ports: + - name: http + container_port: 80 + host_port: 8240 + protocol: tcp + runtime: + # No config and no secrets. Content is PERSONAL and deliberately outside + # the repo — it lives only in the Docker named volume + # narty27_narty27_content, refreshed from SOLARIA (services/narty27/README.md). + # No backup job, no /opt/homelab/data bind. + config_path: services/narty27 + # --- Known unmanaged containers on piha (recon B5/B6, 2026-07-27) ---------- # ~28 running containers have no entry above and are deliberately NOT being # added piecemeal — bringing them under desired state is a later stage diff --git a/services/narty27/README.md b/services/narty27/README.md new file mode 100644 index 0000000..2767e2f --- /dev/null +++ b/services/narty27/README.md @@ -0,0 +1,65 @@ +# narty27 + +Static hosting for a single self-contained `viz.html` (narty 2027 / Saalbach KB +export) on PIHA. Plain `nginx:alpine` serving one Docker named volume — no build, +no database, no dependencies. + +- URL: `http://192.168.31.5:8240/viz.html` (and `/` — same file, see below) +- Exposure: private (LAN/Tailscale only; no npm vhost, no public ingress) +- Volume: `narty27_narty27_content` → `/usr/share/nginx/html:ro` + +## Content is personal and lives outside the repo + +The visualisation is personal content. It is **never committed** — not to this +repo, not to any other. It exists in exactly two places: + +1. the source on SOLARIA (`~/narty-2027/saalbach-kb/viz.html`), and +2. the `narty27_narty27_content` Docker volume on PIHA. + +There is no backup job and no bind mount under `/opt/homelab/data/`. If the +volume is lost, re-run the update procedure below from SOLARIA. + +Two copies of the same file are stored in the volume: `viz.html` (canonical name) +and `index.html` (so the bare root `http://192.168.31.5:8240/` works without a +path). Both must be refreshed together on every update. + +## Updating the content (from SOLARIA) + +```bash +# 1. Ship the file to PIHA +scp ~/narty-2027/saalbach-kb/viz.html piha:/tmp/viz.html + +# 2. Write BOTH copies into the volume via a throwaway helper container +ssh piha 'docker run --rm \ + -v narty27_narty27_content:/content \ + -v /tmp:/src:ro \ + alpine sh -c "cp /src/viz.html /content/viz.html && cp /src/viz.html /content/index.html"' + +# 3. Drop the staging copy +ssh piha 'rm -f /tmp/viz.html' + +# 4. Verify (no container restart needed — nginx serves from disk per request) +curl -sf -o /dev/null -w '%{http_code}\n' http://192.168.31.5:8240/viz.html +curl -sf -o /dev/null -w '%{http_code}\n' http://192.168.31.5:8240/ +``` + +**Why a helper container and not `docker cp`:** the volume is mounted `:ro` into +the nginx container, and `docker cp` writes through the container's mount +namespace — it fails with `mounted volume is marked read-only`, for both running +and stopped containers (verified 2026-07-31). The helper container mounts the +same volume read-write and bypasses nginx entirely. + +## First start + +`nginx:alpine` seeds an empty named volume with its own default `index.html` and +`50x.html` on first run, so the root will show the nginx welcome page until step +2 above overwrites `index.html`. Until then `/viz.html` 404s and the container +healthcheck reports unhealthy — this is expected, not a failure. + +## Operations + +```bash +docker compose -f services/narty27/docker-compose.yml up -d # on PIHA +./healthcheck.sh # container + both paths +docker volume ls | grep narty27 # narty27_narty27_content +``` diff --git a/services/narty27/docker-compose.yml b/services/narty27/docker-compose.yml new file mode 100644 index 0000000..9dfe48f --- /dev/null +++ b/services/narty27/docker-compose.yml @@ -0,0 +1,25 @@ +services: + narty27: + image: nginx:alpine + container_name: narty27 + restart: unless-stopped + ports: + # Next free port in the PIHA 82x0 static-HTTP block + # (8210 paperless, 8220 nextcloud, 8230 kb-query). + - "8240:80" + volumes: + # Content is personal and lives ONLY in this volume — never in the repo. + # Read-only: nginx just serves it; writes go through the helper-container + # procedure in README.md (docker cp cannot write into a :ro mount). + - narty27_content:/usr/share/nginx/html:ro + # busybox wget — nginx:alpine ships no curl. Serves the personal viz.html; + # index.html is a second copy of the same file so the bare root works. + healthcheck: + test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1/viz.html"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 5s + +volumes: + narty27_content: diff --git a/services/narty27/env.example b/services/narty27/env.example new file mode 100644 index 0000000..a357632 --- /dev/null +++ b/services/narty27/env.example @@ -0,0 +1,6 @@ +# narty27 has NO configuration and NO secrets. +# +# The port bind (8240:80) is static and the content lives in the +# narty27_narty27_content Docker volume, not in env vars. This file exists only +# to keep the services// layout from CLAUDE.md complete — there is +# nothing to copy to .env. diff --git a/services/narty27/healthcheck.sh b/services/narty27/healthcheck.sh new file mode 100755 index 0000000..8e0c0f1 --- /dev/null +++ b/services/narty27/healthcheck.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# Healthcheck for narty27 (nginx:alpine serving a single self-contained viz.html) + +# Container must be running +if ! docker ps --filter "name=narty27" --filter "status=running" | grep -qw "narty27"; then + echo "[FAIL] narty27 container is not running" + exit 1 +fi + +# Both copies must answer: viz.html is the canonical name, index.html makes the +# bare root work. An empty volume means the content was never loaded — see README. +for path in viz.html ""; do + if ! curl -sf -o /dev/null "http://127.0.0.1:8240/${path}"; then + echo "[FAIL] narty27 is not serving /${path} on 127.0.0.1:8240 (content loaded?)" + exit 1 + fi +done + +echo "[OK] narty27 is healthy" +exit 0 diff --git a/services/narty27/service.yaml b/services/narty27/service.yaml new file mode 100644 index 0000000..00ac8d2 --- /dev/null +++ b/services/narty27/service.yaml @@ -0,0 +1,26 @@ +service: + name: narty27 + owner_node: piha + role: static-html-host # single self-contained viz.html (narty 2027 / Saalbach KB export) + exposure: private # LAN/Tailscale only; no npm vhost, no public ingress + dependencies: [] # nginx serving a local volume — nothing else required + ports: + - container: 80 + host: 8240 + protocol: tcp + healthcheck: + type: http + endpoint: http://192.168.31.5:8240/viz.html # PIHA LAN IP; content must be loaded first (see README) + interval: 30s + timeout: 10s + retries: 5 + restart_policy: unless-stopped + persistence: + # Docker named volume narty27_narty27_content (compose project prefix), NOT + # a bind under /opt/homelab/data. Content is personal, lives only here, and + # is deliberately outside the repo — see README.md. + paths: + - narty27_narty27_content + runtime: + config_files: [] # no .env — the port bind is static, no secrets + env_vars: []