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>
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.ymlbinds./data:/dataand127.0.0.1:3100:3000.docker-compose.override.ymluses the!overrideYAML tag onvolumes:andports:to redirectdatato/mnt/library/documents/artifacts. - Data dir:
/mnt/library/documents/artifacts— owner UID 10001 : GID 10001 (matches theappuser baked into the Dockerfile), mode 750. Parent/mnt/library/documentsstayswww-data:www-data 750; bind targets the child directly so no traversal needed. .env(untracked) holdsADMIN_PASSWORD,SESSION_SECRET(openssl rand -hex 32),BASE_URL=https://artifacto.hubris.network,SSO_GATEWAY_SECRET(must matchARTIFACTO_GATEWAY_SECRETin/etc/caddy/caddy.envon caddy). Don'tgit checkout .env.- Auth: Authentik forward-auth on the admin surface (
/,/login,/a/*,/api/*).X-Authentik-Usernameis honored only when theX-Artifacto-Gatewayheader matchesSSO_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/health→ok - 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-readerkey (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). /revokeis admin-token-gated by/etc/secrets-issuance/admin-token; shreds the local key file and adds the hostname to the denylist. Called byhomelab 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
mediato the LXC alone is not enough for inner Docker containers — each container needs--group-add 10000oruser: "<uid>:10000"to write into/mnt/library.
Related
- Gitea (104) — uses the PlantUML server
- Caddy (121)
- Authentik (124)
- DNS
- Auto-deploy
- Public ingress (Artifacto + blog)
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.