From 6c48bdc7071f9893d9a920dd9554702868bd3c3d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 04:11:24 +0000 Subject: [PATCH 1/2] Fix Caddy reverse-proxy target for host-network services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Caddy runs in its own container on the caddy_net bridge network, so "localhost" in a Caddyfile site block resolves to Caddy's own container — never the host, and never a sibling container. That broke every reverse proxy pointed at a network_mode: host service (confirmed live with asterisk-do's web admin): once nothing else (like a forward_auth redirect) intercepted the request first, Caddy couldn't actually reach the upstream. - services/caddy.sh: add extra_hosts so host.docker.internal resolves inside the Caddy container (Linux Docker needs this explicitly — it's automatic only on Docker Desktop). - lib/common.sh's configure_caddy_for_service: bare-port upstreams (its documented "host-network service" case) now target host.docker.internal instead of localhost. - services/asterisk-do.sh: its self-contained Caddy block (doesn't go through configure_caddy_for_service) gets the same fix for local Caddy, and now correctly targets the droplet's public IP instead of localhost for the remote-Caddy snippet case, which had the same bug. services/asterisk.sh needs no direct change — it already goes through configure_caddy_for_service, so it inherits the fix. --- lib/common.sh | 13 +++++++++---- services/asterisk-do.sh | 11 ++++++++++- services/caddy.sh | 6 ++++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/lib/common.sh b/lib/common.sh index 91c00df..cbd3578 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -416,12 +416,17 @@ configure_caddy_for_service() { local SERVICE_NAME="$1" SERVICE_UPSTREAM="$2" DEFAULT_SUBDOMAIN="$3" EXTRA_CONFIG="${4:-}" # Derive the proxy upstream and a port number for display messages. - # Plain number → localhost:PORT (host-network or legacy services) - # name:port → used as-is (preferred: service on shared caddy_net) + # Plain number → host.docker.internal:PORT (host-network or legacy + # services — Caddy itself runs in its own container on + # caddy_net, a bridge network, so "localhost" here would + # resolve to Caddy's own container, not the host. Requires + # the extra_hosts entry set in services/caddy.sh's compose + # file — see the comment there.) + # name:port → used as-is (preferred: service on shared caddy_net) local _UPSTREAM _DISPLAY_PORT case "$SERVICE_UPSTREAM" in - *:*) _UPSTREAM="$SERVICE_UPSTREAM"; _DISPLAY_PORT="${SERVICE_UPSTREAM##*:}" ;; - *) _UPSTREAM="localhost:$SERVICE_UPSTREAM"; _DISPLAY_PORT="$SERVICE_UPSTREAM" ;; + *:*) _UPSTREAM="$SERVICE_UPSTREAM"; _DISPLAY_PORT="${SERVICE_UPSTREAM##*:}" ;; + *) _UPSTREAM="host.docker.internal:$SERVICE_UPSTREAM"; _DISPLAY_PORT="$SERVICE_UPSTREAM" ;; esac # ── Determine Caddy mode ────────────────────────────────────────────────── diff --git a/services/asterisk-do.sh b/services/asterisk-do.sh index 9379bbf..0761624 100755 --- a/services/asterisk-do.sh +++ b/services/asterisk-do.sh @@ -769,12 +769,21 @@ ENV local _CADDY_MODE="local" [[ ! -d "$DOCKER_DIR/caddy" ]] && [[ -n "${CADDY_REMOTE_HOST:-}" ]] && _CADDY_MODE="remote" + # Asterisk runs with network_mode: host, so whatever proxies to it + # needs a way to reach the host, not "localhost" (which resolves + # to the proxying container's own netns). A local Caddy container + # reaches the host via host.docker.internal (wired up in + # services/caddy.sh's compose file); a remote Caddy machine needs + # this droplet's actual public IP instead. + local _PROXY_TARGET="host.docker.internal:${WEB_ADMIN_PORT_VAL}" + [[ "$_CADDY_MODE" == "remote" ]] && _PROXY_TARGET="${PUBLIC_IP}:${WEB_ADMIN_PORT_VAL}" + local _SITE_BLOCK _SITE_BLOCK="$(cat << CADDY_BLOCK # Asterisk Web Admin ${DOMAIN_NAME} { - reverse_proxy localhost:${WEB_ADMIN_PORT_VAL} + reverse_proxy ${_PROXY_TARGET} header { Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" diff --git a/services/caddy.sh b/services/caddy.sh index 0ef9e46..483e34c 100644 --- a/services/caddy.sh +++ b/services/caddy.sh @@ -267,6 +267,12 @@ services: - ACME_AGREE=true labels: - "io.podman.annotations.label/crowdsec.enable=true" + # Lets Caddyfile blocks reach services that use network_mode: host + # (e.g. asterisk/asterisk-do) via "host.docker.internal:PORT" — Caddy + # itself is on the caddy_net bridge network below, so plain "localhost" + # in a site block resolves to Caddy's own container, not the host. + extra_hosts: + - "host.docker.internal:host-gateway" networks: - caddy_net From b6046b3ca110ef8e8587008551a117d52aa8d1fd Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 04:14:56 +0000 Subject: [PATCH 2/2] Ensure caddy_net exists via require_docker instead of per-service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 43 services declare caddy_net as "external: true" in their compose file, meaning they require it to already exist — but only Caddy's own compose file actually creates it (authelia.sh was the sole exception, with its own inline check-and-create). Installing any of the other 42 before Caddy fails outright with "network caddy_net declared as external, but could not be found." Adds ensure_caddy_network to lib/common.sh, called from require_docker (which every install_* function already calls first), so the network exists regardless of install order without touching each service file. Removes authelia.sh's now-redundant duplicate of the same check. Also documents in CLAUDE.md that network_mode: host services (asterisk/ asterisk-do) need host.docker.internal, not localhost, when Caddy reverse-proxies to them — the fix from the previous commit. --- CLAUDE.md | 16 ++++++++++++++++ lib/common.sh | 20 ++++++++++++++++++++ services/authelia.sh | 9 ++------- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e0db519..9ed1803 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -330,3 +330,19 @@ networks: ``` And read the network name from `.env` using `CADDY_NET=$SITE_CADDY_NET`. + +`external: true` means *this* service expects the network to already exist — +it doesn't create it. `require_docker` creates it for you (via +`ensure_caddy_network` in `lib/common.sh`) the first time any service calls +it, so as long as your `install_()` calls `require_docker` before +`docker compose up` (it always should), the network is guaranteed to exist +regardless of whether Caddy itself has been installed yet. + +**`network_mode: host` services (e.g. `asterisk`/`asterisk-do`) don't join +`caddy_net` at all** — Caddy reaching them (or anything else on the host +network) needs `host.docker.internal:PORT` in the Caddyfile, not +`localhost:PORT` or a container name. Caddy's own compose file +(`services/caddy.sh`) sets `extra_hosts: host.docker.internal:host-gateway` +so that hostname resolves; `configure_caddy_for_service`'s bare-port upstream +case already does this for you — don't hand-roll `localhost:PORT` in a +Caddy site block. diff --git a/lib/common.sh b/lib/common.sh index cbd3578..169f5a8 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -164,6 +164,7 @@ require_root() { require_docker() { if command -v docker &>/dev/null; then + ensure_caddy_network return 0 fi @@ -222,6 +223,25 @@ require_docker() { local _docker_bin _docker_bin="$(command -v docker 2>/dev/null || echo /usr/bin/docker)" log_success "Docker installed ($("$_docker_bin" --version 2>/dev/null))" + + ensure_caddy_network +} + +# Create the shared caddy_net bridge network if it doesn't exist yet. +# Most services declare it "external: true" in their docker-compose.yml (see +# CLAUDE.md → Caddy network wiring) — meaning THEY require it to already +# exist, and only Caddy's own compose file (services/caddy.sh) actually +# creates it. Installing any caddy_net-dependent service before Caddy would +# otherwise fail outright with "network caddy_net declared as external, but +# could not be found." Called from require_docker so every service gets this +# for free regardless of install order. No-op in DRY_RUN; safe/idempotent +# otherwise — docker network create is a no-op if the network already exists. +ensure_caddy_network() { + [ "$DRY_RUN" = true ] && return 0 + local _net="${SITE_CADDY_NET:-caddy_net}" + docker network inspect "$_net" &>/dev/null && return 0 + docker network create "$_net" &>/dev/null \ + && log_info "Created Docker network ${_net} (needed by Caddy-fronted services)" } # ── SSH client config (~/.ssh/config) Host aliases ──────────────────────────── diff --git a/services/authelia.sh b/services/authelia.sh index 39625c2..602cbf3 100644 --- a/services/authelia.sh +++ b/services/authelia.sh @@ -387,13 +387,8 @@ AUTHELIA_USERS chown -R 1000:1000 "$AUTHELIA_DIR/config" "$AUTHELIA_DIR/data" log_success "Authelia configured at $AUTHELIA_DIR" - # ── Docker network ──────────────────────────────────────────────────────── - if ! docker network ls --format '{{.Name}}' | grep -q "^${CADDY_NET}$"; then - docker network create "$CADDY_NET" >/dev/null 2>&1 && echo " ✓ Created docker network ${CADDY_NET}" \ - || echo " ⚠ Failed to create ${CADDY_NET}" - else - echo " ✓ Docker network ${CADDY_NET} already exists" - fi + # $CADDY_NET already exists at this point — require_docker (called at the + # top of this function) creates it via ensure_caddy_network in lib/common.sh. # ── Caddyfile forward-auth snippet + portal block ──────────────────────── local CADDY_FILE="$DOCKER_DIR/caddy/Caddyfile"