Files
oikos/knowledge/wiki/containers/105-apps.md
dtoro 8a6422bd7d docs: move narrative wiki under knowledge/wiki/ (phase 3)
Problem: node and cross-cutting narratives lived at the repo root
(containers/, vms/, infrastructure/, host .md files), interleaved with the
machine-readable substrate.

Change:
- Move containers/ -> knowledge/wiki/containers/, vms/ -> knowledge/wiki/vms/,
  infrastructure/ -> knowledge/wiki/infrastructure/, hosts/{hubris,strong}.md ->
  knowledge/wiki/hosts/, infrastructure/references/ -> knowledge/sources/references/,
  GLOSSARY.md -> knowledge/GLOSSARY.md.
- Add knowledge/{index.md,log.md,sources/index.md} scaffolding.
- Rewrite all relative links repo-wide via a path-resolving mapper (inbound +
  outbound + between-moved-files), including .hermes/, runbooks, operations,
  investigations, plans, README, AGENTS.
- Repoint inventory.yaml doc_page fields and regenerate hosts/*.yaml (which
  embed doc_page); update oikos/gen-topology.py output path, candidate doc
  paths, and footer links; update code-comment doc paths.

Substrate untouched in place: inventory.yaml, hosts/*.yaml (regenerated,
idempotent), oikos/ code, mcp/, secrets/, bin/.

Verification:
- Logical broken-link set identical to pre-move baseline (net 128 -> 127; the
  topology regen fixed one, introduced none). Remaining are pre-existing refs
  to destroyed/archived nodes, out of scope for this move.
- gen-topology.py --check exit 0 (in sync); cards carry knowledge/wiki/ doc paths.
- build_host_files.py idempotent; all inventory doc_page targets resolve.
- MCP contract verified: get_page/search_docs/get_changelog resolve moved pages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 14:35:23 +02:00

10 KiB

105 — apps

Docker host for everything that doesn't justify its own LXC. Currently runs Artifacto, PlantUML server, Portainer (and historically WriteFreely / blog), plus the homelab-context distribution services (MCP + secrets-issuance) since 2026-05-20. Booklore migrated to grimmory (130) on 2026-06-29.

At a glance

  • Hostname: apps
  • IP: 192.168.8.205
  • Privilege: privileged (so container UID = host UID for bind-mount sanity)
  • Resources: 2 cores / 4 GiB RAM / 30 GiB rootfs
  • Mounts: /mnt/library/mnt/library
  • Public hostnames: see table below

Public hostname → service map

Hostname Container Backend port Notes
docker.hubris.network Portainer :9443 Native OAuth2 via Authentik. Trusted-origins requires hostname only (no scheme/port).
artifacto.hubris.network Artifacto :3100 Public /p/*, /static/*, /healthz exposed via VPS traefik.
blog.hubris.network WriteFreely :8080 Native OIDC via [oauth.generic].
git.hubris.network/_plantuml/* PlantUML server :8079 Same-origin route from gitea (104).
mcp.hubris.network homelab-mcp :9810 MCP server over SSE. No SSO — agent-facing, gated at source-IP layer. See homelab-context.
secrets.hubris.network secrets-issuance :9820 Per-client age-key issuance. Mesh+LAN source-IP gated (MESH_SUBNETS).

Stacks

Artifacto (/opt/artifacto/)

Self-hosted HTML-artifact share (Go + SQLite). Repo dtoro/Artifacto is the working tree itself — git push origin main triggers rebuild.

  • Compose: tracked docker-compose.yml binds ./data:/data and 127.0.0.1:3100:3000. docker-compose.override.yml uses the !override YAML tag on volumes: and ports: to redirect data to /mnt/library/documents/artifacts.
  • Data dir: /mnt/library/documents/artifacts — owner UID 10001 : GID 10001 (matches the app user baked into the Dockerfile), mode 750. Parent /mnt/library/documents stays www-data:www-data 750; bind targets the child directly so no traversal needed.
  • .env (untracked) holds ADMIN_PASSWORD, SESSION_SECRET (openssl rand -hex 32), BASE_URL=https://artifacto.hubris.network, SSO_GATEWAY_SECRET (must match ARTIFACTO_GATEWAY_SECRET in /etc/caddy/caddy.env on caddy). Don't git checkout .env.
  • Auth: Authentik forward-auth on the admin surface (/, /login, /a/*, /api/*). X-Authentik-Username is honored only when the X-Artifacto-Gateway header matches SSO_GATEWAY_SECRET — prevents direct LAN connections from spoofing identity.
  • Public exposure: VPS traefik gates path scope to share URLs; admin surface returns 404 at the edge. See ingress.

Auto-deploy pipeline (Artifacto)

Receiver at /opt/artifacto-deploy/ (outside the app repo): deploy.sh + webhook.py, secret in /etc/artifacto-deploy/secret, systemd unit artifacto-deploy-webhook.service. Listens on 0.0.0.0:9798, validates X-Gitea-Signature HMAC-SHA256, filters to refs/heads/main, runs deploy in a daemon thread (returns 202 immediately). Gitea webhook id 7 on dtoro/Artifacto.

  • Logs: pct exec 105 -- journalctl -u artifacto-deploy-webhook -f
  • Manual deploy: pct exec 105 -- /opt/artifacto-deploy/deploy.sh
  • Health: pct exec 105 -- curl -s http://127.0.0.1:9798/healthok
  • Slug length bumped 5 → 8 chars (in internal/store/artifacts.go:41) once the endpoint went public.

PlantUML (/opt/plantuml/)

plantuml/plantuml-server:jetty listening on 192.168.8.205:8079, context path /plantuml (BASE_URL=plantuml). Reached only via gitea's caddy handle_path /_plantuml/* rewrite — no DNS record of its own. Same-origin from gitea so the embedded markdown previewer can call it without CORS.

Portainer

Native OAuth2 (Settings → Authentication → OAuth → Custom). Manual endpoints (no OIDC discovery). Uses portainer-uid custom-claim scope from Authentik. Container is not compose-managed — safe to docker run recreate; data lives in named volume portainer_data. CLI flag: --trusted-origins docker.hubris.network (hostname only — IsTrustedOrigin rejects strings containing ://).

homelab-mcp (/opt/homelab-mcp/)

FastMCP server (Python venv at /opt/homelab-mcp/.venv). Reads from /opt/homelab-context/ (this LXC is itself an enrolled homelab-context client). Source is dtoro/Homelab-Docs/mcp/server.py; service unit homelab-mcp.service binds 0.0.0.0:9810. DNS-rebinding protection disabled at the FastMCP layer because mesh+LAN gating is the actual trust boundary.

  • Endpoint: https://mcp.hubris.network/mcp (Caddy → :9810). StreamableHTTP transport (POST /mcp).
  • 14 tools registered: get_host, list_services, find_service, get_topology, search_docs, get_page, get_changelog, whoami, list_my_secrets (context); get_service_status, tail_log, list_lxcs, get_lxc_state, ping_service (read-only management).
  • Management tools shell out via SSH using a dedicated mcp-reader key (planned; not provisioned yet — management tools fail until then).

Auto-deploy pipeline (homelab-mcp)

Receiver at /opt/homelab-mcp/mcp/deploy/webhook/ (in-tree alongside the server code). Listens on 0.0.0.0:9811, secret in /etc/homelab-mcp-deploy/secret, systemd unit homelab-mcp-deploy.service. Gitea webhook id 10 on dtoro/Homelab-Docs. Deploy script reinstalls the service unit and restarts on push.

secrets-issuance (/opt/secrets-issuance/)

Tiny HTTP service that issues per-client age keypairs the first time each client calls /issue. Idempotent: subsequent calls return the same key. Mesh+LAN source-IP gated via the MESH_SUBNETS env in secrets-issuance.service. State (per-client keys + denylist) lives at /var/lib/secrets-issuance/ (NOT in the repo).

  • Endpoint: https://secrets.hubris.network/issue (Caddy → :9820).
  • /revoke is admin-token-gated by /etc/secrets-issuance/admin-token; shreds the local key file and adds the hostname to the denylist. Called by homelab client remove.
  • Trust subnets today: 100.122.0.0/16 (Netbird), 100.64.0.0/10 (Tailscale), 192.168.8.0/24 (LAN). Tighten if the LAN gets untrusted devices.

Auto-deploy pipeline (secrets-issuance)

Receiver at /opt/secrets-issuance/secrets-issuance/deploy/webhook/, port 9821, secret in /etc/secrets-issuance-deploy/secret, unit secrets-issuance-deploy.service. Gitea webhook id 11.

WriteFreely (/opt/writefreely-docker/)

Native OIDC via [oauth.generic] in config/config.ini. host = https://auth.hubris.network, scope openid profile email, map_user_id = sub. Redirect URI in Authentik: https://blog.hubris.network/oauth/callback/generic. Container has extra_hosts: auth.hubris.network:192.168.8.175. First-time users with a local WF account must connect SSO via Customize/Settings → Linked Accounts.

Permissions

  • LXC is privileged → in-container UID == host UID.
  • Member of media GID 10000. Adding media to the LXC alone is not enough for inner Docker containers — each container needs --group-add 10000 or user: "<uid>:10000" to write into /mnt/library.

Changelog

2026-06-29 — Booklore migrated to Grimmory on LXC 130

Booklore stack removed from Portainer. MariaDB dump taken first, then restored into grimmory (130)'s fresh MariaDB. books.hubris.network Caddy backend updated to 192.168.8.213:6060. Authentik OIDC provider updated to Public client type (PKCE) for Grimmory compatibility.

2026-05-20 — homelab-mcp + secrets-issuance live

Two new services from the homelab-context distribution plan: homelab-mcp.service on :9810 (MCP read+management surface) and secrets-issuance.service on :9820 (per-client age-key provisioning). Caddy fronts both with Let's Encrypt; new vhosts on caddy, split-horizon DNS entries on authentik (124). Gitea webhook ids 10 + 11 wire auto-deploy. LXC is itself an enrolled context client (/opt/homelab-context/).

2026-04-28 — wiki entry created

Initial documentation.

2026-04-22 — Artifacto deployed + Authentik gateway-secret auto-login wired

Repo dtoro/Artifacto. Compose, data dir, .env, webhook (id 7), Caddyfile site block, gateway-secret pattern, public-internet share exposure on the VPS — all set up that day. Slug length bumped 5 → 8 for brute-force resistance.

2026-04-22 — Booklore mariadb data wiped (lesson, not a feature)

docker compose up -d from the host shell on a Portainer-managed stack recreated services and re-initialized the bind-mounted ./mariadb/config. Recovered by reseeding from snapshot. Rule recorded: edit Portainer-managed stacks via the Portainer UI.

2026-04-22 — Portainer wired into Authentik (native OAuth2)

Custom-claim portainer_uid scope. CLI added --trusted-origins docker.hubris.network.

2026-04-22 — app.ini host whitelist on gitea extended

Added 192.168.8.205 so the Artifacto webhook delivery wouldn't be rejected. See gitea (104).

2026-04-21 — Booklore + WriteFreely wired into Authentik OIDC

Native OIDC for both. extra_hosts override added inside each container so DNS resolves auth.hubris.network to the Caddy IP.

2026-04-20 — PlantUML server deployed

Used by gitea (104)'s footer.tmpl injection. Same-origin via caddy handle_path /_plantuml/* rewrite — no separate hostname.