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:
parent
5c9d4a99cc
commit
7ecd1b32ae
8 changed files with 261 additions and 97 deletions
|
|
@ -3,7 +3,10 @@ dist
|
||||||
.astro
|
.astro
|
||||||
.git
|
.git
|
||||||
.github
|
.github
|
||||||
|
.forgejo
|
||||||
docs/design-system
|
docs/design-system
|
||||||
|
compose.yml
|
||||||
|
compose.dev.yml
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
*.log
|
*.log
|
||||||
|
|
|
||||||
64
.forgejo/workflows/deploy.yml
Normal file
64
.forgejo/workflows/deploy.yml
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# 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
|
||||||
154
Caddyfile
154
Caddyfile
|
|
@ -1,89 +1,99 @@
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
# jb-website — Caddy site config.
|
# jb-website — INTERNAL static server (behind the edge reverse proxy).
|
||||||
#
|
#
|
||||||
# Routing model (see docs/planning.md §6.2):
|
# This Caddy no longer terminates TLS or owns 80/443. The edge proxy
|
||||||
# joshbairstow.com -> /srv/* (apex / home)
|
# (caddy-docker-proxy, see the infra repo) terminates HTTPS for every host and
|
||||||
# blog.joshbairstow.com -> /srv/blog/* (writing archive + posts)
|
# reverse-proxies plaintext to this container on :80, preserving the original
|
||||||
# art.joshbairstow.com -> /srv/art/* (generative art focus view)
|
# Host header. We branch on that Host header to serve the right slice of the
|
||||||
# code.joshbairstow.com -> /srv/code/* (experiments & tools)
|
# single static build:
|
||||||
#
|
#
|
||||||
# Each block points at the same /srv directory but uses `root` with the path
|
# joshbairstow.com / www. -> /srv (apex / home)
|
||||||
# prefix appropriate to the subdomain. Caddy handles HTTPS via the automatic
|
# blog.joshbairstow.com -> /srv/blog (writing archive + posts)
|
||||||
# ACME / Let's Encrypt flow at the email address below.
|
# art.joshbairstow.com -> /srv/art (generative art focus view)
|
||||||
|
# code.joshbairstow.com -> /srv/code (experiments & tools)
|
||||||
#
|
#
|
||||||
# Local dev: see the `localhost` block at the bottom — Caddy listens on :8080
|
# Shared build assets (/_astro/*, /assets/*) live at the apex root, so the
|
||||||
# with no TLS so `docker compose up` works out of the box on a laptop.
|
# 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.
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
{
|
{
|
||||||
# Replace with the address you want ACME-challenge contact / cert-expiry mail at.
|
# Edge proxy owns TLS; this server is plaintext-only and has no admin needs.
|
||||||
email josh@joshbairstow.com
|
auto_https off
|
||||||
|
admin off
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---- apex ------------------------------------------------------------------
|
:80 {
|
||||||
joshbairstow.com, www.joshbairstow.com {
|
|
||||||
root * /srv
|
|
||||||
encode zstd gzip
|
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
|
@www host www.joshbairstow.com
|
||||||
redir @www https://joshbairstow.com{uri} permanent
|
redir @www https://joshbairstow.com{uri} permanent
|
||||||
}
|
|
||||||
|
|
||||||
# ---- blog ------------------------------------------------------------------
|
# ---- apex --------------------------------------------------------------
|
||||||
blog.joshbairstow.com {
|
@apex host joshbairstow.com
|
||||||
root * /srv/blog
|
handle @apex {
|
||||||
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
|
root * /srv
|
||||||
file_server
|
|
||||||
}
|
|
||||||
handle {
|
|
||||||
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
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,5 +20,6 @@ FROM caddy:2-alpine
|
||||||
COPY Caddyfile /etc/caddy/Caddyfile
|
COPY Caddyfile /etc/caddy/Caddyfile
|
||||||
COPY --from=build /app/dist /srv
|
COPY --from=build /app/dist /srv
|
||||||
|
|
||||||
# Caddy listens on 80/443; the host's reverse proxy or docker compose maps these.
|
# Internal plaintext server only — the edge reverse proxy terminates TLS and
|
||||||
EXPOSE 80 443
|
# forwards to this port. No 443 here.
|
||||||
|
EXPOSE 80
|
||||||
|
|
|
||||||
40
README.md
40
README.md
|
|
@ -45,24 +45,48 @@ npm run build # outputs static site to dist/
|
||||||
npm run preview # serve the build locally
|
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
|
```sh
|
||||||
docker compose build
|
docker compose -f compose.dev.yml up --build # http://localhost:8080
|
||||||
docker compose up -d
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Caddy's site config (`Caddyfile`) maps host headers → path prefixes:
|
`localhost:8080` falls through to the apex build; subdomain Host-matching only
|
||||||
- `joshbairstow.com` → `/srv/`
|
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)
|
||||||
- `blog.joshbairstow.com` → `/srv/blog/`
|
- `blog.joshbairstow.com` → `/srv/blog/`
|
||||||
- `art.joshbairstow.com` → `/srv/art/`
|
- `art.joshbairstow.com` → `/srv/art/`
|
||||||
- `code.joshbairstow.com` → `/srv/code/`
|
- `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
|
## Writing a blog post
|
||||||
|
|
||||||
|
|
|
||||||
25
compose.dev.yml
Normal file
25
compose.dev.yml
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# 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"
|
||||||
43
compose.yml
43
compose.yml
|
|
@ -1,26 +1,35 @@
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
# jb-website — production compose file.
|
# jb-website — PRODUCTION compose (lives on the VPS at /opt/jb-website/).
|
||||||
#
|
#
|
||||||
# Runs the Caddy-fronted static site. Caddy handles HTTPS via the automatic
|
# This file does NOT build. The image is built on the home-desktop CI runner
|
||||||
# ACME flow at the email configured in Caddyfile. Side-hustle container
|
# and pushed to the Forgejo container registry; the VPS only pulls and runs it.
|
||||||
# services (signage, keycaps, etc.) would slot in alongside this one — Caddy
|
# The deploy step (see .forgejo/workflows/deploy.yml) does:
|
||||||
# in this container can proxy to them, or you can put a dedicated Caddy in
|
#
|
||||||
# front of all containers.
|
# TAG=<commit-sha> docker compose -f /opt/jb-website/compose.yml pull
|
||||||
|
# TAG=<commit-sha> 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).
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
services:
|
services:
|
||||||
web:
|
web:
|
||||||
build: .
|
# REGISTRY_OWNER must be the lowercase Forgejo owner (user/org). TAG is the
|
||||||
image: jb-website:latest
|
# commit sha set by the deploy step; falls back to latest for manual `up`.
|
||||||
|
image: git.joshbairstow.com/${REGISTRY_OWNER:-josh}/jb-website:${TAG:-latest}
|
||||||
container_name: jb-website
|
container_name: jb-website
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
networks:
|
||||||
- "80:80"
|
- edge
|
||||||
- "443:443"
|
labels:
|
||||||
volumes:
|
caddy: joshbairstow.com, www.joshbairstow.com, blog.joshbairstow.com, art.joshbairstow.com, code.joshbairstow.com
|
||||||
- caddy_data:/data
|
caddy.reverse_proxy: "{{upstreams 80}}"
|
||||||
- caddy_config:/config
|
|
||||||
|
|
||||||
volumes:
|
networks:
|
||||||
caddy_data:
|
edge:
|
||||||
caddy_config:
|
external: true
|
||||||
|
|
|
||||||
|
|
@ -170,9 +170,8 @@ Recommendation for a project of this size:
|
||||||
**Suggested stack on the VPS:**
|
**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.
|
- **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.
|
- **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** options:
|
- **Deployment** (decided 2026-06-02 — see §6.5):
|
||||||
- Build locally → push image to a registry (GHCR or Docker Hub) → SSH to VPS → `compose pull && up -d`. Fully manual, very transparent.
|
- 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).
|
||||||
- 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
|
### 6.4 Other open questions
|
||||||
|
|
||||||
|
|
@ -182,6 +181,32 @@ Recommendation for a project of this size:
|
||||||
- **`code.` subdomain content:** ~~is this also served from this repo~~ — confirmed in-repo (2026-06-01).
|
- **`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/`.
|
- **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
|
## 7. Decisions log
|
||||||
|
|
||||||
| Date | Decision | Rationale |
|
| Date | Decision | Rationale |
|
||||||
|
|
@ -196,6 +221,9 @@ Recommendation for a project of this size:
|
||||||
| 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 | `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 | 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-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
|
## 8. Next steps
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue