A Github action for deploying PaaS on Edge / Home PCs
A batteries-included GitHub repo to bootstrap and operate a CapRover cluster on Ubuntu 24.04 with:
- Cloudflare Tunnel (all apps tunneled by default)
- Optional public IPv6 + Let’s Encrypt (direct AAAA)
- Docker IPv6 with autogenerated ULA on all nodes
- Tailscale (optional, exit-node capable)
- GPU/TPU prep (NVIDIA, AMD, Coral) + node labels and placement rules
- Turnkey Immich deployment (quota + extra mounts + GPU ML placement)
- Per-app IPv6 network (ipvlan) and tunnel deny/allow control
- Build on GitHub, Deploy from self-hosted to avoid tunnel upload limits
- .env secrets masking during env sync
- Rollout/Rollback workflow to pin images and revert quickly
- Auto subdomain generation in CI (
SUBDOMAIN_PREFIX→prefix.apps.<root>), with input validation
.github/workflows/
├─ caprover-bootstrap.yml # Manager bootstrap (tunneled + optional IPv6/LE)
├─ caprover-node-join.yml # Join nodes: IPv6, GPU/TPU, Tailscale, auto-label
├─ caprover-node-labels.yml # Manual labels
├─ immich-deploy.yml # Immich deploy (quota + mounts + GPU placement)
├─ caprover-app-placement-patch.yml # Placement constraints (+ optional ipv6-public)
├─ cloudflared-tunnel-toggle.yml # Per-FQDN tunnel deny/allow
├─ ipv6-pool-setup.yml # Create ipv6-public (ipvlan) + ndppd
├─ app-build-and-deploy.yml # Build on GitHub, deploy on self-hosted (auto subdomains)
├─ caprover-app-init-and-env.yml # App init + healthcheck + env sync (denylist masking)
└─ caprover-rollout-rollback.yml # Pin image (rollout) & revert (rollback)
apps/apex-welcome/
├─ Dockerfile
├─ index.html
└─ captain-definition
PROJECT_DESCRIPTION_FOR_LLM.md
README.md
-
Secrets (Repo → Settings → Secrets and variables → Actions)
- Required:
CAPROVER_PASSWORD,CF_TUNNEL_TOKEN - Optional:
TAILSCALE_AUTHKEY,MANAGER_SSH_KEY,SSH_KNOWN_HOSTS - For non‑GHCR registries:
REGISTRY_USERNAME,REGISTRY_PASSWORD - Convenience:
ROOT_DOMAIN,APPS_WILDCARD_SUBDOMAIN(defaultapps),CAPTAIN_SUBDOMAIN(defaultcaptain)
- Required:
-
Bootstrap manager — run
caprover-bootstrap.yml- Provisions CapRover and Cloudflared
- Serves
captain.<root>and*.apps.<root>through the tunnel by default - Optional: set
EXPOSE_PUBLIC_IPV6=trueto allow direct IPv6 + Let’s Encrypt
-
Join workers — run
caprover-node-join.ymlper nodeGPU_TYPE+APPLY_GPU_PREP=trueif needed- Nodes auto‑labeled (e.g.,
gpu=nvidia)
These steps are one‑time per zone if you want any subdomain under *.apps.<root> to route via your tunnel.
- Add CNAME:
- Name:
*.apps - Target:
<TUNNEL_UUID>.cfargotunnel.com - Proxy: Proxied (orange cloud)
- Name:
You can also automate via
cloudflared:cloudflared tunnel route dns <tunnel-name> *.apps.yourdomain.com
/etc/cloudflared/config.yml should include:
ingress:
- hostname: captain.yourdomain.com
service: http://127.0.0.1:80
originRequest: { httpHostHeader: captain.yourdomain.com }
- hostname: "*.apps.yourdomain.com"
service: http://127.0.0.1:80
- service: http_status:404Reload:
sudo systemctl restart cloudflared- Nothing special: CapRover matches
Hostheader and routes to the app. - In UI (or CI), add Custom Domain for the app (e.g.,
rahul.apps.yourdomain.com).
If you want certain apps to skip the tunnel:
-
In DNS, add an AAAA:
- Name:
*.apps(or a specific subdomain) - IPv6: your server’s global IPv6
- Proxy: DNS only (gray cloud)
- Name:
-
Ensure
EXPOSE_PUBLIC_IPV6=true(bootstrap), so port 80/443 v6 are open and Let’s Encrypt is enabled. -
To force a given FQDN away from the tunnel, run
cloudflared-tunnel-toggle.ymlwithALLOW_TUNNEL=falsefor that hostname.
Use caprover-app-init-and-env.yml to create the app, set health checks, and sync .env from another repo.
- Denylist masking (skipped keys):
SECRET,_SECRET$,TOKEN,PASSWORD,PASS$,API_KEY,PRIVATE,CERT,AWS_,GCP_,AZURE_
Use app-build-and-deploy.yml:
-
Inputs you care about
APP_NAMEREPO_URL,REPO_BRANCH,DOCKERFILE,CONTEXTIMAGE_REGISTRY(defaultghcr.io),IMAGE_NAMESPACE,IMAGE_NAME,IMAGE_TAGSUBDOMAIN_PREFIXorCAPROVER_CUSTOM_DOMAIN
-
Auto subdomain
- If
CAPROVER_CUSTOM_DOMAINis empty and you passSUBDOMAIN_PREFIX, CI computes
SUBDOMAIN_PREFIX + "." + (APPS_WILDCARD_SUBDOMAIN or "apps") + "." + ROOT_DOMAIN - Validation: prefix must match
^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$(lowercase, digits, hyphens; 1–63 chars; no leading/trailing hyphen)
- If
-
What happens
- GitHub‑hosted runners build + push the image (multi‑arch supported)
- Self‑hosted runner updates CapRover app to the image ref (origin pulls directly → avoids Cloudflare’s 100MB upload limit)
Use caprover-app-placement-patch.yml to pin services to labeled nodes (e.g., gpu=nvidia), and optionally attach ipv6-public + fixed IPv6s.
Use caprover-rollout-rollback.yml:
- Rollout: saves current image to
PREV_IMAGE(changeable) and pins toIMAGE_REF. - Rollback: restores from
PREV_IMAGE.
- DNS Pre-req: wildcard CNAME for
*.appsto the tunnel is a one‑time setup. - TLS at the Edge: with tunnel, Cloudflare terminates TLS. CapRover origin TLS (LE) applies to direct IPv6 and optional internal policies.
- Rate Limits: Cloudflare has per‑request and bandwidth rules; container image pulls go direct from CapRover to your registry (not via the tunnel).
- Let’s Encrypt Challenges: for direct IPv6, ensure ports 80/443 v6 are reachable; for tunneled hosts, Cloudflare handles edge certs.
- Naming: subdomain prefixes must be DNS‑safe and unique. The validation step enforces safe characters/length.
- Rollbacks: the workflow stores the previous image tag in an env var (
PREV_IMAGE). Rotate or snapshot as needed for more slots.
- Quota volume via
IMMICH_STORAGE_GB - Extra mounts via
IMMICH_EXTRA_MOUNTS - ML service constrained to GPU nodes with device reservation
- Tunnel:
systemctl status cloudflared, inspect/etc/cloudflared/config.yml - IPv6:
ip -6 addrshows global v6; for per‑app v6, ensure routed/64+ndppd - GPU: NVIDIA requires
nvidia-container-toolkit; AMD/Coral prep included - CapRover: workflows are idempotent—re‑run safely