fix(dns-updater): target the live forge droplet's host Caddy, not a container

The running ct-forge droplet (134.199.243.61 / lilith-forge) terminates TLS
with a HOST Caddy (/etc/caddy/Caddyfile, systemd) proxying to localhost ports —
it does NOT run a Caddy container or the cloud-init compose stack. Rework:
- compose.yml publishes 127.0.0.1:8090 (loopback) instead of joining an edge net
- deploy.sh appends the dns.ct vhost to /etc/caddy/Caddyfile, caddy-validates,
  systemctl reload caddy; default target is the IP (forge.ct won't resolve until
  DNSSEC is removed)
- revert the forge.yaml cloud-init edits (edge network + container vhost) that
  assumed a Caddy container
- README documents the host-Caddy reality

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Natalie 2026-06-29 15:13:03 -04:00
parent 5b6faba4f7
commit 056a33a417
4 changed files with 41 additions and 61 deletions

View file

@ -53,21 +53,25 @@ Generate node tokens with `openssl rand -hex 24`. See `env.example`.
## Deploy
Runs on the **ct-forge droplet** behind the existing Caddy, on a shared `edge`
docker network.
Runs on the **ct-forge droplet** (`134.199.243.61`). The droplet terminates TLS
with a **host Caddy** (`/etc/caddy/Caddyfile`, systemd) that reverse-proxies to
localhost ports. The container publishes `127.0.0.1:8090`; Caddy proxies
`dns.ct.uvlava.com` → it.
```bash
# one-time: put secrets on the droplet
ssh root@forge.ct.uvlava.com 'mkdir -p /opt/dns-updater'
scp env.example root@forge.ct.uvlava.com:/opt/dns-updater/.env # then edit + fill
# one-time: put secrets on the droplet (use the IP; forge.ct won't resolve
# until uvlava.com DNSSEC is removed)
ssh root@134.199.243.61 'mkdir -p /opt/dns-updater'
scp env.example root@134.199.243.61:/opt/dns-updater/.env # then edit + fill
./deploy.sh # rsync + build + start + wire Caddy vhost + reload
./deploy.sh # rsync + build + start + wire host Caddy vhost + reload
```
`deploy.sh` is idempotent: ensures the `edge` network, attaches Caddy, appends
the `dns.ct.uvlava.com` vhost to `/opt/forge/Caddyfile` if missing, reloads
Caddy, and health-checks the container. (The same wiring is declared in
`terraform/do/cloud-init/forge.yaml` for clean reprovisions.)
`deploy.sh` is idempotent: builds/starts the container, appends the
`dns.ct.uvlava.com` vhost to `/etc/caddy/Caddyfile` if missing, `caddy validate`s
before reloading, and health-checks on loopback. The LE cert is auto-issued by
Caddy via HTTP-01 **once `dns.ct.uvlava.com` resolves publicly** (i.e. after the
uvlava.com DNSSEC DS is removed at joker).
## One-time registrar setup (manual, at joker.com)

View file

@ -1,9 +1,9 @@
# dns-updater stack on the ct-forge droplet.
#
# Joins the external "edge" network that the forge Caddy is attached to, so
# Caddy reaches this service by name (reverse_proxy dns-updater:8090). The
# `edge` network + the dns.ct.uvlava.com vhost are added to the forge stack in
# terraform/do/cloud-init/forge.yaml.
# The live forge droplet terminates TLS with a HOST Caddy (/etc/caddy/Caddyfile,
# systemd), not a Caddy container. So this service publishes a loopback-only port
# and the host Caddy reverse-proxies dns.ct.uvlava.com -> 127.0.0.1:8090.
# deploy.sh wires the vhost + reloads Caddy.
#
# Secrets (.env, gitignored): DO_TOKEN, DNS_UPDATER_TOKENS. See env.example.
services:
@ -17,10 +17,6 @@ services:
- DNS_DOMAIN=${DNS_DOMAIN:-uvlava.com}
- PORT=8090
- TRUST_PROXY=true
networks:
- edge
networks:
edge:
external: true
name: edge
# Loopback-only: reachable by the host Caddy, never directly from the internet.
ports:
- "127.0.0.1:8090:8090"

View file

