feat(deploy): self-hosted CI/CD + dedicated edge-proxy topology

Refactor the site container from a TLS-owning, port-binding proxy into an
internal plaintext static server that sits behind a dedicated edge reverse
proxy, and add an automated build/deploy pipeline.

- Caddyfile: single :80 listener (auto_https off), Host-matcher routing for
  apex/blog/art/code with /_astro+/assets re-root and www->apex redirect
- Dockerfile: EXPOSE 80 only (TLS now handled by the edge proxy)
- compose.yml: pull registry image, join external 'edge' network, declare
  routing via caddy-docker-proxy labels; drop build/ports/cert volumes
- compose.dev.yml: local container preview on :8080
- .forgejo/workflows/deploy.yml: push-to-main -> desktop runner builds, pushes
  to the Forgejo registry, SSH-deploys to the VPS
- docs + .dockerignore updated for the new topology

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Josh Bairstow 2026-06-02 15:31:16 +10:00
parent 5c9d4a99cc
commit 7ecd1b32ae
8 changed files with 261 additions and 97 deletions

View file

@ -45,24 +45,48 @@ npm run build # outputs static site to dist/
npm run preview # serve the build locally
```
## Deploy (containerised, target = Vultr Sydney VPS)
## Local container preview
The repo ships a Caddy-fronted container that serves the static build directly with automatic HTTPS.
`npm run dev` is the primary local loop. To eyeball the *built container* (real
Caddy + static output) without the production edge proxy:
```sh
docker compose build
docker compose up -d
docker compose -f compose.dev.yml up --build # http://localhost:8080
```
Caddy's site config (`Caddyfile`) maps host headers → path prefixes:
- `joshbairstow.com``/srv/`
`localhost:8080` falls through to the apex build; subdomain Host-matching only
fires for the real hostnames (use `npm run dev`, or hosts-file entries, to
exercise cross-subdomain routing locally).
## Deploy (CI/CD → Vultr VPS behind a shared edge proxy)
Deployment is automated. The site no longer terminates its own TLS — a
dedicated **caddy-docker-proxy** edge container (in the separate
[`jb-infra`](../jb-infra) repo) owns 80/443, does automatic HTTPS for every
host, and routes to this container over the shared `edge` Docker network using
the labels in [`compose.yml`](compose.yml). This container is now an internal
plaintext static server that still maps host → path prefix in its `Caddyfile`:
- `joshbairstow.com``/srv/` &nbsp;(`www.` → 301 apex)
- `blog.joshbairstow.com``/srv/blog/`
- `art.joshbairstow.com``/srv/art/`
- `code.joshbairstow.com``/srv/code/`
Asset paths (`/_astro/*` and `/assets/*`) are re-rooted back to `/srv` from the subdomain blocks so they resolve regardless of which subdomain served the HTML.
Shared assets (`/_astro/*`, `/assets/*`) are re-rooted back to `/srv` so they
resolve from any subdomain.
To add another in-repo subdomain later, add a directory under `src/pages/` and a matching block in the Caddyfile — that's it.
**Pipeline** ([`.forgejo/workflows/deploy.yml`](.forgejo/workflows/deploy.yml)):
push to `main` → the home-desktop Forgejo Actions runner builds the image,
pushes it to the Forgejo container registry (`git.joshbairstow.com`), then SSHes
to the VPS to `docker compose pull && up -d`. The VPS never builds.
See [`jb-infra/README.md`](../jb-infra/README.md) for the one-time bootstrap
(DNS, edge proxy + Forgejo, runner registration, secrets) and the required
Forgejo variables/secrets.
To add another in-repo subdomain: add a directory under `src/pages/`, a `@host`
matcher block in the `Caddyfile`, and the hostname to the `caddy` label in
`compose.yml`.
## Writing a blog post