diff --git a/hosts/piha/services.yaml b/hosts/piha/services.yaml index 74f6c11..1865d47 100644 --- a/hosts/piha/services.yaml +++ b/hosts/piha/services.yaml @@ -165,6 +165,26 @@ services: # No backup job, no /opt/homelab/data bind. config_path: services/narty27 + kb-site: + role: static-html-host # public KB slice generated by scripts/kb/gen_pages.py + deployment_model: docker-compose + exposure: public # via npm@PIHA vhost kb.okit.pl; container binds LAN :8250 + offline_required: false + depends_on: + local: [] + external: [] + ports: + - name: http + container_port: 80 + host_port: 8250 + protocol: tcp + runtime: + # No config and no secrets. Content is a pure artifact of the repo: it + # lives only in the Docker named volume kb-site_kb-site_content and is + # refreshed by regenerating from kb/ (kb/runbooks/kb-site-deploy.md). + # No backup job, no /opt/homelab/data bind. + config_path: services/kb-site + # --- 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/kb-site/README.md b/services/kb-site/README.md new file mode 100644 index 0000000..3146c16 --- /dev/null +++ b/services/kb-site/README.md @@ -0,0 +1,5 @@ +# kb-site + +Public slice of the knowledge base (`kb.okit.pl`) — static HTML generated from `kb/**/*.md` by `scripts/kb/gen_pages.py`, served by nginx on PIHA. + +Dokumentacja: [kb/services/kb-site.md](../../kb/services/kb-site.md) diff --git a/services/kb-site/docker-compose.yml b/services/kb-site/docker-compose.yml new file mode 100644 index 0000000..d991ac6 --- /dev/null +++ b/services/kb-site/docker-compose.yml @@ -0,0 +1,28 @@ +services: + kb-site: + image: nginx:alpine + container_name: kb-site + restart: unless-stopped + ports: + # PIHA 82x0 static-HTTP block: 8210 paperless, 8220 nextcloud, + # 8230 kb-query, 8240 narty27 -> 8250 is the next free slot. + # Publicly the site is reached only through the npm@PIHA vhost + # kb.okit.pl; this bind is the proxy's upstream. + - "8250:80" + volumes: + # Generated output of scripts/kb/gen_pages.py — never committed, never + # bind-mounted from the repo. Read-only: nginx only serves it; writes go + # through the helper-container procedure in kb/runbooks/kb-site-deploy.md + # (docker cp cannot write into a :ro mount). + - kb-site_content:/usr/share/nginx/html:ro + # busybox wget — nginx:alpine ships no curl. index.html is generated on + # every run, so it is the one file that must always be there. + healthcheck: + test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1/index.html"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 5s + +volumes: + kb-site_content: diff --git a/services/kb-site/env.example b/services/kb-site/env.example new file mode 100644 index 0000000..85022ed --- /dev/null +++ b/services/kb-site/env.example @@ -0,0 +1,10 @@ +# kb-site has NO configuration and NO secrets. +# +# The port bind (8250:80) is static and the content lives in the +# kb-site_kb-site_content Docker volume, generated from kb/ by +# scripts/kb/gen_pages.py. This file exists only to keep the +# services// layout from CLAUDE.md complete — there is nothing to +# copy to .env. +# +# The public address is a generator argument, not an env var: +# python3 scripts/kb/gen_pages.py --base-url https://kb.okit.pl diff --git a/services/kb-site/healthcheck.sh b/services/kb-site/healthcheck.sh new file mode 100755 index 0000000..33f11e8 --- /dev/null +++ b/services/kb-site/healthcheck.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# Healthcheck for kb-site (nginx:alpine serving the generated public KB) + +# Container must be running +if ! docker ps --filter "name=kb-site" --filter "status=running" | grep -qw "kb-site"; then + echo "[FAIL] kb-site container is not running" + exit 1 +fi + +# The index is generated on every run, so both the bare root and /index.html +# must answer. An empty volume means the content was never loaded — see +# kb/runbooks/kb-site-deploy.md. +for path in "" index.html; do + if ! curl -sf -o /dev/null "http://127.0.0.1:8250/${path}"; then + echo "[FAIL] kb-site is not serving /${path} on 127.0.0.1:8250 (content loaded?)" + exit 1 + fi +done + +echo "[OK] kb-site is healthy" +exit 0 diff --git a/services/kb-site/service.yaml b/services/kb-site/service.yaml new file mode 100644 index 0000000..5cbdc84 --- /dev/null +++ b/services/kb-site/service.yaml @@ -0,0 +1,33 @@ +service: + name: kb-site + owner_node: piha + role: static-html-host # public slice of the KB, rendered by scripts/kb/gen_pages.py + exposure: public # public via npm@PIHA (kb.okit.pl). The container itself binds + # 8250 on the LAN; npm is the sole public entry point. + dependencies: [] # nginx serving a local volume — nothing else required at runtime + ports: + - container: 80 + host: 8250 + protocol: tcp + healthcheck: + type: http + endpoint: http://127.0.0.1:8250/index.html # content must be loaded first (see runbook) + interval: 30s + timeout: 10s + retries: 5 + restart_policy: unless-stopped + persistence: + # Docker named volume kb-site_kb-site_content (compose project prefix), NOT a + # bind under /opt/homelab/data. The content is a pure artifact: regenerate it + # from the repo with scripts/kb/gen_pages.py, no backup job needed. + paths: + - kb-site_kb-site_content + runtime: + config_files: [] # no .env — the port bind is static, no secrets + env_vars: [] + content: + # Only kb/ documents with `visibility: public` are published; the generator + # is fail-closed (no frontmatter / no visibility field = private). + generator: scripts/kb/gen_pages.py + leak_check: scripts/kb/gen_pages.py --check # must pass before publishing + source: kb/**/*.md