- Migrations 010 (content_hash) + 011 (search tsvector column) - new: internal/knowledge/seed.go — knowledge seed ingest engine - new: internal/httpapi/knowledge.go — SearchKnowledge + GetEntityKnowledge - wire knowledge ingest into oikos seed pipeline - convert all 36 wiki docs + 6 investigations + 12 runbooks → seeds/knowledge.yaml - archive: knowledge/wiki/→archive/, oikos/cards/→archive/, .hermes/plans/→archive/ - delete: 9 superseded Python kernel files, ledger/, mcp/build_host_files.py - remove empty knowledge/ directory tree
155 lines
11 KiB
Markdown
155 lines
11 KiB
Markdown
# 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](../infrastructure/homelab-context.md) (MCP + secrets-issuance) since 2026-05-20. Booklore migrated to [grimmory (130)](130-grimmory.md) 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](../infrastructure/ingress.md). |
|
|
| `blog.hubris.network` | WriteFreely | `:8080` | Native OIDC via `[oauth.generic]`. |
|
|
| `git.hubris.network/_plantuml/*` | PlantUML server | `:8079` | Same-origin route from [gitea (104)](104-gitea.md). |
|
|
| `mcp.hubris.network` | homelab-mcp | `:9810` | MCP server over SSE. No SSO — agent-facing, gated at source-IP layer. See [homelab-context](../infrastructure/homelab-context.md). |
|
|
| `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](121-caddy.md)). 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](../infrastructure/ingress.md).
|
|
|
|
#### 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](104-gitea.md)'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/`) — DEPRECATED (Go rewrite, Phase 6)
|
|
|
|
> **Status:** This Python MCP server is being replaced by the Go `oikos api` binary
|
|
> running in Docker on mac-mini. Cutover pending — see
|
|
> [scripts/cutover-checklist.md](../../scripts/cutover-checklist.md) for the
|
|
> execution plan. The Go MCP uses the official MCP Go SDK (Streamable HTTP, not
|
|
> FastMCP) with 15 tools including `get_blast_radius`, `request_execution`, and
|
|
> `get_agent_activity`. Source: `internal/mcp/server.go`.
|
|
|
|
**Current (Python) implementation — DO NOT MODIFY, awaiting cutover:**
|
|
FastMCP server (Python venv at `/opt/homelab-mcp/.venv`). Reads from
|
|
`/opt/homelab-context/` (this LXC is itself an enrolled
|
|
[homelab-context](../infrastructure/homelab-context.md) 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/`) — DEPRECATED (Go Phase 5)
|
|
|
|
> **Status:** Replaced by `internal/secrets/` in the Go rewrite. Machine identities
|
|
> are now managed via Infisical (`docker compose --profile infisical up`).
|
|
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](../infrastructure/media-permissions.md). 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`.
|
|
|
|
## Related
|
|
- [Gitea (104)](104-gitea.md) — uses the PlantUML server
|
|
- [Caddy (121)](121-caddy.md)
|
|
- [Authentik (124)](106-auth-outpost.md)
|
|
- [DNS](../infrastructure/dns.md)
|
|
- [Auto-deploy](../infrastructure/auto-deploy.md)
|
|
- [Public ingress (Artifacto + blog)](../infrastructure/ingress.md)
|
|
|
|
## 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)](130-grimmory.md)'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](../infrastructure/homelab-context.md):
|
|
`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](121-caddy.md), split-horizon DNS entries on
|
|
[authentik (124)](106-auth-outpost.md). 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)](104-gitea.md).
|
|
|
|
### 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)](104-gitea.md)'s footer.tmpl injection. Same-origin via caddy `handle_path /_plantuml/*` rewrite — no separate hostname.
|