diff --git a/.dockerignore b/.dockerignore index 65e5f5c..e5c3dda 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,10 +3,7 @@ dist .astro .git .github -.forgejo docs/design-system -compose.yml -compose.dev.yml .env .env.* *.log diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml deleted file mode 100644 index dc8bf8b..0000000 --- a/.forgejo/workflows/deploy.yml +++ /dev/null @@ -1,64 +0,0 @@ -# ---------------------------------------------------------------------------- -# Build + deploy jb-website. -# -# Runs on the home-desktop host runner (label `desktop`): builds the image, -# pushes it to the Forgejo container registry, then SSHes to the VPS to pull -# and restart. The VPS never builds — it only runs the prebuilt image. -# -# Required Forgejo settings (Repo > Settings > Actions): -# Variables: -# REGISTRY_OWNER lowercase Forgejo user/org that owns the package -# Secrets: -# REGISTRY_USER Forgejo username for `docker login` -# REGISTRY_TOKEN Forgejo PAT with write:package scope -# DEPLOY_HOST VPS host/IP -# DEPLOY_USER SSH user on the VPS (deploy account) -# DEPLOY_SSH_KEY private key whose public half is in the VPS authorized_keys -# ---------------------------------------------------------------------------- - -name: deploy - -on: - push: - branches: [main] - -env: - REGISTRY: git.joshbairstow.com - IMAGE_NAME: jb-website - -jobs: - build-and-deploy: - runs-on: desktop - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Resolve image reference - run: | - IMAGE="${REGISTRY}/${{ vars.REGISTRY_OWNER }}/${IMAGE_NAME}" - echo "IMAGE=${IMAGE}" >> "$GITHUB_ENV" - echo "Building ${IMAGE}:${{ github.sha }}" - - - name: Log in to the Forgejo registry - run: | - echo "${{ secrets.REGISTRY_TOKEN }}" \ - | docker login "${REGISTRY}" -u "${{ secrets.REGISTRY_USER }}" --password-stdin - - - name: Build and push image - run: | - docker build -t "${IMAGE}:${{ github.sha }}" -t "${IMAGE}:latest" . - docker push "${IMAGE}:${{ github.sha }}" - docker push "${IMAGE}:latest" - - - name: Deploy to VPS over SSH - run: | - install -m 600 /dev/null deploy_key - printf '%s\n' "${{ secrets.DEPLOY_SSH_KEY }}" > deploy_key - ssh -i deploy_key -o StrictHostKeyChecking=accept-new \ - "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" \ - "export REGISTRY_OWNER='${{ vars.REGISTRY_OWNER }}' TAG='${{ github.sha }}'; \ - cd /opt/jb-website && \ - docker compose -f compose.yml pull && \ - docker compose -f compose.yml up -d && \ - docker image prune -f" - rm -f deploy_key diff --git a/Caddyfile b/Caddyfile index 5ef20b5..6e51226 100644 --- a/Caddyfile +++ b/Caddyfile @@ -1,99 +1,89 @@ # ---------------------------------------------------------------------------- -# jb-website — INTERNAL static server (behind the edge reverse proxy). +# jb-website — Caddy site config. # -# This Caddy no longer terminates TLS or owns 80/443. The edge proxy -# (caddy-docker-proxy, see the infra repo) terminates HTTPS for every host and -# reverse-proxies plaintext to this container on :80, preserving the original -# Host header. We branch on that Host header to serve the right slice of the -# single static build: +# Routing model (see docs/planning.md §6.2): +# joshbairstow.com -> /srv/* (apex / home) +# blog.joshbairstow.com -> /srv/blog/* (writing archive + posts) +# art.joshbairstow.com -> /srv/art/* (generative art focus view) +# code.joshbairstow.com -> /srv/code/* (experiments & tools) # -# joshbairstow.com / www. -> /srv (apex / home) -# blog.joshbairstow.com -> /srv/blog (writing archive + posts) -# art.joshbairstow.com -> /srv/art (generative art focus view) -# code.joshbairstow.com -> /srv/code (experiments & tools) +# Each block points at the same /srv directory but uses `root` with the path +# prefix appropriate to the subdomain. Caddy handles HTTPS via the automatic +# ACME / Let's Encrypt flow at the email address below. # -# Shared build assets (/_astro/*, /assets/*) live at the apex root, so the -# subdomain handlers re-root those paths back to /srv. -# -# One bare `:80` listener + host matchers (NOT named-host site blocks) keeps -# this purely plaintext: named-host blocks would re-trigger Caddy's automatic -# HTTPS, which the edge proxy already handles. Local preview is provided by -# compose.dev.yml mapping a host port to this :80. +# Local dev: see the `localhost` block at the bottom — Caddy listens on :8080 +# with no TLS so `docker compose up` works out of the box on a laptop. # ---------------------------------------------------------------------------- { - # Edge proxy owns TLS; this server is plaintext-only and has no admin needs. - auto_https off - admin off + # Replace with the address you want ACME-challenge contact / cert-expiry mail at. + email josh@joshbairstow.com } -:80 { +# ---- apex ------------------------------------------------------------------ +joshbairstow.com, www.joshbairstow.com { + root * /srv encode zstd gzip + file_server + try_files {path} {path}/index.html /404.html - # ---- www -> apex redirect ---------------------------------------------- - # The upstream only ever sees http, so the scheme is hardcoded https. @www host www.joshbairstow.com redir @www https://joshbairstow.com{uri} permanent +} - # ---- apex -------------------------------------------------------------- - @apex host joshbairstow.com - handle @apex { +# ---- blog ------------------------------------------------------------------ +blog.joshbairstow.com { + root * /srv/blog + encode zstd gzip + # Assets and the canvas-island JS live at /_astro and /assets on the apex build. + # Re-route them from the subdomain root by falling through to the parent /srv. + @asset path /_astro/* /assets/* + handle @asset { root * /srv - try_files {path} {path}/index.html /404.html file_server } - - # ---- blog -------------------------------------------------------------- - @blog host blog.joshbairstow.com - handle @blog { - @asset path /_astro/* /assets/* - handle @asset { - root * /srv - file_server - } - handle { - root * /srv/blog - try_files {path} {path}/index.html /srv/404.html - file_server - } - } - - # ---- art --------------------------------------------------------------- - @art host art.joshbairstow.com - handle @art { - @asset path /_astro/* /assets/* - handle @asset { - root * /srv - file_server - } - handle { - root * /srv/art - try_files {path} {path}/index.html /srv/404.html - file_server - } - } - - # ---- code -------------------------------------------------------------- - @code host code.joshbairstow.com - handle @code { - @asset path /_astro/* /assets/* - handle @asset { - root * /srv - file_server - } - handle { - root * /srv/code - try_files {path} {path}/index.html /srv/404.html - file_server - } - } - - # ---- fallback ---------------------------------------------------------- - # Any other Host (e.g. hitting the container directly, or local preview at - # localhost:8080) serves the apex build so the site is never blank. handle { - root * /srv - try_files {path} {path}/index.html /404.html file_server + try_files {path} {path}/index.html /srv/404.html } } + +# ---- art ------------------------------------------------------------------- +art.joshbairstow.com { + root * /srv/art + encode zstd gzip + @asset path /_astro/* /assets/* + handle @asset { + root * /srv + file_server + } + handle { + file_server + try_files {path} {path}/index.html /srv/404.html + } +} + +# ---- code ------------------------------------------------------------------ +code.joshbairstow.com { + root * /srv/code + encode zstd gzip + @asset path /_astro/* /assets/* + handle @asset { + root * /srv + file_server + } + handle { + file_server + try_files {path} {path}/index.html /srv/404.html + } +} + +# ---- local dev (no TLS) ---------------------------------------------------- +# Hit http://localhost:8080 to preview the built apex; the subdomain rewrites +# above only fire when accessed by their real hostnames. +:8080 { + root * /srv + encode zstd gzip + file_server + try_files {path} {path}/index.html /404.html +} diff --git a/Dockerfile b/Dockerfile index 15a6456..6117b22 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,6 +20,5 @@ FROM caddy:2-alpine COPY Caddyfile /etc/caddy/Caddyfile COPY --from=build /app/dist /srv -# Internal plaintext server only — the edge reverse proxy terminates TLS and -# forwards to this port. No 443 here. -EXPOSE 80 +# Caddy listens on 80/443; the host's reverse proxy or docker compose maps these. +EXPOSE 80 443 diff --git a/README.md b/README.md index e5bf4e3..e755e8c 100644 --- a/README.md +++ b/README.md @@ -45,48 +45,24 @@ npm run build # outputs static site to dist/ npm run preview # serve the build locally ``` -## Local container preview +## Deploy (containerised, target = Vultr Sydney VPS) -`npm run dev` is the primary local loop. To eyeball the *built container* (real -Caddy + static output) without the production edge proxy: +The repo ships a Caddy-fronted container that serves the static build directly with automatic HTTPS. ```sh -docker compose -f compose.dev.yml up --build # http://localhost:8080 +docker compose build +docker compose up -d ``` -`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/`  (`www.` → 301 apex) +Caddy's site config (`Caddyfile`) maps host headers → path prefixes: +- `joshbairstow.com` → `/srv/` - `blog.joshbairstow.com` → `/srv/blog/` - `art.joshbairstow.com` → `/srv/art/` - `code.joshbairstow.com` → `/srv/code/` -Shared assets (`/_astro/*`, `/assets/*`) are re-rooted back to `/srv` so they -resolve from any subdomain. +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. -**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`. +To add another in-repo subdomain later, add a directory under `src/pages/` and a matching block in the Caddyfile — that's it. ## Writing a blog post diff --git a/compose.dev.yml b/compose.dev.yml deleted file mode 100644 index d4e6ac9..0000000 --- a/compose.dev.yml +++ /dev/null @@ -1,25 +0,0 @@ -# ---------------------------------------------------------------------------- -# jb-website — LOCAL preview of the built container. -# -# The production compose.yml pulls a registry image, joins the external `edge` -# network, and publishes no ports — none of which works on a laptop. This file -# builds the image locally and maps a host port straight to the internal :80 so -# you can eyeball the real Caddy/static output without the edge proxy. -# -# docker compose -f compose.dev.yml up --build -# -> http://localhost:8080 -# -# Subdomain Host-matching only fires for the real hostnames, so localhost:8080 -# falls through to the apex build (see the fallback handler in Caddyfile). To -# exercise subdomain routing locally, add hosts-file entries pointing the real -# names at 127.0.0.1, or just use `npm run dev` (the urls.ts helpers collapse -# cross-subdomain links to relative paths in dev). -# ---------------------------------------------------------------------------- - -services: - web: - build: . - image: jb-website:dev - container_name: jb-website-dev - ports: - - "8080:80" diff --git a/compose.yml b/compose.yml index db063dc..8e548ad 100644 --- a/compose.yml +++ b/compose.yml @@ -1,35 +1,26 @@ # ---------------------------------------------------------------------------- -# jb-website — PRODUCTION compose (lives on the VPS at /opt/jb-website/). +# jb-website — production compose file. # -# This file does NOT build. The image is built on the home-desktop CI runner -# and pushed to the Forgejo container registry; the VPS only pulls and runs it. -# The deploy step (see .forgejo/workflows/deploy.yml) does: -# -# TAG= docker compose -f /opt/jb-website/compose.yml pull -# TAG= docker compose -f /opt/jb-website/compose.yml up -d -# -# Routing + TLS are handled by the edge proxy (caddy-docker-proxy, infra repo). -# This service is internal-only: no published ports, no certs. It joins the -# shared external `edge` network and declares its hostnames via labels; the edge -# proxy discovers them and reverse-proxies (plaintext, Host preserved) to :80. -# -# For local preview use compose.dev.yml instead (this file won't run off-VPS -# because the `edge` network is external). +# Runs the Caddy-fronted static site. Caddy handles HTTPS via the automatic +# ACME flow at the email configured in Caddyfile. Side-hustle container +# services (signage, keycaps, etc.) would slot in alongside this one — Caddy +# in this container can proxy to them, or you can put a dedicated Caddy in +# front of all containers. # ---------------------------------------------------------------------------- services: web: - # REGISTRY_OWNER must be the lowercase Forgejo owner (user/org). TAG is the - # commit sha set by the deploy step; falls back to latest for manual `up`. - image: git.joshbairstow.com/${REGISTRY_OWNER:-joshbairstow}/jb-website:${TAG:-latest} + build: . + image: jb-website:latest container_name: jb-website restart: unless-stopped - networks: - - edge - labels: - caddy: joshbairstow.com, www.joshbairstow.com, blog.joshbairstow.com, art.joshbairstow.com, code.joshbairstow.com - caddy.reverse_proxy: "{{upstreams 80}}" + ports: + - "80:80" + - "443:443" + volumes: + - caddy_data:/data + - caddy_config:/config -networks: - edge: - external: true +volumes: + caddy_data: + caddy_config: diff --git a/docs/planning.md b/docs/planning.md index ae82d1c..ff6c79b 100644 --- a/docs/planning.md +++ b/docs/planning.md @@ -170,8 +170,9 @@ Recommendation for a project of this size: **Suggested stack on the VPS:** - **Caddy** as the reverse proxy — automatic HTTPS via Let's Encrypt, dead-simple Caddyfile, handles the host-header → path-prefix routing cleanly. - **Docker + docker compose** — the Astro static output baked into a tiny container (Caddy + the `dist/` files, or just nginx-alpine + static files); side-hustle containers added alongside under the same compose file. -- **Deployment** (decided 2026-06-02 — see §6.5): - - Self-hosted **Forgejo** on the VPS (`git.joshbairstow.com`) hosts the repo + CI + a container registry; a **Forgejo Actions runner on the home desktop** builds the image and SSH-deploys to the VPS. Earlier GHCR/GitHub-Actions options were dropped in favour of self-hosting (learning value + avoids GitHub). +- **Deployment** options: + - Build locally → push image to a registry (GHCR or Docker Hub) → SSH to VPS → `compose pull && up -d`. Fully manual, very transparent. + - Or GitHub Actions: build + push image on tag, then a webhook or SSH step triggers the VPS pull. Slightly more setup, far less ceremony per deploy. Recommended once the workflow is steady. ### 6.4 Other open questions @@ -181,32 +182,6 @@ Recommendation for a project of this size: - **`code.` subdomain content:** ~~is this also served from this repo~~ — confirmed in-repo (2026-06-01). - **Design system docs source:** ~~URL 404'd~~ — imported successfully (2026-06-01); see `docs/design-system/`. -### 6.5 CI/CD + multi-project topology (decided 2026-06-02) - -The site is one project among several that will eventually share the VPS via -subdomains (separate repos/containers each). To keep their lifecycles -independent, infrastructure splits in two: - -- **`jb-infra` repo (bootstrapped manually on the VPS):** a dedicated - **caddy-docker-proxy** edge container owns 80/443, terminates TLS for all - hosts, and routes by **Docker labels** over a shared external `edge` network; - plus **Forgejo** (`git.joshbairstow.com`) for self-hosted git + Actions + a - container registry. -- **This repo:** refactored to an *internal* plaintext static server (drops - TLS/ACME/port-binding; keeps host→path + asset re-root in `Caddyfile`), joins - the `edge` network, and declares its hostnames via `caddy` labels in - `compose.yml`. CI lives in `.forgejo/workflows/deploy.yml`. - -**Flow:** push to `main` → home-desktop runner builds the image → pushes to the -Forgejo registry → SSHes to the VPS → `compose pull && up -d`. The small VPS -never builds (build offloaded to the always-on desktop). New projects plug in by -joining `edge` + adding labels — no central config edits. - -Rationale: self-hosting Forgejo is a deliberate learning investment and avoids -GitHub; the label-driven edge proxy scales to many independent repos cleanly; -offloading builds keeps the VPS on the cheap tier. RAM target bumped to 2 GB (or -1 GB + swap). See `jb-infra/README.md` for the full bootstrap runbook. - ## 7. Decisions log | Date | Decision | Rationale | @@ -221,9 +196,6 @@ offloading builds keeps the VPS on the cheap tier. RAM target bumped to 2 GB (or | 2026-06-01 | `code.` subdomain is in-repo (third in-repo section alongside `blog.` and `art.`) | User confirmed; routing model already supports it | | 2026-06-01 | Architecture must remain open to adding further in-repo subdomain paths later | User flagged this as a constraint; host-aware routing in §6.2 scales to N paths without rework | | 2026-06-01 | Brand-system docs imported to `docs/design-system/` from a fresh handoff bundle | Establishes the canonical brand spec in-repo so future work can cite it | -| 2026-06-02 | Self-host git + CI via Forgejo on the VPS (`git.joshbairstow.com`); runner on the home desktop | Learning value + avoids GitHub availability issues; offloads builds from the small VPS | -| 2026-06-02 | Dedicated `caddy-docker-proxy` edge container owns TLS/routing via Docker labels; website becomes an internal server | Decouples each project's deploy; new subdomains/containers plug in with zero central edits | -| 2026-06-02 | Infra (edge proxy + Forgejo) split into a separate `jb-infra` repo, bootstrapped manually | Infra changes rarely; keeps the app repo's CI focused on the app | ## 8. Next steps