Files
serverAppConfigs/dankclaw/Caddyfile
T
Frank 56763e014e fix(hermes): point the signal adapter at the verified alias path and route hermes.franksdank.cloud to the dashboard
The gateway's SIGNAL_HTTP_URL pointed at the host LAN IP. From container
space on netavark networks only DNAT'd published ports accept connections
to that address; :8080 has no forward, so every connection was refused
and the Signal adapter stayed disconnected since the Aug 26 reboot.
host.containers.internal is the verified working path (probed live from a
probe container on dankclaw_default).

The public hostname also routed to hermes:8090, which serves nothing —
the gateway has no HTTP listener. It now routes to the dashboard's login-
gated admin UI on :9119, behind the existing Anubis+Authentik guard.
2026-08-30 04:43:15 -07:00

315 lines
10 KiB
Caddyfile

# Homelab Caddyfile — the single Caddy for both hosts.
#
# Caddy runs as a container on dankclaw (192.168.0.143, this repo's
# compose.yaml service 'caddy') and fronts every service on both hosts:
# - dankclaw services: by container name (compose network) or by the
# published LAN port (quadlet services on isolated networks)
# - dankserver services (192.168.0.235): by published LAN port
#
# Naming: flat franksdank.cloud scheme (see serverAppConfigs-docs/Infrastructure/LAN-DNS.md).
# DNS for these names is served by the Pi-hole quadlet on this host
# (dankclaw/pihole/addn-hosts — every name resolves to 192.168.0.143)
# and handed to LAN clients by the SURFboard G36 router.
#
# Authentication chain (2026-08-29): the guarded vhosts (homepage, SearXNG,
# Hermes, Prometheus, RomM, Copyparty) proxy EVERYTHING through the Anubis
# bot-challenge proxy first, then the whole request is proxied THROUGH
# Authentik (2026.8 proxy-provider mode: this build serves no forward_auth
# endpoint; the authentication flow runs inline — unauthenticated clients get
# the login UI served in place, and once a session exists Authentik proxies
# the request on to the app via the provider's internal_host). Chain: client →
# Caddy (TLS, public listener) → Anubis :8923 (verify: DENY known AI scrapers,
# CHALLENGE everyone else) → Caddy's internal listener http://:8016 (auth
# decision; Host headers survive every hop) → authenticated traffic is
# forwarded back to http://:8017 and routed straight to the app.
# Apps with their own auth (Grafana, Gitea, OpenWebUI, ComfyUI, Jellyfin,
# *arr, Pi-hole) and the Authentik IdP itself (auth.franksdank.cloud — its
# login flow must never sit behind a challenge) are exempt by construction:
# their vhosts never route through Anubis. The basicauth hash is a
# placeholder; deploy.sh renders the real hash (CADDY_BASICAUTH_HASH from the
# host .env, git-ignored) into /home/dank/Containers/caddy/Caddyfile, which
# the container mounts.
#
# TLS: real Let's Encrypt certs. Site addresses carry no http:// prefix,
# so Caddy serves HTTPS (and auto-redirects HTTP). Certs are issued via
# dns-01 through the Porkbun module built into the franksdank/caddy-porkbun
# image (caddy/Containerfile). That needs no inbound ports and works from the
# LAN, where Pi-hole resolves these names to 192.168.0.143.
{
# Admin stays ON: it binds to 127.0.0.1:2019 inside the container and the
# port is not published, so nothing outside the container can reach it.
# It powers the config file watcher, so a rendered-config change
# hot-reloads without a container restart. 'admin off' once left a broken
# basicauth hash sitting on disk unapplied for hours (2026-08-22).
email admin@franksdank.cloud
# Solve ACME challenges for every site via Porkbun DNS (no port 80/443
# reachability needed). Credentials come from the container environment
# (compose.yaml, from the host .env — never committed).
acme_dns porkbun {
api_key {env.PORKBUN_API_KEY}
api_secret_key {env.PORKBUN_API_SECRET_KEY}
}
}
# --- Bare-IP access (health checks, convenience) ---
:80 {
redir https://www.franksdank.cloud/ 308
}
# --- Homepage (Anubis → Authentik SSO) ---
# www holds the homepage; the naked domain redirects onto it at the TLS edge.
# A redirect needs no challenge, so unauthenticated naked traffic never enters
# the auth chain. The proxy provider's external_host must match the host that
# actually carries traffic (https://www.franksdank.cloud) or Authentik matches
# zero providers and answers its error page instead.
franksdank.cloud {
redir https://www.franksdank.cloud/ 308
}
www.franksdank.cloud {
reverse_proxy anubis:8923 {
header_up X-Real-Ip {remote_host}
}
}
# --- Legacy name: the old homepage name redirects straight onto www ---
dankclaw.franksdank.cloud {
redir https://www.franksdank.cloud/ 308
}
# --- Legacy name: the media host has no Caddy of its own anymore ---
dankserver.franksdank.cloud {
redir https://www.franksdank.cloud/ 308
}
# --- Authentik (SSO / OIDC provider) ---
# Exempt by construction: the IdP login flow must be reachable without a
# challenge, so this vhost never routes through Anubis.
auth.franksdank.cloud {
reverse_proxy authentik-server:9000
}
# ═══════════════════════════════════════════════════════════════
# Services on this host (dankclaw, 192.168.0.143)
# ═══════════════════════════════════════════════════════════════
# --- SearXNG (quadlet; behind the Gluetun tunnel, published 8081) ---
search.franksdank.cloud {
reverse_proxy anubis:8923 {
header_up X-Real-Ip {remote_host}
}
}
# --- ComfyUI ---
comfyui.franksdank.cloud {
reverse_proxy comfyui:8188
}
# --- OpenWebUI (quadlet on slirp4netns; published 3005) ---
chat.franksdank.cloud {
reverse_proxy 192.168.0.143:3005
}
# --- Hermes ---
hermes.franksdank.cloud {
reverse_proxy anubis:8923 {
header_up X-Real-Ip {remote_host}
}
}
# --- Gitea (quadlet; published 3000) ---
# Exempt by construction: Gitea has its own login, and git smart-HTTP
# clients (git push/fetch over HTTPS) cannot solve JS challenges. The
# policy would need a bypass for /info/refs + /git-*-pack paths if this
# ever moves behind Anubis.
gitea.franksdank.cloud {
reverse_proxy 192.168.0.143:3000
}
# --- Grafana ---
grafana.franksdank.cloud {
basicauth {
dankuser REPLACE_WITH_HASH
}
reverse_proxy grafana:3000
}
# --- Prometheus ---
prometheus.franksdank.cloud {
reverse_proxy anubis:8923 {
header_up X-Real-Ip {remote_host}
}
}
# --- Loki ---
loki.franksdank.cloud {
reverse_proxy loki:3100
}
# --- Pi-hole (quadlet on slirp4netns; published 8083) ---
pihole.franksdank.cloud {
basicauth {
dankuser REPLACE_WITH_HASH
}
reverse_proxy 192.168.0.143:8083
}
# ═══════════════════════════════════════════════════════════════
# Services on dankserver (192.168.0.235) — published LAN ports
# ═══════════════════════════════════════════════════════════════
# --- Plex (existing, kept) ---
# http:// on purpose: franksdank.casa is not hosted in the same Porkbun
# account as franksdank.cloud, so the DNS-01 provider cannot issue a
# Let's Encrypt cert for it. It stays plain-HTTP legacy; the canonical
# name is plex.franksdank.cloud (HTTPS, below).
http://plex.franksdank.casa {
reverse_proxy 192.168.0.235:32400 {
transport http {
tls_insecure_skip_verify
}
}
}
# PMS requires TLS from every non-loopback client: plain-HTTP off-loopback
# is read and silently closed (diagnosed 2026-08-26), which surfaced as an
# empty-body 502 here. Dial the upstream over TLS; PMS's self-signed cert is
# skipped on this pinned-LAN hop only (client-facing LE TLS is untouched).
# The web UI lives at /web/; / returns the XML MediaContainer index.
plex.franksdank.cloud {
reverse_proxy 192.168.0.235:32400 {
transport http {
tls_insecure_skip_verify
}
}
}
# --- Calibre-Web ---
books.franksdank.cloud {
reverse_proxy 192.168.0.235:8083
}
# --- RomM ---
romm.franksdank.cloud {
reverse_proxy anubis:8923 {
header_up X-Real-Ip {remote_host}
}
}
# --- Copyparty ---
files.franksdank.cloud {
reverse_proxy anubis:8923 {
header_up X-Real-Ip {remote_host}
}
}
# --- Jellyfin ---
jellyfin.franksdank.cloud {
reverse_proxy 192.168.0.235:8096
}
# --- *arr Stack ---
radarr.franksdank.cloud {
reverse_proxy 192.168.0.235:7878
}
sonarr.franksdank.cloud {
reverse_proxy 192.168.0.235:8989
}
lidarr.franksdank.cloud {
reverse_proxy 192.168.0.235:8686
}
readarr.franksdank.cloud {
reverse_proxy 192.168.0.235:8787
}
prowlarr.franksdank.cloud {
reverse_proxy 192.168.0.235:9696
}
qbittorrent.franksdank.cloud {
reverse_proxy 192.168.0.235:8080
}
# --- Internal listener: auth decision layer ---------------------------------
# Anubis dials http://caddy:8016 after verifying a client (TARGET in
# dankclaw/anubis/quadlet/anubis.container). Host headers survive the hop.
# Every request here is proxied whole-request to Authentik with Host
# preserved. This build's embedded outpost routes ONLY on the ORIGINAL Host
# against each proxy provider's external_host — meta headers are ignored and
# cross-host absolute next= redirects are rejected by the flow's open-redirect
# guard ("Request has been denied: Invalid next URL"), verified live.
# Unauthenticated clients get the login flow served in place; authenticated
# sessions are forwarded by the provider's internal_host back to
# http://:8017 and routed straight to the app. Every host that may enter this
# listener MUST have a proxy provider registered with the embedded outpost —
# the Anubis guard set, the Caddy TLS edge, and the provider list stay in
# lockstep. Exempt vhosts never enter this block.
http://:8016 {
reverse_proxy authentik-server:9000 {
header_up X-Forwarded-Proto https
}
}
# --- Post-auth listener: where Authentik forwards authenticated traffic ----
# The proxy provider's internal_host is http://caddy:8017, so every request
# that passes authentication lands here (Host header preserved) and routes
# straight to its app. This is the end of the chain — nothing dials back
# into Authentik from here, so the chain cannot loop.
http://:8017 {
@home host www.franksdank.cloud
handle @home {
root * /srv/homepage
handle /admin* {
try_files /admin.html
file_server
}
@data path /services.json /status.json /app.js /style.css /deployed.js
handle @data {
file_server
}
handle {
try_files /index.html
file_server
}
}
@search host search.franksdank.cloud
handle @search {
reverse_proxy 192.168.0.143:8081
}
@hermes host hermes.franksdank.cloud
handle @hermes {
reverse_proxy 192.168.0.143:9119
}
@prometheus host prometheus.franksdank.cloud
handle @prometheus {
reverse_proxy prometheus:9090
}
@romm host romm.franksdank.cloud
handle @romm {
reverse_proxy 192.168.0.235:8085
}
@files host files.franksdank.cloud
handle @files {
reverse_proxy 192.168.0.235:3923
}
}
# --- Optional: Jackett ---
# jackett.franksdank.cloud {
# reverse_proxy 192.168.0.235:9117
# }
# --- Optional: Whisparr ---
# whisparr.franksdank.cloud {
# reverse_proxy 192.168.0.235:6969
# }