One runner (cf. @applications/prospector/run): services/deploy/status/logs/ restart over the services/ tree (each a dir with deploy.sh + compose.yml, shipped to /opt/<svc>), plus a `tf` passthrough to terraform/do with the vault token. Auto-discovers services; target from services/<svc>/.target else the forge droplet; health from services/<svc>/.health. Fleet SSH uses a dedicated known_hosts and self-heals a changed host key after a droplet rebuild. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| client | ||
| src | ||
| test | ||
| .gitignore | ||
| .health | ||
| bun.lock | ||
| compose.yml | ||
| deploy.sh | ||
| Dockerfile | ||
| env.example | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
dns-updater (dns.ct.uvlava.com)
A tiny dyndns2-compatible
DNS updater for the uvlava.com zone, backed by the DigitalOcean DNS API.
It exists so region-mobile nodes can repoint their own A record automatically
when they move regions — no registrar GUI, no manual dig/edit:
| FQDN (moving) | Node | Lifecycle |
|---|---|---|
live.ct.uvlava.com |
broadcast relay droplet | ephemeral — per show, nearest the broadcaster, torn down after |
sales.ct.uvlava.com |
"sales" surface = Prospector PWA node | always-on — follows the operator across regions, stays up |
The pretty *.transquinnftw.com names are static CNAMEs onto these (set once,
below), so the only records that ever move are inside the zone we control via the
DO API. The transquinnftw.com zone at joker.com never changes again.
Why not joker.com DynDNS?
Joker offers dyndns2 too, but with per-host credentials at the registrar and
no fleet-wide auth. Since uvlava.com is delegated to DO, we own that zone via
the API — so a single self-hosted service gives us one auth model (token →
hostname allowlist), keeps transquinnftw.com static, and is the foundation for
DNS-01 ACME / multi-zone later.
Endpoints
GET /healthz→{ "ok": true, "domain": "uvlava.com" }GET|POST /nic/update?hostname=<fqdn>&myip=<ipv4>→ dyndns2 response:good <ip>·nochg <ip>·nohost·notfqdn·badauth·911
myip is optional — omit it and the caller's IP (left-most X-Forwarded-For
from the forge Caddy) is used. A token may update only the hostnames in its
grant; it cannot touch forge.ct/npm.ct/another node's record.
Auth
Authorization: Bearer <token> (scripts) or HTTP Basic with the token as the
password (Authorization: Basic base64(any:<token>), what generic dyndns2
clients send).
Configuration (env / .env)
| Var | Required | Meaning |
|---|---|---|
DO_TOKEN |
yes | DO API token (read/write DNS). Use a dedicated token, not the TF one. |
DNS_UPDATER_TOKENS |
yes | JSON: [{"token":"…","hosts":["live.ct.uvlava.com"]}] |
DNS_DOMAIN |
no | Zone (default uvlava.com) |
PORT |
no | Bind port (default 8090) |
TRUST_PROXY |
no | Trust X-Forwarded-For (default true; Caddy fronts it) |
Generate node tokens with openssl rand -hex 24. See env.example.
Deploy
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.
# 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 host Caddy vhost + reload
deploy.sh is idempotent: builds/starts the container, appends the
dns.ct.uvlava.com vhost to /etc/caddy/Caddyfile if missing, caddy validates
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)
transquinnftw.com lives at joker.com (login natily). After the DO records
exist (terraform apply of the records below), add two static CNAMEs there —
these never change again:
live CNAME live.ct.uvlava.com.
sales CNAME sales.ct.uvlava.com.
transquinnftw.comis NOT delegated to DO, so these CNAMEs are added in the joker.com DNS panel by hand, once. The existing*.transquinnftw.comLE wildcard covers TLS for both names (or the node's own Caddy issues per-name).
terraform records
terraform/do/dns.tf creates dns.ct (→ forge) and seeds live.ct /
sales.ct with lifecycle { ignore_changes = [value] } — TF makes them
exist; this service owns their value at runtime.
Node-side usage
Broadcast relay (per show) — after provisioning the droplet, point
live.ct at it (see lilith-platform.live →
scripts/provision-stream-droplet.sh, which calls):
curl -fsS -H "Authorization: Bearer $DNS_UPDATER_TOKEN" \
"https://dns.ct.uvlava.com/nic/update?hostname=live.ct.uvlava.com&myip=$DROPLET_IP"
Sales node (always-on, on boot + every 5 min) — the public "sales" surface
is the Prospector PWA node. Install the reusable client (client/), which
self-reports the node's public IP (myip omitted → caller IP) via a systemd
timer so the record tracks the node across moves:
# on the node, as root:
DNS_HOSTNAME=sales.ct.uvlava.com NODE_TOKEN=<token> \
./client/install-client.sh
It installs dyndns-update.sh + a dyndns-updater.timer (OnBootSec + every
5 min). Under the hood each tick is:
curl -fsS -H "Authorization: Bearer $TOKEN" \
"https://dns.ct.uvlava.com/nic/update?hostname=sales.ct.uvlava.com"
Local dev
bun install
DO_TOKEN=… DNS_UPDATER_TOKENS='[{"token":"dev","hosts":["live.ct.uvlava.com"]}]' \
bun run dev
curl -H 'Authorization: Bearer dev' \
'http://127.0.0.1:8090/nic/update?hostname=live.ct.uvlava.com&myip=1.2.3.4'