Files
oikos/containers/105-apps.md
dtoro f5cd320433 Bootstrap Homelab-Docs wiki
Initial documentation of the hubris Proxmox homelab as a cross-linked
markdown wiki. Per-node pages, cross-cutting infrastructure pages, an
investigation log, and an operations cheatsheet. Each node and topic
ends with a Changelog section so changes can be tracked in-place going
forward.

Refreshed against live state on 2026-04-28 — 14 active LXCs (109
syncthing currently stopped) + 1 VM (108 haos). Reflects post-A/B-test
state of the 2026-04-21 hubris crash-loop investigation.
2026-04-28 22:53:09 +02:00

7.3 KiB

105 — apps

Docker host for everything that doesn't justify its own LXC. Currently runs Artifacto, Booklore, PlantUML server, Portainer (and historically WriteFreely / blog).

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).
books.hubris.network Booklore :6060 Native OIDC. Redirect URI /oauth2-callback.
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).

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 ://).

Booklore

Native OIDC via Authentik (Settings → OIDC). Redirect URI /oauth2-callback (NOT /api/oidc). Container needs extra_hosts: auth.hubris.network:192.168.8.175. Edit via Portainer UI if it's a Portainer-managed stack.

⚠️ Never docker compose up Portainer-managed stacks from the host shell. Portainer's compose state lives at /var/lib/docker/volumes/portainer_data/_data/compose/<N>/. Running docker compose up -d <svc> from the host triggers recreates of OTHER services in the stack and silently destroys bind-mounted data. This wiped Booklore's mariadb data on 2026-04-22. Use the Portainer UI editor for compose changes. See mesh migration for the full warning.

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-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.