diff --git a/docker-compose.yml b/docker-compose.yml index 319c9d1..173917e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -200,6 +200,33 @@ services: - ./landing/Caddyfile:/etc/caddy/Caddyfile:ro - ./landing/site:/srv:ro + # ---------------------------------------------- Cloudflare dynamic DNS (A) # + # Updates apex + subdomains to the host's public IPv4. Secrets from secrets/.env + # Render env first: ./scripts/render-cloudflare-ddns-env.sh + cloudflare-ddns: + image: timothyjmiller/cloudflare-ddns:latest + container_name: cloudflare-ddns + restart: unless-stopped + # Host networking so we see the real WAN IPv4 (not a Docker NAT address) + network_mode: host + security_opt: + - no-new-privileges:true + env_file: + - path: ${CONFIG_DIR}/cloudflare-ddns/ddns.env + required: false + environment: + TZ: ${TZ} + # Fallbacks if ddns.env not rendered yet (prefer secrets via render script) + CLOUDFLARE_API_TOKEN: ${CLOUDFLARE_API_TOKEN:-${CLOUDFLAREAPIKEY:-}} + IP4_DOMAINS: ${CLOUDFLARE_IP4_DOMAINS:-} + IP4_PROVIDER: ${CLOUDFLARE_IP4_PROVIDER:-cloudflare.trace} + IP6_PROVIDER: none + PROXIED: ${CLOUDFLARE_PROXIED:-false} + UPDATE_CRON: ${CLOUDFLARE_UPDATE_CRON:-@every 5m} + UPDATE_ON_START: "true" + TTL: "1" + RECORD_COMMENT: arr-stack cloudflare-ddns + # ------------------------------------------------------------------- music # lidarr: image: lscr.io/linuxserver/lidarr:latest diff --git a/quickstart.md b/quickstart.md index c0ddc23..270750a 100644 --- a/quickstart.md +++ b/quickstart.md @@ -98,6 +98,7 @@ LAN URLs (UFW allows **`192.168.8.0/24`**). Replace host if needed. | Service | URL | |---------|-----| | **Landing page** | http://192.168.8.123/ | +| Cloudflare DDNS | (background) apex + subdomains → public IPv4 | | Jellyfin | http://192.168.8.123:8096 | | qBittorrent | http://192.168.8.123:8080 | | Prowlarr | http://192.168.8.123:9696 | @@ -147,6 +148,17 @@ Root folders: `/data/media/tv`, `/data/media/movies`, `/data/media/music` (hardl **Prowlarr** — Add indexers. Apps: Sonarr `http://sonarr:8989`, Radarr `http://radarr:7878`, Lidarr `http://lidarr:8686` + each API key (*Settings → General*). +**Cloudflare DDNS** (`timothyjmiller/cloudflare-ddns`) — updates **devopshomelab.com** and subdomains to your **public IPv4** every 5 minutes. + +```bash +# secrets/.env must include CLOUDFLAREAPIKEY, CLOUDFLAREDOMAIN, CLOUDFLARESUBDOMAINS +./scripts/render-cloudflare-ddns-env.sh +docker compose up -d cloudflare-ddns +docker logs cloudflare-ddns --tail 30 +``` + +Token needs **Zone → DNS → Edit**. `PROXIED=false` by default (DNS-only; better for VPN). Set `CLOUDFLARE_PROXIED=true` in secrets if you want orange-cloud for all names. + **Usenet (NZBgeek + Newshosting + Tweaknews)** Credentials live in **`secrets/.env`** (not git). Example keys: see `secrets/.env.example`. diff --git a/scripts/bootstrap-stack.sh b/scripts/bootstrap-stack.sh index 8eefa28..4b9c6a6 100755 --- a/scripts/bootstrap-stack.sh +++ b/scripts/bootstrap-stack.sh @@ -119,6 +119,13 @@ fi # --------------------------------------------------------------------------- # Usenet bootstrap # --------------------------------------------------------------------------- +# Cloudflare DDNS (optional — only if secrets present) +if [[ -f "$STACK_DIR/secrets/.env" ]] && grep -qE '^CLOUDFLAREAPIKEY=.+' "$STACK_DIR/secrets/.env" 2>/dev/null; then + log "Rendering Cloudflare DDNS env + starting container" + "$SCRIPT_DIR/render-cloudflare-ddns-env.sh" || log "cloudflare-ddns render failed" + docker compose up -d cloudflare-ddns || log "cloudflare-ddns start failed" +fi + if [[ "$CONFIGURE_USENET" == "true" || "$CONFIGURE_USENET" == "1" ]]; then log "Waiting for SABnzbd…" for i in $(seq 1 60); do diff --git a/scripts/render-cloudflare-ddns-env.sh b/scripts/render-cloudflare-ddns-env.sh new file mode 100755 index 0000000..22da1ed --- /dev/null +++ b/scripts/render-cloudflare-ddns-env.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Build config/cloudflare-ddns.env for timothyjmiller/cloudflare-ddns from secrets/.env +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +STACK_DIR="${STACK_DIR:-$(cd "$SCRIPT_DIR/.." && pwd)}" +SECRETS="${SECRETS_FILE:-$STACK_DIR/secrets/.env}" +OUT_DIR="${CONFIG_DIR:-$STACK_DIR/config}/cloudflare-ddns" +OUT="$OUT_DIR/ddns.env" + +[[ -f "$SECRETS" ]] || { echo "missing $SECRETS"; exit 1; } + +set -a +# shellcheck disable=SC1090 +source "$SECRETS" +set +a + +TOKEN="${CLOUDFLARE_API_TOKEN:-${CLOUDFLAREAPIKEY:-}}" +ZONE="${CLOUDFLARE_ZONE_ID:-${CLOUDFLAREZONEID:-}}" +DOMAIN="${CLOUDFLARE_DOMAIN:-${CLOUDFLAREDOMAIN:-devopshomelab.com}}" +SUBS="${CLOUDFLARE_SUBDOMAINS:-${CLOUDFLARESUBDOMAINS:-}}" +PROXIED="${CLOUDFLARE_PROXIED:-${CLOUDFLAREPROXIED:-false}}" +CRON="${CLOUDFLARE_UPDATE_CRON:-@every 5m}" + +[[ -n "$TOKEN" ]] || { echo "CLOUDFLAREAPIKEY / CLOUDFLARE_API_TOKEN missing"; exit 1; } +[[ -n "$DOMAIN" ]] || { echo "CLOUDFLAREDOMAIN missing"; exit 1; } + +# apex + each subdomain → full FQDNs +DOMAINS=("$DOMAIN") +IFS=',' read -ra parts <<<"$SUBS" +for s in "${parts[@]}"; do + s="$(echo "$s" | tr -d '[:space:]')" + [[ -z "$s" ]] && continue + if [[ "$s" == "$DOMAIN" || "$s" == "@" || "$s" == "." ]]; then + continue + elif [[ "$s" == *.* ]]; then + DOMAINS+=("$s") + else + DOMAINS+=("${s}.${DOMAIN}") + fi +done + +# unique join +IP4_LIST="$(printf '%s\n' "${DOMAINS[@]}" | awk 'NF && !seen[$0]++' | paste -sd, -)" + +mkdir -p "$OUT_DIR" +umask 077 +cat >"$OUT" <>"$OUT" +fi + +chmod 600 "$OUT" +echo "Wrote $OUT" +echo "IP4_DOMAINS=${IP4_LIST}" diff --git a/secrets/.env.example b/secrets/.env.example index 9d2eb38..e8f0a48 100644 --- a/secrets/.env.example +++ b/secrets/.env.example @@ -24,3 +24,17 @@ TWEAKNEWSSSLPORT=563 TWEAKNEWSLOGIN= TWEAKNEWSPASSWORD= TWEAKNEWSCONNECTIONS=40 + +# --- Cloudflare DDNS (timothyjmiller/cloudflare-ddns) --- +# API token needs Zone.DNS Edit on the zone (not the global API key) +CLOUDFLAREAPIKEY= +# optional aliases +CLOUDFLARE_API_TOKEN= +CLOUDFLAREZONEID= +CLOUDFLAREACCOUNTID= +CLOUDFLAREDOMAIN=devopshomelab.com +# Comma-separated labels (or FQDNs). Apex is always included. +CLOUDFLARESUBDOMAINS=mc,minecraft,vpn,www,forgejo,jellyfin +# false = DNS-only (needed for VPN); true = orange-cloud proxy +CLOUDFLARE_PROXIED=false +CLOUDFLARE_UPDATE_CRON=@every 5m