diff --git a/docs/devops/vercel/GODADDY_DNS_SETUP_BYTELYST.md b/docs/devops/vercel/GODADDY_DNS_SETUP_BYTELYST.md index 845d402c..781b591a 100644 --- a/docs/devops/vercel/GODADDY_DNS_SETUP_BYTELYST.md +++ b/docs/devops/vercel/GODADDY_DNS_SETUP_BYTELYST.md @@ -28,6 +28,55 @@ Notes: - `llmlab-dashboard` is internal VM tooling and does not need a public DNS record - `localmemgpt-web` is intended to be hosted on Vercel, so it is not part of this VM DNS runbook +## Automated Option + +If GoDaddy API credentials are already configured, use the repo script instead of editing records by hand: + +```bash +./scripts/godaddy-sync-bytelyst-dns.sh --ip --validate +``` + +Or auto-detect the current machine public IP: + +```bash +./scripts/godaddy-sync-bytelyst-dns.sh --auto-ip --dry-run +./scripts/godaddy-sync-bytelyst-dns.sh --auto-ip --validate +``` + +Credential lookup order: + +- `GODADDY_API_KEY` and `GODADDY_API_SECRET` +- `~/.config/godaddypy/credentials.yaml` + +The script manages these `A` records by default: + +- `api` +- `gitea` +- `admin` +- `tracker` + +Root shortcut: + +```bash +pnpm dns:godaddy:bytelyst -- --ip --validate +``` + +## Current Status + +Status as of `2026-03-31 09:23:20 UTC`: + +- GoDaddy `A` records were updated for `api`, `gitea`, `admin`, and `tracker` +- all four names now resolve to `187.124.159.82` +- authoritative GoDaddy nameservers returned the expected IP for all four names +- public `dig` checks also returned `187.124.159.82` +- public HTTP on port `80` responded, but returned `404 Not Found` +- public HTTPS on port `443` timed out for all four hostnames + +Interpretation: + +- DNS cutover is complete +- the remaining work is on the VM-side gateway and/or Azure network path, not in GoDaddy DNS + ## Preconditions - You know the current Azure VM public IP. @@ -85,6 +134,147 @@ Expected result: - `200`, `302`, or auth-gated responses are acceptable depending on the app - certificate issuance should complete automatically once DNS and ports are correct +## Next Action For Codex On The VM + +Delegate the remaining work to the Codex session running inside the Azure VM. The immediate goal is to make `443` work and ensure the hostnames are actually configured in Caddy. + +Recommended handoff summary: + +- DNS is already correct for `api.bytelyst.com`, `gitea.bytelyst.com`, `admin.bytelyst.com`, and `tracker.bytelyst.com` +- all four names point to `187.124.159.82` +- do not spend time redoing GoDaddy changes unless records drift +- focus on `/opt/bytelyst/Caddyfile`, the `caddy` container, and Azure NSG rules for `443` + +Run these on the VM: + +```bash +cd /opt/bytelyst/learning_ai_common_plat + +docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' +docker logs caddy --tail 200 +docker exec caddy caddy validate --config /etc/caddy/Caddyfile +sudo ss -ltnp | grep -E ':80|:443' + +curl -sI http://localhost:3001 | head -5 +curl -sI http://localhost:3003 | head -5 +curl -sI http://localhost:3300 | head -5 +curl -sI http://localhost:4003/health | head -5 +``` + +Check the live Caddy config: + +```bash +sed -n '1,220p' /opt/bytelyst/Caddyfile +``` + +The live Caddy config should cover at least these hostnames: + +- `api.bytelyst.com` +- `gitea.bytelyst.com` +- `admin.bytelyst.com` +- `tracker.bytelyst.com` + +Expected proxy targets: + +- `api.bytelyst.com`: + - `/platform/*` -> `platform-service:4003` + - `/extraction/*` -> `extraction-service:4005` + - `/mcp/*` -> `mcp-server:4007` +- `gitea.bytelyst.com` -> host or container endpoint for Gitea on port `3300` +- `admin.bytelyst.com` -> `admin-web:3001` +- `tracker.bytelyst.com` -> `tracker-web:3003` + +If the file is missing host blocks, update it and reload Caddy: + +```bash +docker exec caddy caddy reload --config /etc/caddy/Caddyfile +docker logs caddy --tail 100 +``` + +If `443` still times out after Caddy config is corrected: + +- verify the Azure NSG allows inbound `443/tcp` +- verify no other process is conflicting with port `443` +- verify the `caddy` container actually publishes `0.0.0.0:443->443/tcp` + +Re-test from the VM: + +```bash +curl -vk https://api.bytelyst.com/platform/health +curl -vk https://gitea.bytelyst.com +curl -vk https://admin.bytelyst.com +curl -vk https://tracker.bytelyst.com +``` + +Ready-to-paste prompt for the Codex session running inside the VM: + +```text +The GoDaddy DNS cutover is already complete. Do not redo DNS unless records drift. + +Known-good DNS state as of 2026-03-31: +- api.bytelyst.com -> 187.124.159.82 +- gitea.bytelyst.com -> 187.124.159.82 +- admin.bytelyst.com -> 187.124.159.82 +- tracker.bytelyst.com -> 187.124.159.82 + +Known current failure: +- HTTP on port 80 responds, but returns 404 +- HTTPS on port 443 times out for all four hostnames + +Your task: +1. Inspect the live VM deployment under /opt/bytelyst +2. Read /opt/bytelyst/Caddyfile +3. Verify the caddy container is running and publishing 80/443 +4. Verify Azure-side access assumptions from inside the VM as far as possible +5. Fix the live Caddy config so these hosts are served: + - api.bytelyst.com + - gitea.bytelyst.com + - admin.bytelyst.com + - tracker.bytelyst.com +6. Ensure proxy targets are correct: + - api.bytelyst.com: + - /platform/* -> platform-service:4003 + - /extraction/* -> extraction-service:4005 + - /mcp/* -> mcp-server:4007 + - gitea.bytelyst.com -> Gitea on port 3300 + - admin.bytelyst.com -> admin-web:3001 + - tracker.bytelyst.com -> tracker-web:3003 +7. Reload Caddy +8. Verify: + - curl -vk https://api.bytelyst.com/platform/health + - curl -vk https://gitea.bytelyst.com + - curl -vk https://admin.bytelyst.com + - curl -vk https://tracker.bytelyst.com + +Run these first: + +cd /opt/bytelyst/learning_ai_common_plat +docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' +docker logs caddy --tail 200 +docker exec caddy caddy validate --config /etc/caddy/Caddyfile +sudo ss -ltnp | grep -E ':80|:443' +sed -n '1,220p' /opt/bytelyst/Caddyfile +curl -sI http://localhost:3001 | head -5 +curl -sI http://localhost:3003 | head -5 +curl -sI http://localhost:3300 | head -5 +curl -sI http://localhost:4003/health | head -5 + +If /opt/bytelyst/Caddyfile is missing host blocks, fix it there and reload: + +docker exec caddy caddy reload --config /etc/caddy/Caddyfile +docker logs caddy --tail 100 + +If 443 still times out after the config fix: +- verify Azure NSG allows inbound 443/tcp +- verify no other process is bound to 443 +- verify the caddy container publishes 0.0.0.0:443->443/tcp + +When done, report: +- what was wrong +- what file(s) you changed +- exact verification results for all four public hostnames +``` + ## Troubleshooting If records do not resolve as expected: @@ -102,10 +292,19 @@ docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' | grep -E 'caddy| docker logs caddy --tail 100 ``` +Likely root causes for the current state: + +- the live `/opt/bytelyst/Caddyfile` only includes `api.bytelyst.com` +- `gitea`, `admin`, and `tracker` host blocks were never added on the VM +- Azure NSG is allowing `80` but not `443` +- Caddy is not healthy or is failing certificate issuance / bind on `443` + ## Change Log Use this section to record real DNS cutovers: -| Date | Operator | Change | Result | -| ------------ | -------- | ------------------------------------------------------- | -------------- | -| `2026-03-31` | Codex | Created GoDaddy-specific DNS runbook for `bytelyst.com` | Document added | +| Date | Operator | Change | Result | +| ------------ | -------- | ------------------------------------------------------------------------------------------ | -------------------- | +| `2026-03-31` | Codex | Created GoDaddy-specific DNS runbook for `bytelyst.com` | Document added | +| `2026-03-31` | Codex | Updated GoDaddy `A` records for `api`, `gitea`, `admin`, and `tracker` to `187.124.159.82` | DNS cutover complete | +| `2026-03-31` | Codex | Verified DNS propagation and recorded VM-side HTTPS follow-up steps | VM action pending | diff --git a/package.json b/package.json index e4292cbe..f929f263 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"", "audit": "pnpm -r audit --audit-level moderate", "clean": "pnpm -r exec rm -rf dist", + "dns:godaddy:bytelyst": "./scripts/godaddy-sync-bytelyst-dns.sh", "prototype:self-test": "./scripts/prototype-self-test.sh", "prepare": "husky" }, diff --git a/scripts/godaddy-sync-bytelyst-dns.sh b/scripts/godaddy-sync-bytelyst-dns.sh new file mode 100755 index 00000000..75959af2 --- /dev/null +++ b/scripts/godaddy-sync-bytelyst-dns.sh @@ -0,0 +1,210 @@ +#!/usr/bin/env bash +set -euo pipefail + +DOMAIN="${GODADDY_DOMAIN:-bytelyst.com}" +TTL="${GODADDY_DNS_TTL:-600}" +TARGET_IP="${GODADDY_DNS_TARGET_IP:-}" +AUTO_IP=false +DRY_RUN=false +VALIDATE=false +HOSTS=("api" "gitea" "admin" "tracker") + +CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/godaddypy/credentials.yaml" + +usage() { + cat <<'USAGE' +Upsert ByteLyst GoDaddy A records for the Azure VM cutover. + +Usage: + scripts/godaddy-sync-bytelyst-dns.sh [options] + +Options: + --ip
Target IPv4 address for all managed A records + --auto-ip Detect the current public IP with api.ipify.org + --domain GoDaddy zone to update (default: bytelyst.com) + --ttl TTL for the managed A records (default: 600) + --hosts Hostnames to manage (default: api,gitea,admin,tracker) + --validate Run dig validation after changes + --dry-run Print the API operations without applying them + -h, --help Show help + +Credentials: + - Preferred: GODADDY_API_KEY and GODADDY_API_SECRET + - Fallback: ~/.config/godaddypy/credentials.yaml + +Examples: + scripts/godaddy-sync-bytelyst-dns.sh --ip 20.81.12.34 --validate + scripts/godaddy-sync-bytelyst-dns.sh --auto-ip --dry-run +USAGE +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --ip) + TARGET_IP="${2:-}" + shift 2 + ;; + --auto-ip) + AUTO_IP=true + shift + ;; + --domain) + DOMAIN="${2:-}" + shift 2 + ;; + --ttl) + TTL="${2:-}" + shift 2 + ;; + --hosts) + IFS=',' read -r -a HOSTS <<<"${2:-}" + shift 2 + ;; + --validate) + VALIDATE=true + shift + ;; + --dry-run) + DRY_RUN=true + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown arg: $1" >&2 + usage + exit 2 + ;; + esac +done + +trim() { + local value="$1" + value="${value#"${value%%[![:space:]]*}"}" + value="${value%"${value##*[![:space:]]}"}" + printf '%s' "$value" +} + +load_credential_from_file() { + local key_name="$1" + + if [[ ! -f "$CONFIG_FILE" ]]; then + return 1 + fi + + sed -nE "s/^[[:space:]]*${key_name}:[[:space:]]*['\"]?([^'\"]+)['\"]?[[:space:]]*$/\\1/p" "$CONFIG_FILE" | head -n 1 +} + +GODADDY_API_KEY="${GODADDY_API_KEY:-}" +GODADDY_API_SECRET="${GODADDY_API_SECRET:-}" + +if [[ -z "$GODADDY_API_KEY" ]]; then + GODADDY_API_KEY="$(load_credential_from_file key || true)" +fi + +if [[ -z "$GODADDY_API_SECRET" ]]; then + GODADDY_API_SECRET="$(load_credential_from_file secret || true)" +fi + +if [[ -z "$GODADDY_API_KEY" || -z "$GODADDY_API_SECRET" ]]; then + echo "Missing GoDaddy credentials. Set GODADDY_API_KEY and GODADDY_API_SECRET, or configure $CONFIG_FILE." >&2 + exit 2 +fi + +if [[ "$AUTO_IP" == true ]]; then + TARGET_IP="$(curl -fsS https://api.ipify.org)" +fi + +if [[ -z "$TARGET_IP" ]]; then + echo "Missing target IP. Use --ip
or --auto-ip." >&2 + exit 2 +fi + +if [[ ! "$TARGET_IP" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then + echo "Target IP does not look like an IPv4 address: $TARGET_IP" >&2 + exit 2 +fi + +if [[ ! "$TTL" =~ ^[0-9]+$ ]]; then + echo "TTL must be numeric: $TTL" >&2 + exit 2 +fi + +if ! command -v curl >/dev/null 2>&1; then + echo "curl is required." >&2 + exit 1 +fi + +api_request() { + local method="$1" + local path="$2" + local body="${3:-}" + + local -a curl_args=( + -fsS + -X "$method" + -H "Authorization: sso-key ${GODADDY_API_KEY}:${GODADDY_API_SECRET}" + -H "Accept: application/json" + "https://api.godaddy.com${path}" + ) + + if [[ -n "$body" ]]; then + curl_args+=(-H "Content-Type: application/json" --data "$body") + fi + + curl "${curl_args[@]}" +} + +printf 'Domain: %s\n' "$DOMAIN" +printf 'Target IP: %s\n' "$TARGET_IP" +printf 'TTL: %s\n' "$TTL" +printf 'Hosts: %s\n' "$(IFS=,; printf '%s' "${HOSTS[*]}")" + +for raw_host in "${HOSTS[@]}"; do + host="$(trim "$raw_host")" + if [[ -z "$host" ]]; then + continue + fi + + path="/v1/domains/${DOMAIN}/records/A/${host}" + payload=$(printf '[{"data":"%s","ttl":%s}]' "$TARGET_IP" "$TTL") + + echo + printf 'Record: %s.%s\n' "$host" "$DOMAIN" + + if [[ "$DRY_RUN" == true ]]; then + printf 'Dry run: PUT %s %s\n' "$path" "$payload" + continue + fi + + current_records="$(api_request GET "$path" || true)" + if [[ -n "$current_records" ]]; then + printf 'Existing A records: %s\n' "$current_records" + else + echo "Existing A records: none" + fi + + api_request PUT "$path" "$payload" >/dev/null + echo "Updated A record." +done + +if [[ "$VALIDATE" == true ]]; then + echo + echo "Validation:" + if ! command -v dig >/dev/null 2>&1; then + echo "dig not found; skipping DNS validation." >&2 + exit 0 + fi + + for raw_host in "${HOSTS[@]}"; do + host="$(trim "$raw_host")" + if [[ -z "$host" ]]; then + continue + fi + + resolved_ip="$(dig +short "${host}.${DOMAIN}" | tail -n 1)" + printf '%s.%s -> %s\n' "$host" "$DOMAIN" "${resolved_ip:-}" + done +fi