spa-host — one-off static hosting at *.delo.sh
Publishes a folder of static files at https://<name>.delo.sh in one step, using Jarad's
existing homelab ingress. This is personal-infra tooling — it assumes big-chungus with
Traefik + the Cloudflare Tunnel running. It is intentionally global (not tied to any repo).
How the plumbing works (why this is a one-liner)
Internet → Cloudflare (wildcard *.delo.sh DNS, proxied)
→ Cloudflare Tunnel (ingress: *.delo.sh → https://traefik:443)
→ Traefik (routes by Host label, terminates TLS via Let's Encrypt DNS-01)
→ your nginx:alpine container (on the `proxy` network, serving your folder)
Because a wildcard *.delo.sh DNS record and a wildcard *.delo.sh tunnel ingress
already exist, a single-label subdomain like lego.delo.sh needs no DNS and no tunnel
changes — dropping the labeled container on the proxy network is enough. Traefik mints a
real cert on first request via the Cloudflare DNS-01 challenge (so the origin never has to
be publicly reachable). The reference example is ~/docker/stacks/websites/domipacolypse/.
Prerequisites (verify, don't assume)
dockerreachable, and the external network exists:docker network inspect proxy- Traefik + tunnel up:
docker ps | grep -E 'traefik|cloudflare-tunnel' - The subdomain resolves (wildcard):
getent hosts <name>.delo.sh→ Cloudflare IPs. - The folder to serve exists and contains
index.html.
Usage
The work is done by spa-host.sh (next to this file). It is idempotent — re-running updates
the stack in place.
~/.claude/skills/spa-host/spa-host.sh <domain> <static-dir>
# e.g.
~/.claude/skills/spa-host/spa-host.sh lego.delo.sh /home/delorenj/code/legofirst/apps/web
Tear a host down:
~/.claude/skills/spa-host/spa-host.sh <domain> --down
What the script does
- Derives a docker-safe stack/router name from the domain (
lego.delo.sh→lego). - Guards against nested subdomains (
x.y.delo.sh) — the wildcard ingress matches only one label, so it prints the exact tunnel-ingress rule to add + reminds you to restart the tunnel. Single-label subdomains need nothing extra. - Writes
~/docker/stacks/websites/<name>/compose.ymlfrom the domipacolypse template (nginx:alpine, read-only bind-mount of<static-dir>→/usr/share/nginx/html, on theproxynetwork, with the five Traefik labels: enable, Host rule, websecure entrypoint, letsencrypt certresolver, loadbalancer port 80, docker network proxy). docker compose up -d.- Polls
https://<domain>until it returns 200 (first cert issue can take ~90–120s because Traefik's resolver hasdelayBeforeCheck: 90).
The bind-mount is live — editing files in <static-dir> updates the site on refresh, no
rebuild. Perfect for a static SPA whose per-deploy config (e.g. a git-ignored config.js)
lives right in that folder.
When driving this as an LLM
- Get the two args. If the user gave only a domain, ask for (or infer) the static dir — the
folder containing
index.html(a repo'sapps/web, a build'sdist/, etc.). - Run the script. Relay its ✓/⚠ output.
- If it ends on ⚠ (cert still minting), wait ~60s and
curl -I https://<domain>once more before reporting; only escalate to Traefik logs if it still isn't 200. - Report the live URL. Never paste secrets; the folder's
config.js/.envare the user's.
Notes & guardrails
- Single base zone:
delo.sh. For a different zone, this pattern doesn't apply as-is. - Don't publish someone else's or sensitive content without the user's ok — this makes it publicly reachable on the internet.
- Teardown stops the container but leaves the stack dir;
rm -rf ~/docker/stacks/websites/<name>to remove it entirely. - If
getent hosts <domain>doesn't resolve, the wildcard DNS record may be missing — add a proxied CNAME<name>→<tunnel-id>.cfargotunnel.comin Cloudflare (tunnel id in~/docker/core/cloudflare-tunnel/config.yml).