Add Cloudflare DDNS for devopshomelab.com (IPv4 A records)

timothyjmiller/cloudflare-ddns with host networking; render env from
secrets (apex + subdomains mc/minecraft/vpn/www/forgejo/jellyfin).
This commit is contained in:
tim 2026-07-22 19:35:21 -07:00
parent 60f8a89c87
commit c130080c52
5 changed files with 129 additions and 0 deletions

View file

@ -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

View file

@ -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`.

View file

@ -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

View file

@ -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" <<EOF
# Generated by scripts/render-cloudflare-ddns-env.sh — do not commit
CLOUDFLARE_API_TOKEN=${TOKEN}
IP4_DOMAINS=${IP4_LIST}
IP4_PROVIDER=cloudflare.trace
IP6_PROVIDER=none
PROXIED=${PROXIED}
UPDATE_CRON=${CRON}
UPDATE_ON_START=true
TTL=1
RECORD_COMMENT=arr-stack cloudflare-ddns
TZ=${TZ:-UTC}
EOF
# optional zone id is not required by current image (token + domains)
if [[ -n "$ZONE" ]]; then
echo "# CLOUDFLARE_ZONE_ID=${ZONE} (informational; env-mode uses token scopes)" >>"$OUT"
fi
chmod 600 "$OUT"
echo "Wrote $OUT"
echo "IP4_DOMAINS=${IP4_LIST}"

View file

@ -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