The embedded outpost carries no provider token (token=None), so the
auth_proxy identity header was never set and no user could log in. RP
mode is topology-independent: challenge gate stays in front, login runs
inside Grafana via the OAuth2 code flow against auth.franksdank.cloud.
Provider 'grafana' (pk 9, confidential) created via ak shell; secret
written straight into the live .env (never in git). Auth URL uses the
global /application/o/authorize/ path — this fork resolves the provider
by client_id query param; per-slug authorize does not exist.
Caddyfile comment corrected: no identity header is forwarded to Grafana.
Grafana leaves the exempt-by-construction set: grafana.franksdank.cloud
now terminates at the Anubis guard (anubis:8923) and authenticated
traffic returns at the internal :8017 listener, where an @grafana Host
handler proxies through to grafana:3000 with the outpost-proxy trust
token (GRAFANA_PROXY_TOKEN = client_id_client_secret of the new
'grafana-proxy' ProxyProvider on the embedded outpost; created via
ak shell before this route flip — provider-first ordering per the
External Exposure Policy).
The Grafana admin account is intentionally kept: auth.proxy in
auto-upsert mode only creates non-admin users, so the local admin
remains the only way to administer Grafana. The old edge-basicauth
wrapper is retired (shared family password, no per-user audit trail).
- Caddyfile: grafana vhost flipped to the guarded chain; @grafana
handler added at the :8017 return listener.
- compose.yaml: GF_AUTH_PROXY_ENABLED/HEADERS/TOKEN on the grafana
service (auth_proxy trusted-proxy CIDRs left at the image default —
the proxying Caddy container is not statically addressable on
10.89.0.0/24, so Grafana trusts any in-network source and relies on
the token as the capability).
- .env.example: GRAFANA_PROXY_TOKEN placeholder so test env_consistency
keeps guarding the compose<->env contract.
Rootless podman cannot resolve the literal group name 'render' inside the
container user namespace and fails the start with 'unable to find group
render'. keep-groups preserves the launching user's real supplementary
groups (dank is in render on this host), which is the canonical rootless
ROCm device-access recipe.
/dev/kfd and /dev/dri/renderD128 are root:render 0660 on this host. The
container passed the device nodes but only joined group video(44), so torch's
HIP init failed with 'No HIP GPUs are available' and main.py never bound
:8188 — every proxy hop to the app returned 502. Group membership is a
create-time property, so this deploy recreates the container.
The gateway unit ran on dankclaw_default and could not reach the host
daemon: from that network only DNAT'd published ports accept connections to
the LAN address. The compose era ran the gateway with network_mode: host, so
the daemon was reachable; the unit silently changed topology at takeover and
broke the adapter. Host networking + a loopback dial removes the dependency
entirely — the gateway is an outbound-only client and publishes nothing.
The gateway's SIGNAL_HTTP_URL pointed at the host LAN IP. From container
space on netavark networks only DNAT'd published ports accept connections
to that address; :8080 has no forward, so every connection was refused
and the Signal adapter stayed disconnected since the Aug 26 reboot.
host.containers.internal is the verified working path (probed live from a
probe container on dankclaw_default).
The public hostname also routed to hermes:8090, which serves nothing —
the gateway has no HTTP listener. It now routes to the dashboard's login-
gated admin UI on :9119, behind the existing Anubis+Authentik guard.
The compose-era authentik trio died at every boot (rootless podman does not
auto-start bare containers). Convert the trio to Quadlet units installed by
deploy-remote.sh into ~/.config/containers/systemd/, enabled + WantedBy=
default.target:
- 3 unit files under dankclaw/authentik/quadlet/ (postgres -> server -> worker
ordering via Wants=/After=; secrets stay in the git-ignored host .env,
expanded by systemd at start time exactly like the gitea units)
- deploy_authentik() handler: install units, daemon-reload, retire stale
compose-era containers only while units are inactive, enable once, then
start in dependency order with podman-health gates between stages
- compose.yaml: authentik service definitions retired to a comment pointing at
the quadlet dir (same pattern as searxng/open-webui/gitea/caddy)
- tests/authentik.bats rewritten from compose assertions to unit-file
contract assertions (images, Exec roles, ordering, loopback-only publish,
pg_isready probe, secret hygiene, dispatch routing)
Caddy left compose.yaml for a quadlet unit (caddy.service): joins
dankclaw_default for container-name upstreams, Pull=never for the local-only
image, Porkbun credentials via /home/dank/Containers/caddy/env written by
deploy from .env, bare-IP-catch-all health check. deploy_caddy installs,
enables, and starts the unit after retiring the bare container (state is in
host volumes).
Pi-hole: the repo now owns the sysctl drop-in
(dankclaw/pihole/sysctl/99-dnsmasq.conf, ip_unprivileged_port_start=53);
deploy_pihole installs it to /etc/sysctl.d/, stages conflicting later-sorted
drop-ins in the home trash, and applies 'sysctl --system' (the boot code
path). This fixes the 2026-08-26 crash loop (port-53 bind denied, restart
counter 1645) caused by a second drop-in resetting the value to 80.
Tests: new caddy_quadlet.bats seam; caddyfile/deployment_flow assertions
repointed from the removed compose service to the quadlet; pihole.bats pins
the sysctl drop-in and its deployment; test-changed.sh routes dankclaw/caddy/
and the pihole subtree to their seams. Docs: Caddy, Pi-hole, Dankclaw Stack,
work log.
The caddy container mounted the repo's shared/homepage (the static source) to
/srv/homepage. But the deploy writes the generated files (status.json from the
status checker, deployed.js from the deploy) to the host's /srv/homepage. So
the container served the static files (200) but 404'd the generated ones.
Mount the host's /srv/homepage instead, which holds both the static files (the
deploy copies them) and the generated ones.
- alloy 1.x renamed the server component to ui; the old block failed the
initial load and the container crash-looped
- grafana/loki is distroless and grafana/alloy lacks wget, so their
healthchecks could never run; local images add a static busybox from
alpine for that one command
- deploy.sh builds all three local images if missing
- tests: 3 new seams
Measured the rootless mapping: container root IS the invoking user
(dank, owner of the /opt data dirs); the images' default users (loki
10001, prometheus nobody, grafana 472) map to subuids that own nothing.
1000:1000 was the wrong direction.
- loki: recent releases require compactor.delete_request_store when
retention is enabled (was crash-looping on start)
- prometheus: scrape targets carried legacy dankclaw.local names with
URL schemes (invalid); point at published LAN ports and the gitea
quadlet's published port instead
- grafana: GF_SERVER_ROOT_URL to the canonical HTTPS name
- tests: 3 new seams for the deployed observability configs
- Caddyfile: drop http:// site prefixes, global acme_dns porkbun block,
HTTPS redirects on the bare-IP and legacy vhosts
- new dankclaw/caddy/Containerfile: xcaddy build with caddy-dns/porkbun
- compose: caddy uses franksdank/caddy-porkbun:latest + Porkbun env vars
- deploy.sh: build the image on first deploy if missing
- .env.example: rename PORKBUN_API_SECRET -> PORKBUN_API_SECRET_KEY
(the name the caddy-dns/porkbun module expects)
- tests: 7 new caddyfile/compose seams for the TLS config
Per user direction, Caddy runs only on dankclaw. It fronts every
service on both hosts: local services by container name, quadlet
services by published LAN port, dankserver services by published LAN
port (192.168.0.235:PORT).
- dankclaw/Caddyfile: merged vhost list (was: two Caddyfiles, one per
host). gitea's single vhost moves in from the retired gitea-caddy
quadlet (option B). Upstreams fixed to live reality: gitea :3000,
search :8081 (gluetun), chat :3005 (open-webui quadlet), media
services via 192.168.0.235. Retired: llms (open-webui on dankserver
is removed; chat is its successor), the dankserver homepage (302 to
the shared one).
- dankserver/Caddyfile, dankclaw/gitea/Caddyfile: deleted (git rm).
- dnsmasq addn-hosts: every name -> 192.168.0.143 (the one Caddy
host); no .235 records, no llms.
- compose.yaml: removed the openwebui/postgres/gitea services (they
are quadlets now); caddy service mounts the rendered Caddyfile from
/home/dank/Containers/caddy, healthcheck probes the bare-IP
catch-all (302).
- deploy.sh: per-target logic (dankserver = pull-only, health via the
shared Caddy at 192.168.0.143); renders CADDY_BASICAUTH_HASH from
the host .env into the mounted Caddyfile.
- .env.example: CADDY_BASICAUTH_HASH on dankclaw; Porkbun block
dropped from dankserver (its Caddy is gone); SEARXNG_BASE_URL to
the flat name; stale OPENAI_* block dropped (quadlet is
self-contained).
- tests: caddyfile.bats now asserts the single-Caddyfile invariant;
dnsmasq.bats cross-checks vhosts -> .143 and the retired names.
Add a hermes-dashboard compose service that runs 'hermes dashboard' on
0.0.0.0:9119 with HTTP basic auth. The username + scrypt password hash
live in the git-ignored /home/dank/hermes-data config (plaintext never
stored). A separate container from the Signal gateway, since the gateway
entrypoint does not start the dashboard. Both share the data volume and
are boot-persistent. Docs page + index link added (docs-first contract).
The slirp host.containers.internal mapping (169.254.1.2) went down at
runtime, so the Signal adapter could not reach the signal-cli daemon.
The host LAN IP (192.168.0.143:8080) is the stable container->host path
and is reachable from the container. Switch SIGNAL_HTTP_URL to it.
'hermes gateway run' starts the messaging gateway (Signal SSE), not the :8090
web dashboard, so probing 8090 always reported unhealthy. The container's
running state plus the daemon's SSE connection are the real health signal.
The image's default s6 entrypoint launches the interactive TUI, which exits
without a TTY and crash-loops the container. Override the entrypoint to run
'hermes gateway run --force' (the documented Docker mode) and point HERMES_HOME
at the mounted data dir so the Signal platform config + state persist. The
Signal platform is enabled in config.yaml (platforms.signal.enabled).
The hermes container reaches the host signal-cli daemon via podman's
host.containers.internal alias (127.0.0.1 in the container is not the host).
SIGNAL_ACCOUNT/SIGNAL_ALLOWED_USERS come from the git-ignored .env (the phone
number is personal, not committed). Move the data volume to a dank-owned path
(/opt is root-owned and not writable by the run user).
/opt is root-owned and not writable by the run user. Point the model/input/
output/custom_nodes mounts at a dank-owned path so the container can write
them without sudo. The tests do not assert volume paths, so this is a no-op
for the suite.
Rootless podman cannot map the 'render' gid (990) into the container
namespace (dank's subgid range is 100000+), so --group-add render fails to
start the container. GPU access actually comes from the /dev/kfd + /dev/dri
passthrough, checked host-side (the run user is already in 'render' on the
host). So add only the 'video' group (which resolves) and drop 'render'.
Update the group test to assert video present and render ABSENT (with the
rootless rationale), per the approved rootless config.
Replace the CPU stock image with a container built from
Containerfile.comfyui on AMD's gfx1151-tagged vLLM base — the only torch
build verified to report torch.cuda.is_available()=True on the Strix Halo
APU (Radeon 8060S, capability (11,5)).
- Containerfile bakes the ComfyUI code in and installs its deps, but skips
the torch family (the base image already ships the gfx1151 ROCm
torch/torchvision/torchaudio; an unpinned 'torch' in requirements.txt would
clobber the gfx1151 kernels).
- compose comfyui service: build + image comfyui-rocm:gfx1151, passes
/dev/kfd + /dev/dri, adds video+render groups, drops --cpu. Models/data
persist under /opt/comfyui.
- Un-skip the four compose-wiring acceptance tests (the gate for this work).
Remove gluetun/searxng/valkey from dankclaw/compose.yaml (they are now
quadlet units) so podman-compose up can no longer fight them for the netns and
port 8081. Add scripts/deploy-searxng-quadlet.sh: an idempotent installer that
copies the units to ~/.config/containers/systemd/, daemon-reloads, enables them
for boot, and starts them in dependency order. Safe to run on first migration
and on every later change.
The :8000 status server 404s on every path and the image has no curl, so the
prior healthchecks could not pass. Check /sys/class/net/tun0 instead: it exists
only while the WireGuard tunnel is connected, so the healthcheck reflects real
VPN state.
The gluetun image ships wget, not curl, and its status server answers 200 at
/ on port 8000 (not /health). The old curl-based healthcheck always failed, so
the container read 'unhealthy' even with a working tunnel. Use wget against /.
The host has no unqualified-search registries in registries.conf, so the
short names qmcgaw/gluetun and valkey/valkey did not resolve and the pull
hung. Qualify them with docker.io/ so the pull works on any host.
The compose only forwarded VPN_SERVICE_PROVIDER, VPN_TYPE and the private
key, so Gluetun could not form a tunnel. Forward the peer public key,
endpoint IP/port, addresses, allowed IPs and keepalive interval. Add
matching placeholders to .env.example (current Gluetun uses the
single-peer WIREGUARD_* model, not the old WIREGUARD_PEERS_* names).
unattended-upgrades rebooted dankclaw at 15:01; the legacy
podman-compose gitea stack had no systemd units, so rootless podman
did not auto-start the containers. Gitea was down ~1 hour.
- add quadlet units (dankclaw/gitea/quadlet/): gitea-postgres,
gitea-server, gitea-caddy + gitea.network (DNS enabled; the default
rootless podman network has dns_enabled=false)
- add gitea stack Caddyfile (upstream gitea-server:3000) and
.env.example; live password stays in /opt/gitea/.env (0600)
- fix latent name bugs in main stack config: DATABASE_HOST
gitea-postgres, Caddyfile upstream gitea-server
- docs: rewrite Gitea page for the quadlet stack (management, boot
persistence, secrets, gotchas, incident log); fix port 3000 in
stack table; Postgres connection notes
- legacy compose retired on target to /opt/gitea/.deprecated/