Add Fly.io public reverse proxy for docs.eblu.me #120

Merged
eblume merged 8 commits from feature/flyio-proxy into main 2026-02-08 02:36:20 -08:00
Owner

Summary

  • Adds a Fly.io reverse proxy (blumeops-proxy) that tunnels public traffic to homelab services over Tailscale
  • First service exposed: docs.eblu.me — the Quartz static docs site
  • Includes Pulumi IaC for Tailscale auth key/ACLs and Gandi DNS CNAME
  • Adds mise tasks (fly-deploy, fly-setup, fly-shutoff) and Forgejo CI workflow

Key details

  • Fly.io Firecracker VMs support TUN devices natively — no userspace networking needed
  • Tailscale auth key is preauthorized=True to avoid device approval hangs on container restarts
  • nginx caches aggressively for the static site; health check is on the default_server block
  • ACLs restrict tag:flyio-proxy to tag:k8s on port 443 only
  • DNS CNAME deployed and verified: docs.eblu.meblumeops-proxy.fly.dev

Test plan

  • curl -sf https://blumeops-proxy.fly.dev/healthz returns ok
  • curl -I -H "Host: docs.eblu.me" https://blumeops-proxy.fly.dev/ returns 200 with X-Cache-Status
  • curl -I https://docs.eblu.me/ returns 200 with valid Let's Encrypt cert
  • dig forge.ops.eblu.me still resolves to 100.98.163.89 (private services unaffected)
  • Set FLY_DEPLOY_TOKEN Forgejo Actions secret for CI auto-deploy

🤖 Generated with Claude Code

## Summary - Adds a Fly.io reverse proxy (`blumeops-proxy`) that tunnels public traffic to homelab services over Tailscale - First service exposed: `docs.eblu.me` — the Quartz static docs site - Includes Pulumi IaC for Tailscale auth key/ACLs and Gandi DNS CNAME - Adds mise tasks (`fly-deploy`, `fly-setup`, `fly-shutoff`) and Forgejo CI workflow ## Key details - Fly.io Firecracker VMs support TUN devices natively — no userspace networking needed - Tailscale auth key is `preauthorized=True` to avoid device approval hangs on container restarts - nginx caches aggressively for the static site; health check is on the default_server block - ACLs restrict `tag:flyio-proxy` to `tag:k8s` on port 443 only - DNS CNAME deployed and verified: `docs.eblu.me` → `blumeops-proxy.fly.dev` ## Test plan - [x] `curl -sf https://blumeops-proxy.fly.dev/healthz` returns `ok` - [x] `curl -I -H "Host: docs.eblu.me" https://blumeops-proxy.fly.dev/` returns 200 with `X-Cache-Status` - [x] `curl -I https://docs.eblu.me/` returns 200 with valid Let's Encrypt cert - [x] `dig forge.ops.eblu.me` still resolves to 100.98.163.89 (private services unaffected) - [x] Set `FLY_DEPLOY_TOKEN` Forgejo Actions secret for CI auto-deploy 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Introduces the fly/ directory with nginx + Tailscale container config,
Pulumi changes for Tailscale ACLs and auth key, DNS CNAME for
docs.eblu.me (staged but not yet deployed), mise tasks for deploy/setup/
shutoff, and Forgejo CI workflow for auto-deploy on push.

First target service: docs.eblu.me

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prevents "no stack selected" errors when running from a fresh
environment or after stack state is cleared.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Re-enabled devpi cache and regenerated lock files against it. Removed
uv.lock from tailscale .gitignore so locks are tracked. Mise tasks now
run uv sync before Pulumi and suggest 'devpi off' if sync fails (e.g.
during a power outage or devpi cache clear).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fly-setup now allocates shared IPv4 + IPv6 (both free for HTTP/HTTPS),
stages secrets with --stage to avoid unnecessary redeployments, and
selects the Pulumi stack explicitly. Updated docs with cost note for
dedicated IPv4.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolves multiple issues found during first deploy:
- Drop --tun=userspace-networking: Fly.io Firecracker VMs support TUN
  natively; userspace mode broke MagicDNS and Tailscale IP routing
- Add preauthorized=True to TailnetKey: required when tailnet has
  device approval enabled, otherwise containers hang on restart
- Move /healthz to default_server: Fly health checks send no Host
  header, so healthz must be on the catch-all server block
- Change region from sea (deprecated) to sjc
- Add iptables/ip6tables for TUN device support
- Add proxy_ssl_server_name for proper TLS SNI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove status line, update code examples to reflect lessons learned:
TUN networking (not userspace), iptables, healthz on default_server,
proxy_ssl_server_name, and preauthorized auth key.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace docs.ops.eblu.me with docs.eblu.me across all references
- Add Fly.io proxy reference card and operations how-to
- Move shutoff escalation levels to manage-flyio-proxy how-to
- Update index, Caddy, and docs reference cards with Fly.io context
- Update homepage link in docs ingress annotation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extends the forgejo_actions_secrets role to sync the Fly.io deploy
token from 1Password, enabling CI auto-deploy on push to fly/.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
eblume merged commit 64a78422b1 into main 2026-02-08 02:36:20 -08:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
eblume/blumeops!120
No description provided.