# jb-infra VPS edge stack for `joshbairstow.com`: a **caddy-docker-proxy** edge reverse proxy (TLS + label-based routing) and a self-hosted **Forgejo** git server with CI and a container registry. Everything else — `jb-website` and future side projects — is deployed by CI and just attaches to the `edge` network and declares routing labels. ``` [home desktop] [Vultr VPS] Forgejo runner (host exec) docker network: edge (external) └ docker build ───push image──► ┌────────── edge-caddy :80/:443 (TLS, routing) └ ssh deploy ───pull+up─────► ├────────── forgejo git.joshbairstow.com (+registry) ├────────── jb-website internal :80 └────────── e.g. shop.joshbairstow.com ``` ## Why this shape - **Edge proxy decouples projects.** Each project ships its own container with `caddy` labels; the edge picks them up automatically. Adding a project needs **zero edits here** — no central config to touch. - **Build off-box.** The RAM-hungry Docker build runs on the always-on desktop runner; the VPS only pulls and runs prebuilt images, so it stays small. - **Self-hosted git/CI** (Forgejo) — independent of GitHub/GitLab, with a cloud push-mirror kept only as backup. --- ## Bootstrap (one-time, in order) Order matters — TLS issuance needs DNS + open ports first, and the `edge` network must exist before any stack references it. ### 1. DNS Point at the VPS public IP, and **wait for propagation** before step 4: - `A joshbairstow.com → ` - `A *.joshbairstow.com → ` (wildcard covers blog/art/code/git + future) - `AAAA` equivalents if the VPS has IPv6. A wildcard record means new subdomains resolve with no DNS change; certs are still issued per-host via HTTP-01, so no DNS-challenge plugin is required. ### 2. VPS base ```sh # Install Docker Engine + compose plugin (official convenience script or distro pkg) curl -fsSL https://get.docker.com | sh # Firewall: allow only what we serve. # 80, 443 -> edge proxy (HTTP/HTTPS, incl. ACME challenges) # 222 -> Forgejo git-over-SSH # 22 -> your normal host SSH (keep it!) ufw allow 80,443/tcp && ufw allow 222/tcp && ufw allow OpenSSH && ufw enable ``` RAM: prefer the **2 GB** Vultr tier. On 1 GB, add swap first: ```sh fallocate -l 2G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile echo '/swapfile none swap sw 0 0' >> /etc/fstab ``` ### 3. Create the shared network + bring up the stack ```sh docker network create edge # MUST exist before `up` git clone /opt/jb-infra && cd /opt/jb-infra docker compose up -d docker compose logs -f caddy # watch the first cert issuance ``` ### 4. Configure Forgejo Browse to `https://git.joshbairstow.com` (cert should be live): - Complete first-run setup; create your admin user with a **lowercase** username (the registry path is case-sensitive and must be lowercase). - Confirm Actions is enabled (`Site Administration → Actions`). - Create a **Personal Access Token** with **`write:package`** scope (`Settings → Applications`). The automatic Actions token CANNOT push packages. ### 5. Register the desktop runner On the **home desktop** (has Docker + ssh): ```sh # Get a registration token: Forgejo → Site Admin → Actions → Runners → Create forgejo-runner register --no-interactive \ --instance https://git.joshbairstow.com \ --token \ --name desktop --labels desktop:host forgejo-runner daemon --config /path/to/jb-infra/runner/config.yml ``` (For always-on, wrap the daemon in a systemd user service.) See [`runner/config.yml`](runner/config.yml). ### 6. Wire the jb-website repo In the `jb-website` repo settings on Forgejo (`Settings → Actions`): - **Variables:** `REGISTRY_OWNER` = your lowercase owner. - **Secrets:** `REGISTRY_USER`, `REGISTRY_TOKEN` (the PAT), `DEPLOY_HOST`, `DEPLOY_USER`, `DEPLOY_SSH_KEY`. On the **VPS**, prepare the deploy target: ```sh mkdir -p /opt/jb-website # copy jb-website/compose.yml here (the production one — pulls, no build) docker login git.joshbairstow.com # one-time, so `compose pull` can read the package # (or mark the jb-website package public in Forgejo to skip VPS login) ``` Create a restricted deploy user whose `authorized_keys` holds the public half of `DEPLOY_SSH_KEY`. ### 7. First deploy ```sh # in the jb-website repo (rename master→main first; see that repo's README) git remote add origin git@git.joshbairstow.com:222//jb-website.git git push -u origin main ``` The push triggers `.forgejo/workflows/deploy.yml`: the desktop builds + pushes the image, then SSHes to the VPS to `pull && up -d`. The edge issues certs for the apex + subdomains on first request. --- ## Adding another project later No changes to this repo. In the new project: 1. Ship a `compose.yml` on the VPS that joins the external `edge` network and carries labels, e.g.: ```yaml services: app: image: git.joshbairstow.com//:latest networks: [edge] labels: caddy: shop.joshbairstow.com caddy.reverse_proxy: "{{upstreams 3000}}" networks: edge: external: true ``` 2. Give it its own `.forgejo/workflows/deploy.yml` (copy jb-website's). 3. `*.joshbairstow.com` already resolves, so the edge issues its cert automatically on first hit. --- ## Maintenance / hardening - **Backups:** the `forgejo_data` volume is now the source of truth (repos, DB, registry blobs). Run `docker compose exec forgejo forgejo dump` on a schedule, and set a **push-mirror** on each repo to a cloud host (GitLab/GitHub) for off-box git backup. - **Registry GC:** per-commit `:sha` tags accumulate. Enable Forgejo's package cleanup rules / run periodic GC so the VPS disk doesn't fill. - **Certs:** never delete the `caddy_data` volume — losing it re-issues every cert and risks Let's Encrypt rate limits. - **Least privilege:** restrict the deploy key in `authorized_keys` with a forced `command=` that only runs the compose pull/up script. ```