No description
Find a file
Josh Bairstow 76df80a26c chore: initial VPS edge stack (caddy-docker-proxy + Forgejo)
Edge reverse proxy (TLS + label routing) and self-hosted Forgejo (git +
Actions + container registry) for joshbairstow.com, plus the desktop runner
config and the one-time bootstrap runbook.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 15:31:25 +10:00
runner chore: initial VPS edge stack (caddy-docker-proxy + Forgejo) 2026-06-02 15:31:25 +10:00
.gitignore chore: initial VPS edge stack (caddy-docker-proxy + Forgejo) 2026-06-02 15:31:25 +10:00
compose.yml chore: initial VPS edge stack (caddy-docker-proxy + Forgejo) 2026-06-02 15:31:25 +10:00
README.md chore: initial VPS edge stack (caddy-docker-proxy + Forgejo) 2026-06-02 15:31:25 +10:00

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
                                     └────────── <future apps> 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 → <VPS_IP>
  • A *.joshbairstow.com → <VPS_IP> (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

# 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:

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

docker network create edge          # MUST exist before `up`
git clone <this repo> /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):

# Get a registration token: Forgejo → Site Admin → Actions → Runners → Create
forgejo-runner register --no-interactive \
  --instance https://git.joshbairstow.com \
  --token <REGISTRATION_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.

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:

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

# in the jb-website repo (rename master→main first; see that repo's README)
git remote add origin git@git.joshbairstow.com:222/<owner>/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.:
    services:
      app:
        image: git.joshbairstow.com/<owner>/<app>: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.