@ -1,16 +1,17 @@
#!/usr/bin/env bash
# Deploy dns-updater to the ct-forge droplet.
#
# Rsyncs this service dir to /opt/dns-updater on the forge droplet, ensures the
# shared `edge` docker network exists and the forge Caddy is attached to it,
# then (re)builds and starts the container. The .env (secrets) must already be
# placed on the droplet at /opt/dns-updater/.env (it is gitignored and never
# rsynced over an existing one).
# Rsyncs this service dir to /opt/dns-updater, (re)builds + starts the container
# (published loopback-only on 127.0.0.1:8090), then wires the HOST Caddy
# (/etc/caddy/Caddyfile, systemd) to reverse-proxy dns.ct.uvlava.com -> :8090
# and reloads it. The .env (secrets) must already be on the droplet at
# /opt/dns-updater/.env (gitignored; never rsynced over an existing one).
#
# Usage: ./deploy.sh [user@host] (default: root@forge.ct.uvlava.com)
# Usage: ./deploy.sh [user@host] (default: root@134.199.243.61 — forge.ct
# won't resolve until uvlava.com DNSSEC is removed; use the IP).
set -euo pipefail
TARGET="${1:-root@forge.ct.uvlava.com}"
TARGET="${1:-root@134.199.243.61}"
REMOTE_DIR="/opt/dns-updater"
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@ -22,16 +23,6 @@ rsync -az --delete \
--exclude '.git' \
"${HERE}/" "${TARGET}:${REMOTE_DIR}/"
echo "==> Ensure shared 'edge' network + forge Caddy attached"
ssh "${TARGET}" bash -s <<'REMOTE'
set -euo pipefail
docker network inspect edge >/dev/null 2>&1 || docker network create edge
# Attach the forge Caddy container to edge (idempotent).
if docker ps --format '{{.Names}}' | grep -qx caddy; then
docker network connect edge caddy 2>/dev/null || true
fi
REMOTE
echo "==> Verify .env present (secrets) on droplet"
ssh "${TARGET}" "test -f ${REMOTE_DIR}/.env" || {
echo "ERROR: ${REMOTE_DIR}/.env missing on droplet. Copy from env.example and fill DO_TOKEN + DNS_UPDATER_TOKENS." >&2
@ -41,25 +32,28 @@ ssh "${TARGET}" "test -f ${REMOTE_DIR}/.env" || {
echo "==> Build + start dns-updater"
ssh "${TARGET}" "cd ${REMOTE_DIR} && docker compose up -d --build"
echo "==> Ensure dns.ct.uvlava.com vhost in the live forge Caddyfile + reload"
echo "==> Ensure dns.ct.uvlava.com vhost in the host Caddyfile + reload"
ssh "${TARGET}" bash -s <<'REMOTE'
set -euo pipefail
CF=/opt/forge/Caddyfile
CF=/etc/caddy/Caddyfile
if [ -f "$CF" ] && ! grep -q "dns.ct.uvlava.com" "$CF"; then
cat >>"$CF" <<'VHOST'
# dyndns2 updater for region-mobile nodes (services/dns-updater).
dns.ct.uvlava.com {
reverse_proxy dns-updater:8090
reverse_proxy 127.0.0.1:8090
}
VHOST
echo "vhost appended"
else
echo "vhost already present"
fi
# Reload Caddy in place (no downtime for the other vhosts).
docker exec caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null \
|| docker restart caddy
# Validate before reloading so a bad edit never takes Caddy down.
caddy validate --config "$CF" >/dev/null 2>&1 || { echo "Caddyfile validate FAILED — not reloading" >&2; exit 1; }
systemctl reload caddy || systemctl restart caddy
REMOTE
echo "==> Health check"
ssh "${TARGET}" "docker run --rm --network edge curlimages/curl:latest -fsS http://dns-updater:8090/healthz" && echo
echo "==> Done. Verify https://dns.ct.uvlava.com/healthz once the A record + LE cert are live."
echo "==> Health check (loopback on droplet)"
ssh "${TARGET}" "curl -fsS http://127.0.0.1:8090/healthz" && echo
echo "==> Done. https://dns.ct.uvlava.com/healthz will work once uvlava.com DNSSEC"
echo " is removed (Caddy then auto-issues the LE cert via HTTP-01)."

View file

@ -73,15 +73,6 @@ write_files:
- ./caddy-data:/data
- ./caddy-config:/config
- ./Caddyfile:/etc/caddy/Caddyfile
# Also join the shared "edge" net so Caddy can reach the separately
# deployed dns-updater (services/dns-updater) as dns-updater:8090.
networks:
- default
- edge
networks:
edge:
name: edge
- path: /opt/forge/Caddyfile
permissions: "0644"
@ -102,11 +93,6 @@ write_files:
swift.ct.uvlava.com {
reverse_proxy forgejo:3000
}
# dyndns2 updater for region-mobile nodes (see services/dns-updater).
# Container deployed separately; reachable over the shared "edge" network.
dns.ct.uvlava.com {
reverse_proxy dns-updater:8090
}
runcmd:
# Install Docker engine + compose plugin.