/run/user/<uid> is created by PAM/logind and doesn't exist in non-login
shells. Fall back to /tmp/podman-<uid> when XDG_RUNTIME_DIR is unset so
mkdir always succeeds.
https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
systemctl --user fails in non-interactive shells (no D-Bus session bus).
podman system service starts the socket directly without systemd/D-Bus,
backgrounding the process and waiting up to 5 s for the socket to appear.
https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
start.sh now activates the Podman user socket via systemctl --user if it
isn't running yet, then exports DOCKER_HOST and PODMAN_SOCK so that
podman compose (which delegates to the docker-compose plugin) can connect.
docker-compose.yml mounts ${PODMAN_SOCK} into the socat proxy container
at a fixed internal path (/podman.sock), so it works for both rootful
(/run/podman/podman.sock) and rootless (/run/user/<UID>/podman/podman.sock)
without hardcoding the UID.
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
- New HIY_ADMIN_USER / HIY_ADMIN_PASS env vars control access
- Login page at /login with redirect-after-login support
- Cookie-based sessions (HttpOnly, SameSite=Strict); cleared on restart
- Auth middleware applied to all routes except /webhook/:app_id (HMAC) and /login
- Auth is skipped when credentials are not configured (dev mode, warns at startup)
- Logout link in both dashboard nav bars
- Caddy admin port 2019 no longer published to the host in docker-compose
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
Without a fallback, an unset DOMAIN_SUFFIX expands to an empty string,
making Caddy parse the site block as a second global options block and
fail to start. Using {:localhost} defaults to localhost.
Using compose-level ${DOMAIN_SUFFIX} substitution only works when docker
compose is run from the same directory as the .env file. env_file loads
the file relative to the compose file, so it works regardless of CWD.
Caddy's email directive requires a non-empty argument. Since ACME_EMAIL
wasn't set, Caddy failed to parse the config. Email is optional for
Let's Encrypt — remove the directive entirely and document it as a
manual opt-in comment.
Caddy's built-in ACME support handles TLS automatically — no CF_API_TOKEN,
no Cloudflare account, no DNS plugin needed. Requires ports 80+443 forwarded
to the Pi and ACME_EMAIL set in infra/.env.
Use printf instead of heredoc for cargo config — heredoc inside a
conditional RUN block confuses Docker's parser (fi becomes an unknown
instruction). The config is always written; unused linker entries are
harmless on native builds.
gcc-aarch64-linux-gnu is an x86→arm64 cross-compiler; it doesn't exist
on arm64 hosts (like the Pi). Only install cross-toolchains and set cargo
linker config when BUILDPLATFORM != TARGETPLATFORM.
start.sh now generates proxy/caddy.json at launch time with Let's Encrypt
automatic HTTPS (HTTP-01 or TLS-ALPN-01 challenge — no Cloudflare needed).
Reads DOMAIN_SUFFIX and ACME_EMAIL from infra/.env before starting.
Added infra/.env.example to document required vars.
start.sh builds via 'make build' (platform auto-detected) then starts
services detached with 'docker compose up -d'.
Makefile gains build/build-<platform> targets that build images without
starting, mirroring the existing up/<platform> targets.
Remove hardcoded platform from compose file so plain 'make up' (or
'docker compose up --build') always builds natively for the host.
Explicit targets (up-arm64, up-armv7, etc.) set DOCKER_DEFAULT_PLATFORM.
Dockerfile now uses BuildKit TARGETARCH/TARGETVARIANT to pick the Rust
cross-compilation target automatically. The build stage always runs on
the host platform for speed.
Makefile provides named targets:
make up-amd64 # Mac Intel / Linux desktop
make up-arm64 # Mac M1/M2/M3, Pi 4/5 (64-bit OS)
make up-armv7 # Pi 2/3/4 (32-bit OS)
make up-armv6 # Pi Zero / Pi 1
Dashboard now shows:
- System card at top: CPU 1-min load average, RAM used/total, disk used/total
(reads /proc/loadavg, /proc/meminfo, df -k /)
- Two status columns in the apps table:
- "Container" — actual Docker runtime state (running/exited/restarting/not deployed)
via `docker inspect` on each app's hiy-{id} container
- "Last Deploy" — build pipeline status (queued/building/success/failed)
- Auto-refresh now calls /api/status every 5 s and updates both columns
(fixes the previous broken refresh that used app.status which didn't exist)
New API endpoint: GET /api/status → {app_id: {deploy, container}} for all apps
https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
- app_detail now redirects to / instead of 404 when app is not found
(handles case where app was removed while user was on the detail page)
- Add a "← Dashboard" button in the log panel that appears once a
deployment finishes (both success and failed), giving the user a clear
path back to the main screen
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.
Build hiy-server targeting aarch64-unknown-linux-musl so the binary
has no glibc dependency at all, making the runtime image irrelevant
to glibc version mismatches. Uses rustls (already in Cargo.toml) so
no OpenSSL vendoring needed. SQLite is bundled by sqlx.
rust:1.77-slim has drifted to a newer Debian base with glibc 2.39,
but debian:bookworm-slim only has glibc 2.36, causing a GLIBC_2.39
not found error at runtime. Pinning to the explicit bookworm variant
keeps both stages on the same glibc version.
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
Without this, git on macOS with core.autocrlf=true checks out .sh
files with CRLF. Bash then reads 'CONTAINER_NAME\r' as the variable
name (never set), causing 'unbound variable' with set -u.
https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH