Route Forgejo through beta's existing Caddy instead of Traefik (#244)
beta (Forgejo's pinned node) is also today's live thermograph.org host, with Caddy already bound to ports 80/443. A second reverse proxy binding those same ports would collide with it. Drop Traefik entirely: forgejo's web port now publishes to 127.0.0.1:3080 only, and a new Caddy site block (deploy/forgejo/caddy-git.conf) reverse-proxies git.thermograph.org to it, reusing Caddy's existing automatic-HTTPS instead of a second ACME flow. That same Caddy block resolves the registry-exposure question (hazard #15 in the hop1 runbook) as its first listed option: /v2/* (the registry API) is blocked to everything outside the WireGuard mesh CIDR, while the git/web UI stays public. README documents the /etc/hosts override mesh clients need so registry traffic actually routes over the tunnel rather than the public IP.
This commit is contained in:
parent
7ff3873661
commit
441af55084
3 changed files with 106 additions and 69 deletions
|
|
@ -37,6 +37,50 @@ docker stack deploy -c deploy/forgejo/docker-stack.yml forgejo
|
||||||
```
|
```
|
||||||
|
|
||||||
Re-running is safe — Swarm only touches services whose spec actually changed.
|
Re-running is safe — Swarm only touches services whose spec actually changed.
|
||||||
|
Do this **before** the DNS + Caddy step below — Caddy's reverse_proxy target
|
||||||
|
(`127.0.0.1:3080`) needs the `forgejo` service actually listening first, or
|
||||||
|
its first health check just fails harmlessly until it is.
|
||||||
|
|
||||||
|
## DNS + TLS: reusing beta's existing Caddy, not a second reverse proxy
|
||||||
|
|
||||||
|
Forgejo is pinned to beta (`role=forge`) — but beta is also **today's live
|
||||||
|
thermograph.org host**, and its Caddy already owns ports 80/443
|
||||||
|
(`/etc/caddy/Caddyfile` on that box). A second ingress (Traefik) trying to
|
||||||
|
bind the same ports would collide with it. So there's no Traefik in this
|
||||||
|
stack: `forgejo`'s web port publishes to `127.0.0.1:3080` only (host-local),
|
||||||
|
and beta's *existing* Caddy gets one more site block reverse-proxying to it —
|
||||||
|
same pattern as its `thermograph.org` block, same automatic-HTTPS.
|
||||||
|
|
||||||
|
1. Point the Forgejo domain (default `git.thermograph.org`; override with
|
||||||
|
`FORGEJO_DOMAIN=...` before `docker stack deploy`) at **beta's** public IP
|
||||||
|
— that's where the task actually runs, not prod's or the desktop's.
|
||||||
|
2. Append `deploy/forgejo/caddy-git.conf` to beta's `/etc/caddy/Caddyfile`,
|
||||||
|
adjusting the domain if you didn't use the default, then `systemctl reload
|
||||||
|
caddy`.
|
||||||
|
3. That file also resolves the registry-exposure hazard (#15 in
|
||||||
|
`docs/runbooks/hop1-forgejo-registry-cutover.md`): `/v2/*` (the registry
|
||||||
|
API) is blocked to everything except the WireGuard mesh CIDR
|
||||||
|
(`10.10.0.0/24`); the git/web UI stays public. CI runners and Swarm nodes
|
||||||
|
reach the registry over the mesh, not the public internet — see "Registry
|
||||||
|
access from mesh clients" below.
|
||||||
|
|
||||||
|
## Registry access from mesh clients
|
||||||
|
|
||||||
|
Any node that needs `docker login`/push/pull against the registry (the
|
||||||
|
desktop's CI runner building/pushing images, later any Swarm node pulling
|
||||||
|
them) must reach `git.thermograph.org` **over the WireGuard tunnel**, not
|
||||||
|
beta's public IP — otherwise Caddy's `/v2/*` block above refuses the
|
||||||
|
connection. Public DNS resolves the domain to beta's public IP, so add a
|
||||||
|
`/etc/hosts` override on each such node pinning it to beta's WireGuard
|
||||||
|
address instead:
|
||||||
|
|
||||||
|
```
|
||||||
|
echo "10.10.0.2 git.thermograph.org" | sudo tee -a /etc/hosts
|
||||||
|
```
|
||||||
|
|
||||||
|
(`10.10.0.2` is beta's WG address per `deploy/swarm/README.md`'s peer
|
||||||
|
numbering — adjust if you assigned it differently.) The git/web UI keeps
|
||||||
|
working normally for everyone else since only `/v2/*` is restricted.
|
||||||
|
|
||||||
## Register the Actions runner (on the desktop, not through Swarm)
|
## Register the Actions runner (on the desktop, not through Swarm)
|
||||||
|
|
||||||
|
|
@ -54,15 +98,6 @@ See that script's header for exactly what it replaces (the pre-Forgejo GitHub
|
||||||
self-hosted runner on this same machine) and why it registers with two
|
self-hosted runner on this same machine) and why it registers with two
|
||||||
labels where there used to be two separate runners.
|
labels where there used to be two separate runners.
|
||||||
|
|
||||||
## DNS
|
|
||||||
|
|
||||||
Point the Forgejo domain (default `git.thermograph.org`; override with
|
|
||||||
`FORGEJO_DOMAIN=...` before `docker stack deploy`, Swarm reads it from the
|
|
||||||
deploying shell's environment) at **prod's or beta's** public IP — not the
|
|
||||||
desktop's, which isn't a stable publicly-reachable address. The routing mesh
|
|
||||||
forwards published ports to wherever the task actually landed, so either VPS
|
|
||||||
IP works.
|
|
||||||
|
|
||||||
## Why Postgres here and not the Thermograph app's TimescaleDB
|
## Why Postgres here and not the Thermograph app's TimescaleDB
|
||||||
|
|
||||||
Separate instance, separate network (`forgejo_net`, not the app's compose
|
Separate instance, separate network (`forgejo_net`, not the app's compose
|
||||||
|
|
@ -74,8 +109,9 @@ independently.
|
||||||
## Verifying
|
## Verifying
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker service ls # forgejo_db, forgejo_forgejo, forgejo_traefik all Running, 1/1
|
docker service ls # forgejo_db, forgejo_forgejo both Running, 1/1
|
||||||
curl -I https://git.thermograph.org/ # 200, valid cert
|
curl -I https://git.thermograph.org/ # 200, valid cert (Caddy's, not a new one)
|
||||||
|
curl -I https://git.thermograph.org/v2/ # 403 from anywhere off the WireGuard mesh
|
||||||
# On the desktop, after registering the runner:
|
# On the desktop, after registering the runner:
|
||||||
systemctl --user status forgejo-runner # active, both labels registered
|
systemctl --user status forgejo-runner # active, both labels registered
|
||||||
```
|
```
|
||||||
|
|
|
||||||
31
deploy/forgejo/caddy-git.conf
Normal file
31
deploy/forgejo/caddy-git.conf
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Append this block to /etc/caddy/Caddyfile on beta (the box this stack's
|
||||||
|
# `forgejo` service is pinned to — see docker-stack.yml). Reuses beta's
|
||||||
|
# existing Caddy instead of a second reverse proxy/ACME flow: Forgejo
|
||||||
|
# publishes its web UI to 127.0.0.1:3080 only (host-local), and this block is
|
||||||
|
# the only thing that ever talks to it.
|
||||||
|
#
|
||||||
|
# Registry exposure (hazard #15, see docker-stack.yml's header comment):
|
||||||
|
# git.thermograph.org/v2/* is the built-in OCI registry API. It's blocked
|
||||||
|
# from anywhere except the WireGuard mesh (10.10.0.0/24) — reachable to CI
|
||||||
|
# runners and Swarm nodes over wg0, not from the public internet. Everything
|
||||||
|
# else (web UI, git-over-HTTP, PR pages) stays public like the rest of the
|
||||||
|
# site.
|
||||||
|
#
|
||||||
|
# Update FORGEJO_DOMAIN below to match whatever domain you actually pointed
|
||||||
|
# at beta's IP if it's not git.thermograph.org.
|
||||||
|
|
||||||
|
git.thermograph.org {
|
||||||
|
encode zstd gzip
|
||||||
|
|
||||||
|
@registry_external {
|
||||||
|
path /v2/*
|
||||||
|
not remote_ip 10.10.0.0/24
|
||||||
|
}
|
||||||
|
respond @registry_external 403
|
||||||
|
|
||||||
|
reverse_proxy 127.0.0.1:3080
|
||||||
|
|
||||||
|
log {
|
||||||
|
output file /var/log/caddy/git.log
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,17 +1,23 @@
|
||||||
# Forgejo (self-hosted Git + CI/CD) as a Docker Swarm stack — the only workload
|
# Forgejo (self-hosted Git + CI/CD) as a Docker Swarm stack — the only workload
|
||||||
# this Swarm cluster runs (see deploy/swarm/README.md). Deliberately separate
|
# this Swarm cluster runs (see deploy/swarm/README.md). Deliberately separate
|
||||||
# from the Terraform-managed docker-compose.yml that runs the Thermograph app
|
# from the Terraform-managed docker-compose.yml that runs the Thermograph app
|
||||||
# itself: this stack's only job is Forgejo, its container registry, and the
|
# itself: this stack's only job is Forgejo and its container registry.
|
||||||
# ingress in front of it.
|
|
||||||
#
|
#
|
||||||
# The Actions RUNNER is deliberately NOT a service in this stack. Per
|
# The Actions RUNNER is deliberately NOT a service in this stack. Per
|
||||||
# docs/runbooks/implementation-handoff.md (Track B step 5), the canonical
|
# docs/runbooks/implementation-handoff.md (Track B step 5), the canonical
|
||||||
# design registers the runner on the desktop node as a plain systemd service
|
# design registers the runner on the desktop node as a plain systemd service
|
||||||
# (deploy/forgejo/register-lan-runner.sh) — the same place the pre-Forgejo
|
# (deploy/forgejo/register-lan-runner.sh) — the same place the pre-Forgejo
|
||||||
# GitHub self-hosted runner already lived, not a Swarm-scheduled container. An
|
# GitHub self-hosted runner already lived, not a Swarm-scheduled container.
|
||||||
# earlier revision of this file ran the runner + a Docker-in-Docker sidecar as
|
#
|
||||||
# Swarm services pinned to beta; that's gone now in favor of the canonical
|
# No Traefik here. Forgejo is pinned to beta (node.labels.role == forge)
|
||||||
# placement.
|
# because that's what was chosen, but beta is ALSO today's live thermograph.org
|
||||||
|
# host — Caddy already owns its ports 80/443 (see /etc/caddy/Caddyfile on that
|
||||||
|
# box). A second reverse proxy binding those same ports would either fail to
|
||||||
|
# start or fight Caddy. Instead: forgejo's web port publishes to
|
||||||
|
# 127.0.0.1:3080 only (host-local, mode: host), and beta's existing Caddy gets
|
||||||
|
# a new site block reverse-proxying git.thermograph.org -> 127.0.0.1:3080,
|
||||||
|
# same pattern as its thermograph.org block. TLS is Caddy's existing
|
||||||
|
# automatic-HTTPS (HTTP-01), not a second ACME flow.
|
||||||
#
|
#
|
||||||
# Deploy from the manager node (prod), after all three nodes (prod, beta,
|
# Deploy from the manager node (prod), after all three nodes (prod, beta,
|
||||||
# desktop) have joined the swarm and beta is labeled role=forge:
|
# desktop) have joined the swarm and beta is labeled role=forge:
|
||||||
|
|
@ -20,19 +26,13 @@
|
||||||
#
|
#
|
||||||
# Requires this Swarm secret to exist first (see deploy/forgejo/README.md):
|
# Requires this Swarm secret to exist first (see deploy/forgejo/README.md):
|
||||||
# forgejo_db_password
|
# forgejo_db_password
|
||||||
# (forgejo_runner_token is no longer a Swarm secret — see register-lan-runner.sh,
|
|
||||||
# which takes the registration token as a plain argument on the desktop instead.)
|
|
||||||
#
|
#
|
||||||
# DNS: point the Forgejo domain at prod's or beta's public IP (not the
|
# Registry exposure (hazard #15 in docs/runbooks/hop1-forgejo-registry-cutover.md):
|
||||||
# desktop's — it isn't a stable, publicly-reachable address). Swarm's routing
|
# resolved as the runbook's first listed option — serve Git/UI publicly but
|
||||||
# mesh accepts published ports on every node and forwards to wherever the
|
# firewall the /v2/ registry API paths so only WireGuard-mesh clients can
|
||||||
# task actually runs, so either of the two VPS IPs works.
|
# reach them. That's implemented in the Caddy site block (see
|
||||||
#
|
# deploy/forgejo/README.md), not here — nothing in this stack file is
|
||||||
# Registry exposure (hazard #15 in docs/runbooks/hop1-forgejo-registry-cutover.md)
|
# registry-specific, the restriction lives entirely in Caddy's config on beta.
|
||||||
# is still an open call, not resolved here: this file exposes Forgejo (and its
|
|
||||||
# built-in registry) publicly via Traefik/Let's Encrypt. The alternative —
|
|
||||||
# mesh-only registry + DNS-01 ACME + an outbound pull-mirror — is a deliberate
|
|
||||||
# design decision for whoever executes Track B, not something to guess at here.
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
db:
|
db:
|
||||||
|
|
@ -76,54 +76,25 @@ services:
|
||||||
- forgejo_data:/var/lib/gitea
|
- forgejo_data:/var/lib/gitea
|
||||||
networks: [forgejo_net]
|
networks: [forgejo_net]
|
||||||
ports:
|
ports:
|
||||||
# SSH for git@ clones — Swarm's routing mesh publishes this on both nodes.
|
# SSH for git@ clones — published on whichever node the task lands on
|
||||||
|
# (pinned to beta by the placement constraint below, so effectively
|
||||||
|
# always beta's public IP, port 2222).
|
||||||
- target: 2222
|
- target: 2222
|
||||||
published: 2222
|
published: 2222
|
||||||
protocol: tcp
|
protocol: tcp
|
||||||
mode: host
|
mode: host
|
||||||
|
# Web UI/API — host-local only. Not reachable from outside beta itself;
|
||||||
|
# beta's own Caddy reverse-proxies to it (see deploy/forgejo/README.md).
|
||||||
|
- target: 3000
|
||||||
|
published: 3080
|
||||||
|
protocol: tcp
|
||||||
|
mode: host
|
||||||
|
host_ip: 127.0.0.1
|
||||||
deploy:
|
deploy:
|
||||||
placement:
|
placement:
|
||||||
constraints: [node.labels.role == forge]
|
constraints: [node.labels.role == forge]
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
labels:
|
|
||||||
- traefik.enable=true
|
|
||||||
- traefik.http.routers.forgejo.rule=Host(`${FORGEJO_DOMAIN:-git.thermograph.org}`)
|
|
||||||
- traefik.http.routers.forgejo.entrypoints=websecure
|
|
||||||
- traefik.http.routers.forgejo.tls.certresolver=le
|
|
||||||
- traefik.http.services.forgejo.loadbalancer.server.port=3000
|
|
||||||
|
|
||||||
traefik:
|
|
||||||
image: traefik:v3.1
|
|
||||||
command:
|
|
||||||
- --providers.swarm.endpoint=unix:///var/run/docker.sock
|
|
||||||
- --providers.swarm.exposedbydefault=false
|
|
||||||
- --entrypoints.web.address=:80
|
|
||||||
- --entrypoints.websecure.address=:443
|
|
||||||
- --entrypoints.web.http.redirections.entrypoint.to=websecure
|
|
||||||
- --entrypoints.web.http.redirections.entrypoint.scheme=https
|
|
||||||
- --certificatesresolvers.le.acme.email=${ACME_EMAIL:-admin@thermograph.org}
|
|
||||||
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
|
|
||||||
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=web
|
|
||||||
ports:
|
|
||||||
- target: 80
|
|
||||||
published: 80
|
|
||||||
mode: host
|
|
||||||
- target: 443
|
|
||||||
published: 443
|
|
||||||
mode: host
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
|
||||||
- traefik_certs:/letsencrypt
|
|
||||||
networks: [forgejo_net]
|
|
||||||
deploy:
|
|
||||||
placement:
|
|
||||||
# Pinned to the same node as the ACME cert store volume — a floating
|
|
||||||
# Traefik would re-request certs from Let's Encrypt on every
|
|
||||||
# reschedule and hit its rate limit.
|
|
||||||
constraints: [node.labels.role == forge]
|
|
||||||
restart_policy:
|
|
||||||
condition: on-failure
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
forgejo_net:
|
forgejo_net:
|
||||||
|
|
@ -133,7 +104,6 @@ networks:
|
||||||
volumes:
|
volumes:
|
||||||
forgejo_db:
|
forgejo_db:
|
||||||
forgejo_data:
|
forgejo_data:
|
||||||
traefik_certs:
|
|
||||||
|
|
||||||
secrets:
|
secrets:
|
||||||
forgejo_db_password:
|
forgejo_db_password:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue