feat(narty27): statyczny hosting viz.html na PIHA (nginx + named volume)

Nowy serwis services/narty27 — nginx:alpine serwujacy jeden self-contained
plik viz.html z named volume narty27_content (:ro). Port 8240 to nastepny
wolny w bloku 82x0 na PIHA (8210 paperless, 8220 nextcloud, 8230 kb-query).

Content jest personal: zyje wylacznie w volume narty27_narty27_content i w
zrodle na SOLARII (~/narty-2027/saalbach-kb/viz.html). Nigdy w repo, bez
backup joba, bez bindu pod /opt/homelab/data.

Procedura aktualizacji w README uzywa kontenera-pomocnika (alpine z volume
rw), NIE `docker cp` — przy mount :ro docker cp zwraca "mounted volume is
marked read-only", zarowno dla dzialajacego jak i zatrzymanego kontenera
(zweryfikowane empirycznie 2026-07-31). Zapisywane sa dwie kopie tego samego
pliku: viz.html (nazwa kanoniczna) + index.html (zeby golny root dzialal).

Walidacja: docker compose config OK (volume rozwija sie do
narty27_narty27_content), YAML parse OK, bash -n healthcheck.sh OK.
Bez zywego deployu.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
oskar 2026-07-31 16:38:55 +02:00
parent f0f426ef55
commit 2e13a7d1bb
6 changed files with 162 additions and 0 deletions

View file

@ -145,6 +145,26 @@ services:
# .env (KB_DSN, LAN_BIND_IP) lives alongside the compose file; stateless, no data path # .env (KB_DSN, LAN_BIND_IP) lives alongside the compose file; stateless, no data path
config_path: services/kb-query 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) ---------- # --- Known unmanaged containers on piha (recon B5/B6, 2026-07-27) ----------
# ~28 running containers have no entry above and are deliberately NOT being # ~28 running containers have no entry above and are deliberately NOT being
# added piecemeal — bringing them under desired state is a later stage # added piecemeal — bringing them under desired state is a later stage

View file

@ -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
```

View file

@ -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:

View file

@ -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/<service>/ layout from CLAUDE.md complete — there is
# nothing to copy to .env.

20
services/narty27/healthcheck.sh Executable file
View file

@ -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

View file

@ -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: []