Problem: docs-lint (added in the wiki-hq reorg) surfaced 126 broken relative
links that predated this session — a container rename, incident/plan docs
that moved into archive/done subfolders without their inbound links being
updated, and a handful of relative-depth bugs in files nested under
containers/archive/ and plans/done/.
Fixes applied, by category:
- 124-authentik.md -> 106-auth-outpost.md (container was renamed; ~40 refs).
- investigations/{2026-04-21-hubris-crash-loop,2026-05-31-authentik-vps-migration}.md
-> archive/ prefix (both moved to investigations/archive/ previously).
- plans/{2026-06-01-slate-ax-to-sodola-migration,2026-06-04_130000-deprecate-claudio-bot,
2026-06-25-yuvomi-deployment}.md -> plans/done/ prefix.
- Depth bugs in files nested one level deeper than their siblings assumed
(investigations/archive/*, knowledge/wiki/containers/archive/*,
plans/done/*) — corrected relative-path depth.
- Destroyed containers with no surviving page (126-plato) delinked to the
containers/index.md archaeology row instead of a 404.
- ludo-mini.yaml -> strong.yaml (host was renamed, same physical machine).
- netbird-vps.md (no narrative page exists) -> netbird-vps.yaml (substrate
record, matching the existing convention for hosts without a wiki page).
- runbook-dpkg-interrupted.md refs -> .agents/skills/runbook-dpkg-interrupted/SKILL.md
(missed in the phase-4 runbook move because the referencing files used a
bare filename, not a runbooks/ prefix).
- One dangling forward-reference to a never-written investigation delinked
to the actual incident record it was describing.
Left alone: two links in knowledge/wiki/containers/101-jellyfin.md into
devops/homelab-authentik-admin/ — an intentional reference to a sibling repo,
not present in this checkout.
Verification: broken-link count 126 -> 2 (real remainder is the cross-repo
reference above); gen-topology.py --check still exit 0; build_host_files.py
still idempotent; all inventory.yaml doc_page targets still resolve.
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.