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:
parent
f0f426ef55
commit
2e13a7d1bb
|
|
@ -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
|
||||||
|
|
|
||||||
65
services/narty27/README.md
Normal file
65
services/narty27/README.md
Normal 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
|
||||||
|
```
|
||||||
25
services/narty27/docker-compose.yml
Normal file
25
services/narty27/docker-compose.yml
Normal 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:
|
||||||
6
services/narty27/env.example
Normal file
6
services/narty27/env.example
Normal 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
20
services/narty27/healthcheck.sh
Executable 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
|
||||||
26
services/narty27/service.yaml
Normal file
26
services/narty27/service.yaml
Normal 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: []
|
||||||
Loading…
Reference in a new issue