Two root causes:
1. Caddy was started without --resume, so every restart wiped all
dynamically-registered app routes (only the base Caddyfile survived).
Adding --resume makes Caddy reload its auto-saved config (stored in
the caddy-config volume) which includes all app routes.
2. App routes used the container IP address, which changes whenever
hiy-net is torn down and recreated by compose. Switch to the
container name as the upstream dial address; Podman's aardvark-dns
resolves it by name within hiy-net, so it stays valid across
network recreations.
Together with the existing reconnect loop in start.sh these two
changes mean deployed apps survive a platform restart without needing
a redeploy.
https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
Raspberry Pi OS does not enable swap cgroup accounting by default.
Even --memory-swap=-1 causes runc to write "max" to memory.swap.max,
which fails with ENOENT when the file does not exist.
Removing --memory entirely means runc skips all memory.* cgroup writes.
--cpus is unaffected (uses cpu.max, which is always present).
https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
Raspberry Pi OS does not enable swap accounting in cgroups by default,
so the memory.swap.max cgroup v2 file does not exist. Setting --memory
without --memory-swap causes runc to write a swap limit to that file,
which fails with ENOENT.
Adding --memory-swap=-1 tells runc to leave swap unlimited, skipping
the memory.swap.max write entirely.
https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
Control plane:
- Users and app grants stored in SQLite (users + user_apps tables)
- bcrypt password hashing
- Sessions: HashMap<token, user_id> (in-memory, cleared on restart)
- Bootstrap: first admin auto-created from HIY_ADMIN_USER/HIY_ADMIN_PASS if DB is empty
- /admin/users page: create/delete users, toggle admin, grant/revoke app access
- /api/users + /api/users/:id/apps/:app_id REST endpoints (admin-only)
Deployed apps:
- Every app route now uses Caddy forward_auth pointing at /auth/verify
- /auth/verify checks session cookie + user_apps grant (admins have access to all apps)
- Unauthenticated -> 302 to /login?next=<original URL>
- Authorised but not granted -> /denied page
- Session cookie set with Domain=.DOMAIN_SUFFIX for cross-subdomain auth
Other:
- /denied page for "logged in but not granted" case
- Login page skips re-auth if already logged in
- Cookie uses SameSite=Lax (required for cross-subdomain redirect flows)
https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
Caddy's Caddyfile adapter names servers 'srv0' (not 'hiy'), so
PATCHing /config/apps/http/servers/hiy/routes was a no-op. Now we
query /config/apps/http/servers/ to find the actual server name
before updating routes.
https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
Apps follow the Heroku convention of binding to $PORT at runtime.
Without --env PORT=$PORT, containers use their default port which
doesn't match what Caddy is configured to dial, causing 502s.
- Add docker-proxy (alpine/socat) sidecar that exposes the Docker Unix
socket as TCP on port 2375, so server needs no privileged socket mount
- Set DOCKER_HOST=tcp://docker-proxy:2375 in server environment
- App containers are still spawned on the host daemon and join hiy-net,
so Caddy can still reach them
- Log actual Caddy PUT response body and HTTP status on failure
instead of a silent warning
- Add --fail to the GET so a 404 (no 'hiy' server yet, stale volume)
falls back to [] instead of passing error JSON to Python
- Python now guards against non-list responses with try/except
- Always re-append the dashboard catch-all route so it survives
even when routes are rebuilt from scratch
The Caddyfile created a server with an auto-generated name, not 'hiy',
so build.sh's PUT to /config/apps/http/servers/hiy/routes was creating
a parallel server that never received traffic.
- Replace Caddyfile with caddy.json that names the server 'hiy' with
the dashboard as a catch-all fallback route
- Insert app routes at index 0 so host-matched routes are evaluated
before the catch-all dashboard fallback
- Update docker-compose to mount caddy.json and pass --config flag
r.get() crashed when the Caddy API returned a routes array containing
string elements. Added isinstance(r, dict) check and also made the
match[0] traversal safer by using any() over the match list.
The old message just said 'expose manually if needed' with no guidance.
Now it prints the exact docker commands to publish the port directly
and how to find the container IP for a custom reverse proxy.
https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
docker run fails hard if the path passed to --env-file is missing.
Make the flag conditional so apps without an env file (or where the
file hasn't been created yet) still start successfully.
https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH