fix: make ops/deploy.sh rebuild, dry-run safely, and join Traefik at start

- Always build from the checkout (`up --build`, `pull_policy: build`)
  instead of reusing a stale local image or pulling an old registry one.
- --render-only writes to rendered/ and no longer touches the live
  Traefik dynamic directory; the route is installed after health check.
- Join the Traefik network via a generated docker-compose.override.yaml
  rather than `docker network connect` after startup, which raced the
  control-plane's startup call to wg-easy and was lost on recreate.
- Add --non-interactive, check the public HTTPS endpoint, and point the
  closing instructions at Settings -> Gateway (the suggested
  `furtka gateway connect` command doesn't exist).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Robert Syrnicki 2026-10-07 15:49:49 +02:00
parent e240f071d5
commit 6b8cc6e006
5 changed files with 196 additions and 172 deletions

View file

@ -58,12 +58,10 @@ GATEWAY_CERT_RESOLVER=
GATEWAY_CONTROL_PLANE_HOST=gw.example.com GATEWAY_CONTROL_PLANE_HOST=gw.example.com
# Where the control-plane's own API is bound (docker-compose.yaml's port # Where the control-plane's own API is bound (docker-compose.yaml's port
# publish) and where your Traefik reaches it, respectively. Same value # publish) and where your Traefik reaches it, respectively. For the
# for the recommended same-docker-network setup (ops/deploy.sh sets both # recommended same-docker-network setup, ops/deploy.sh keeps the bind
# to furtka-gateway-control-plane:8090 automatically, with the bind # loopback-only and sets the target to the fixed container name
# staying loopback-only — see docker-compose.yaml's comment on why this # furtka-gateway-control-plane:8090.
# is more robust than the compose *service* name "control-plane" once the
# network is joined after the fact via `docker network connect`).
# Different values only for a separate-host setup, where # Different values only for a separate-host setup, where
# GATEWAY_CONTROL_PLANE_BIND must be widened beyond 127.0.0.1 — put real # GATEWAY_CONTROL_PLANE_BIND must be widened beyond 127.0.0.1 — put real
# access control around that bind address, this file carries a WireGuard # access control around that bind address, this file carries a WireGuard
@ -74,8 +72,8 @@ GATEWAY_CONTROL_PLANE_TARGET=furtka-gateway-control-plane:8090
# Same-docker-network setup only (the recommended path — see the README's # Same-docker-network setup only (the recommended path — see the README's
# Prerequisites checklist): your Traefik's docker network name # Prerequisites checklist): your Traefik's docker network name
# (`docker network ls` / `docker inspect <your-traefik-container>`). # (`docker network ls` / `docker inspect <your-traefik-container>`).
# ops/deploy.sh runs `docker network connect` with this after bringing the # ops/deploy.sh writes a docker-compose.override.yaml that joins this
# stack up. Leave blank for a separate-host setup. # network. Leave blank for a separate-host setup.
GATEWAY_TRAEFIK_NETWORK= GATEWAY_TRAEFIK_NETWORK=
# The one box token a single-tenant deployment accepts at # The one box token a single-tenant deployment accepts at

3
.gitignore vendored
View file

@ -10,3 +10,6 @@ traefik/dynamic/route-*.yml
traefik/dynamic/control-plane.yml traefik/dynamic/control-plane.yml
traefik/dynamic/default-cert.yml traefik/dynamic/default-cert.yml
traefik/static/traefik.active.yml traefik/static/traefik.active.yml
examples/starter-stack/dynamic/
rendered/
docker-compose.override.yaml

View file

@ -1,13 +1,12 @@
services: services:
control-plane: control-plane:
build: ./control_plane build: ./control_plane
# CI (.forgejo/workflows/build-image.yml) publishes this same build to # Always build from this checkout — never pull a registry image that may
# the registry on every push to master, tagged `latest` plus the short # be older than the code you just pulled. The image name below only
# commit sha. `docker compose up` builds fresh from `build:` above and # matters for `docker stack deploy` (Swarm can't build; see the Swarm
# tags the result as this name, ignoring this being a registry ref; # caveats in README.md), which pulls what CI publishes on every push to
# `docker stack deploy` (Swarm ignores `build:` entirely, see the # master (`latest` plus the short commit sha).
# Swarm caveats in README.md) pulls it instead — no manual pull_policy: build
# `docker build -t ... ./control_plane` step needed there anymore.
image: ${GATEWAY_CONTROL_PLANE_IMAGE:-forgejo.sourcegate.online/robert/furtka-gateway-control-plane:latest} image: ${GATEWAY_CONTROL_PLANE_IMAGE:-forgejo.sourcegate.online/robert/furtka-gateway-control-plane:latest}
container_name: furtka-gateway-control-plane container_name: furtka-gateway-control-plane
environment: environment:
@ -58,14 +57,10 @@ services:
# implication of doing that when you choose it. # implication of doing that when you choose it.
- "${GATEWAY_CONTROL_PLANE_BIND:-127.0.0.1:8090}:8090" - "${GATEWAY_CONTROL_PLANE_BIND:-127.0.0.1:8090}:8090"
restart: unless-stopped restart: unless-stopped
# No custom `networks:` here on purpose — this stays on the default # No `networks:` here on purpose — your Traefik's network name differs
# bridge network compose creates unless you also run YOUR Traefik in # per setup. ops/deploy.sh writes a gitignored
# this SAME compose project. To reach a Traefik running elsewhere (its # docker-compose.override.yaml (auto-loaded by `docker compose`) that
# own compose project, or your own homelab stack), ops/deploy.sh joins # joins this container to it from the first second.
# this container to your Traefik's existing docker network after
# `up` via `docker network connect` — no external-network dependency
# baked into this file, so it works whether or not that network
# exists yet at compose-up time.
volumes: volumes:
control_plane_data: control_plane_data:

View file

@ -1,24 +1,23 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Interactive setup for furtka-gateway's control-plane, plugged into a # Sets up furtka-gateway's control-plane, plugged into a Traefik and
# Traefik and wg-easy YOU already run (as sibling containers, or otherwise # wg-easy YOU already run. Read the README's "Existing Traefik + wg-easy"
# reachable) — see the root README's Prerequisites checklist before # section first — in particular the routing requirement, which this script
# running this. No zero-infra option here: for that, use # can't set up or verify for you. On an empty server, use
# examples/starter-stack/deploy.sh instead, which bundles its own # examples/starter-stack/ instead.
# Traefik+wg-easy.
# #
# Safe to re-run — existing .env values become the defaults for each # Safe to re-run — existing .env values become the defaults for each
# prompt (press Enter to keep them), and rendering is always fully # prompt (press Enter to keep them).
# regenerated from the current .env, never patched in place.
# #
# Usage: # Usage:
# ops/deploy.sh full setup: prompts, render, compose up, # ops/deploy.sh prompts, writes .env, starts the
# network join, health check # control-plane, installs its Traefik
# ops/deploy.sh --render-only only (re)write .env and the rendered # route, health-checks it
# traefik/dynamic/control-plane.yml — # ops/deploy.sh --non-interactive same, but no prompts: every value
# skips compose up / network join / # comes from .env (fails if one is missing)
# health check. Useful after hand-editing # ops/deploy.sh --render-only only (re)write .env and
# .env, or to inspect what would be # rendered/control-plane.yml — touches
# generated before touching anything live. # nothing live (not even your Traefik's
# dynamic directory)
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@ -26,9 +25,14 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$REPO_ROOT" cd "$REPO_ROOT"
RENDER_ONLY=0 RENDER_ONLY=0
if [[ "${1:-}" == "--render-only" ]]; then INTERACTIVE=1
RENDER_ONLY=1 for arg in "$@"; do
fi case "$arg" in
--render-only) RENDER_ONLY=1 ;;
--non-interactive) INTERACTIVE=0 ;;
*) echo "unknown option: $arg" >&2; exit 2 ;;
esac
done
ENV_FILE=.env ENV_FILE=.env
@ -53,117 +57,109 @@ ask() {
local __var="$1" __prompt="$2" __fallback="${3:-}" local __var="$1" __prompt="$2" __fallback="${3:-}"
local __current="${!__var:-}" local __current="${!__var:-}"
local __default="${__current:-$__fallback}" local __default="${__current:-$__fallback}"
local __ans local __ans=""
if [[ "$INTERACTIVE" -eq 1 ]]; then
if [[ -n "$__default" ]]; then if [[ -n "$__default" ]]; then
read -r -p "$__prompt [$__default]: " __ans read -r -p "$__prompt [$__default]: " __ans
else else
read -r -p "$__prompt: " __ans read -r -p "$__prompt: " __ans
fi fi
fi
printf -v "$__var" '%s' "${__ans:-$__default}" printf -v "$__var" '%s' "${__ans:-$__default}"
} }
ask_secret() { ask_secret() {
local __var="$1" __prompt="$2" local __var="$1" __prompt="$2"
local __current="${!__var:-}" local __current="${!__var:-}"
local __ans local __ans=""
if [[ "$INTERACTIVE" -eq 1 ]]; then
if [[ -n "$__current" ]]; then if [[ -n "$__current" ]]; then
read -r -s -p "$__prompt [unchanged, Enter to keep]: " __ans read -r -s -p "$__prompt [unchanged, Enter to keep]: " __ans
else else
read -r -s -p "$__prompt: " __ans read -r -s -p "$__prompt: " __ans
fi fi
echo echo
fi
printf -v "$__var" '%s' "${__ans:-$__current}" printf -v "$__var" '%s' "${__ans:-$__current}"
} }
require() {
# require VARNAME — fail if empty
if [[ -z "${!1:-}" ]]; then
echo "$1 is required" >&2
exit 1
fi
}
require_cmd() { require_cmd() {
command -v "$1" >/dev/null 2>&1 || { echo "missing required command: $1" >&2; exit 1; } command -v "$1" >/dev/null 2>&1 || { echo "missing required command: $1" >&2; exit 1; }
} }
require_cmd openssl
if [[ "$RENDER_ONLY" -eq 0 ]]; then if [[ "$RENDER_ONLY" -eq 0 ]]; then
require_cmd docker require_cmd docker
require_cmd openssl
require_cmd curl require_cmd curl
fi fi
echo "== furtka-gateway deploy (bring your own Traefik/wg-easy) ==" echo "== furtka-gateway: connect to your existing Traefik + wg-easy =="
echo "Haven't read the README's Prerequisites checklist yet? Do that first —"
echo "this script can't verify your Traefik has a real network path to"
echo "wg-easy's WireGuard peer subnet, and a broken deploy from skipping"
echo "that is hard to debug after the fact."
echo echo
# ---- tenancy mode ---- # ---- tenancy mode ----
while true; do ask GATEWAY_MODE "Mode (single / shared)" "single"
ask GATEWAY_MODE "Mode (single / shared)" "single" case "$GATEWAY_MODE" in
case "$GATEWAY_MODE" in single|shared) ;;
single|shared) break ;; *) echo "GATEWAY_MODE must be single or shared" >&2; exit 1 ;;
*) echo "must be one of: single, shared" ;; esac
esac
done
# ---- wg-easy ---- # ---- wg-easy ----
echo echo
ask WG_EASY_URL "Your wg-easy's admin API URL (e.g. http://wg-easy:51821)" "" ask WG_EASY_URL "Your wg-easy's admin API URL, as the control-plane will reach it (e.g. http://wg-easy:51821)" ""
if [[ -z "$WG_EASY_URL" ]]; then require WG_EASY_URL
echo "WG_EASY_URL is required" >&2 ask WG_EASY_API_VERSION "Your wg-easy image's major version (14 or 15 — check the tag)" "15"
exit 1 case "$WG_EASY_API_VERSION" in
fi 14|15) ;;
while true; do *) echo "WG_EASY_API_VERSION must be 14 or 15 (different, incompatible APIs)" >&2; exit 1 ;;
ask WG_EASY_API_VERSION "Your wg-easy image's version (14 or 15 — check the tag)" "14" esac
case "$WG_EASY_API_VERSION" in
14|15) break ;;
*) echo "must be 14 or 15 — these are different, incompatible APIs (see control_plane/wgeasy.py vs wgeasy_v15.py)" ;;
esac
done
if [[ "$WG_EASY_API_VERSION" == "15" ]]; then if [[ "$WG_EASY_API_VERSION" == "15" ]]; then
ask WG_EASY_ADMIN_USERNAME "wg-easy admin username" "admin" ask WG_EASY_ADMIN_USERNAME "wg-easy admin username" "admin"
ask GATEWAY_WG_ALLOWED_IPS "wg-easy's WireGuard peer subnet (restricts new clients to split-tunnel)" "10.8.0.0/24" ask GATEWAY_WG_ALLOWED_IPS "wg-easy's WireGuard peer subnet (new clients get split-tunnel to this)" "10.8.0.0/24"
fi fi
ask_secret WG_EASY_ADMIN_PASSWORD "wg-easy admin password" ask_secret WG_EASY_ADMIN_PASSWORD "wg-easy admin password"
if [[ -z "${WG_EASY_ADMIN_PASSWORD:-}" ]]; then require WG_EASY_ADMIN_PASSWORD
echo "wg-easy admin password is required" >&2
exit 1
fi
# ---- Traefik ---- # ---- Traefik ----
echo echo
ask GATEWAY_DYNAMIC_DIR_HOST "Host directory YOUR Traefik's file provider watches (or a new one you'll add as an extra providers.file directory)" "" ask GATEWAY_DYNAMIC_DIR_HOST "Host directory your Traefik's file provider watches" ""
if [[ -z "$GATEWAY_DYNAMIC_DIR_HOST" ]]; then require GATEWAY_DYNAMIC_DIR_HOST
echo "GATEWAY_DYNAMIC_DIR_HOST is required" >&2 ask GATEWAY_CERT_RESOLVER "Your Traefik's certResolver name (blank if a default cert applies automatically)" ""
exit 1 ask GATEWAY_CONTROL_PLANE_HOST "Gateway API hostname (what you enter on the box)" ""
fi require GATEWAY_CONTROL_PLANE_HOST
ask GATEWAY_CERT_RESOLVER "Your Traefik's certResolver name (blank if it applies a default cert automatically)" ""
ask GATEWAY_CONTROL_PLANE_HOST "Control-plane API hostname (its own A/AAAA record)" "gw.example.com"
echo echo
echo "How does YOUR Traefik reach this control-plane container?" echo "How does your Traefik reach the control-plane container?"
echo " 1) same docker network as my Traefik (recommended)" echo " 1) it's on the same docker network as my Traefik (recommended)"
echo " 2) separate host — I'll expose it at a reachable host:port myself" echo " 2) separate host — I'll expose it at a reachable host:port myself"
while true; do ask GATEWAY_REACHABILITY_MODE "Choice (1 or 2)" "1"
ask GATEWAY_REACHABILITY_MODE "Choice (1 or 2)" "1" case "$GATEWAY_REACHABILITY_MODE" in
case "$GATEWAY_REACHABILITY_MODE" in 1)
1|2) break ;; ask GATEWAY_TRAEFIK_NETWORK "Your Traefik's docker network name (see: docker inspect <traefik> --format '{{json .NetworkSettings.Networks}}')" ""
*) echo "must be 1 or 2" ;; require GATEWAY_TRAEFIK_NETWORK
esac
done
if [[ "$GATEWAY_REACHABILITY_MODE" == "1" ]]; then
ask GATEWAY_TRAEFIK_NETWORK "Your Traefik's docker network name (docker network ls / docker inspect <traefik-container>)" ""
if [[ -z "$GATEWAY_TRAEFIK_NETWORK" ]]; then
echo "a network name is required for this choice" >&2
exit 1
fi
GATEWAY_CONTROL_PLANE_BIND="127.0.0.1:8090" GATEWAY_CONTROL_PLANE_BIND="127.0.0.1:8090"
GATEWAY_CONTROL_PLANE_TARGET="furtka-gateway-control-plane:8090" GATEWAY_CONTROL_PLANE_TARGET="furtka-gateway-control-plane:8090"
else ;;
2)
echo echo
echo "WARNING: this binds the control-plane API beyond loopback. Its" echo "WARNING: this binds the control-plane API beyond loopback. Its"
echo "registration response carries a WireGuard private key in plaintext" echo "registration response carries a WireGuard private key in plaintext"
echo "JSON — put real access control (firewall/security group) around" echo "JSON — put real access control (firewall/security group) around"
echo "whatever bind address you give it, not just this compose file." echo "whatever bind address you give it."
ask GATEWAY_CONTROL_PLANE_BIND "Bind address:port to expose it on (e.g. 10.0.0.5:8090)" "" ask GATEWAY_CONTROL_PLANE_BIND "Bind address:port to expose it on (e.g. 10.0.0.5:8090)" ""
ask GATEWAY_CONTROL_PLANE_TARGET "The same host:port, as YOUR Traefik will reach it" "$GATEWAY_CONTROL_PLANE_BIND" require GATEWAY_CONTROL_PLANE_BIND
ask GATEWAY_CONTROL_PLANE_TARGET "The same host:port, as your Traefik will reach it" "$GATEWAY_CONTROL_PLANE_BIND"
GATEWAY_TRAEFIK_NETWORK="" GATEWAY_TRAEFIK_NETWORK=""
fi ;;
*) echo "GATEWAY_REACHABILITY_MODE must be 1 or 2" >&2; exit 1 ;;
esac
# ---- tokens (generated once, kept on re-run) ---- # ---- tokens (generated once, kept on re-run) ----
GATEWAY_BOX_TOKEN="${GATEWAY_BOX_TOKEN:-$(openssl rand -hex 32)}" GATEWAY_BOX_TOKEN="${GATEWAY_BOX_TOKEN:-$(openssl rand -hex 32)}"
@ -183,74 +179,111 @@ echo "Writing $ENV_FILE"
echo "GATEWAY_DYNAMIC_DIR_HOST=$GATEWAY_DYNAMIC_DIR_HOST" echo "GATEWAY_DYNAMIC_DIR_HOST=$GATEWAY_DYNAMIC_DIR_HOST"
echo "GATEWAY_CERT_RESOLVER=$GATEWAY_CERT_RESOLVER" echo "GATEWAY_CERT_RESOLVER=$GATEWAY_CERT_RESOLVER"
echo "GATEWAY_CONTROL_PLANE_HOST=$GATEWAY_CONTROL_PLANE_HOST" echo "GATEWAY_CONTROL_PLANE_HOST=$GATEWAY_CONTROL_PLANE_HOST"
echo "GATEWAY_REACHABILITY_MODE=$GATEWAY_REACHABILITY_MODE"
echo "GATEWAY_CONTROL_PLANE_BIND=$GATEWAY_CONTROL_PLANE_BIND" echo "GATEWAY_CONTROL_PLANE_BIND=$GATEWAY_CONTROL_PLANE_BIND"
echo "GATEWAY_CONTROL_PLANE_TARGET=$GATEWAY_CONTROL_PLANE_TARGET" echo "GATEWAY_CONTROL_PLANE_TARGET=$GATEWAY_CONTROL_PLANE_TARGET"
[[ -n "$GATEWAY_TRAEFIK_NETWORK" ]] && echo "GATEWAY_TRAEFIK_NETWORK=$GATEWAY_TRAEFIK_NETWORK" [[ -n "$GATEWAY_TRAEFIK_NETWORK" ]] && echo "GATEWAY_TRAEFIK_NETWORK=$GATEWAY_TRAEFIK_NETWORK"
[[ -n "${GATEWAY_CONTROL_PLANE_IMAGE:-}" ]] && echo "GATEWAY_CONTROL_PLANE_IMAGE=$GATEWAY_CONTROL_PLANE_IMAGE"
echo "GATEWAY_BOX_TOKEN=$GATEWAY_BOX_TOKEN" echo "GATEWAY_BOX_TOKEN=$GATEWAY_BOX_TOKEN"
echo "GATEWAY_ADMIN_TOKEN=$GATEWAY_ADMIN_TOKEN" echo "GATEWAY_ADMIN_TOKEN=$GATEWAY_ADMIN_TOKEN"
} > "$ENV_FILE" } > "$ENV_FILE"
chmod 600 "$ENV_FILE" chmod 600 "$ENV_FILE"
# ---- render traefik/dynamic/control-plane.yml ---- # ---- join your Traefik's network from the compose file itself ----
echo "Rendering traefik/dynamic/control-plane.yml" # An override (auto-loaded by `docker compose`, gitignored) rather than a
mkdir -p "$GATEWAY_DYNAMIC_DIR_HOST" # `docker network connect` after startup: the control-plane talks to
# Only the http: block onward — the .example's header comment explains the # wg-easy as soon as it starts, so the network must be there from the
# placeholders BY NAME (for a human reading that template), which would # first second, and it must survive container recreation.
# otherwise also get corrupted by these same substitutions. if [[ -n "$GATEWAY_TRAEFIK_NETWORK" ]]; then
sed -n '/^http:/,$p' traefik/dynamic/control-plane.yml.example > "$GATEWAY_DYNAMIC_DIR_HOST/control-plane.yml" echo "Writing docker-compose.override.yaml (joins network: $GATEWAY_TRAEFIK_NETWORK)"
sed -i "s|GATEWAY_CONTROL_PLANE_HOST|$GATEWAY_CONTROL_PLANE_HOST|" "$GATEWAY_DYNAMIC_DIR_HOST/control-plane.yml" cat > docker-compose.override.yaml <<EOF
sed -i "s|GATEWAY_CONTROL_PLANE_TARGET|$GATEWAY_CONTROL_PLANE_TARGET|" "$GATEWAY_DYNAMIC_DIR_HOST/control-plane.yml" # Generated by ops/deploy.sh — joins your Traefik's docker network.
if [[ -z "$GATEWAY_CERT_RESOLVER" ]]; then services:
# No resolver configured on your Traefik — drop the tls: block entirely control-plane:
# so its own default cert applies, mirroring what GATEWAY_CERT_RESOLVER networks:
# blank means for every app route (see traefikconf.py). - default
sed -i "/^ tls:$/,/^ certResolver:.*$/d" "$GATEWAY_DYNAMIC_DIR_HOST/control-plane.yml" - traefik
networks:
traefik:
name: $GATEWAY_TRAEFIK_NETWORK
external: true
EOF
else else
sed -i "s|certResolver: le|certResolver: $GATEWAY_CERT_RESOLVER|" "$GATEWAY_DYNAMIC_DIR_HOST/control-plane.yml" rm -f docker-compose.override.yaml
fi
# ---- render the control-plane's Traefik route (not yet installed) ----
echo "Writing rendered/control-plane.yml"
mkdir -p rendered
# Only the http: block onward — the .example's header comment explains the
# placeholders BY NAME, which these substitutions would otherwise corrupt.
sed -n '/^http:/,$p' traefik/dynamic/control-plane.yml.example > rendered/control-plane.yml
sed -i "s|GATEWAY_CONTROL_PLANE_HOST|$GATEWAY_CONTROL_PLANE_HOST|" rendered/control-plane.yml
sed -i "s|GATEWAY_CONTROL_PLANE_TARGET|$GATEWAY_CONTROL_PLANE_TARGET|" rendered/control-plane.yml
if [[ -z "$GATEWAY_CERT_RESOLVER" ]]; then
# No resolver on your Traefik — drop the tls: block so its default cert
# applies, mirroring what a blank GATEWAY_CERT_RESOLVER means for every
# app route (see traefikconf.py).
sed -i "/^ tls:$/,/^ certResolver:.*$/d" rendered/control-plane.yml
else
sed -i "s|certResolver: le|certResolver: $GATEWAY_CERT_RESOLVER|" rendered/control-plane.yml
fi fi
if [[ "$RENDER_ONLY" -eq 1 ]]; then if [[ "$RENDER_ONLY" -eq 1 ]]; then
echo echo
echo "--render-only: stopping before compose up / network join / health check." echo "--render-only: nothing live was touched. Inspect .env and rendered/control-plane.yml."
exit 0 exit 0
fi fi
# ---- bring the control-plane up ---- # ---- bring the control-plane up (always built from this checkout) ----
echo echo
echo "== docker compose up -d ==" echo "== docker compose up -d --build =="
docker compose up -d docker compose up -d --build
# ---- join your Traefik's network, if that's the reachability choice ----
if [[ -n "$GATEWAY_TRAEFIK_NETWORK" ]]; then
echo
echo "== joining docker network: $GATEWAY_TRAEFIK_NETWORK =="
docker network connect "$GATEWAY_TRAEFIK_NETWORK" furtka-gateway-control-plane 2>/dev/null \
|| echo " (already connected, or connect failed — check \`docker network ls\` and the name above)"
fi
# ---- health check ----
echo echo
echo "== health check ==" echo "== health check =="
for _ in $(seq 1 10); do healthy=0
for _ in $(seq 1 30); do
if curl -sf "http://${GATEWAY_CONTROL_PLANE_BIND}/healthz" >/dev/null 2>&1; then if curl -sf "http://${GATEWAY_CONTROL_PLANE_BIND}/healthz" >/dev/null 2>&1; then
echo "control-plane is up." healthy=1
break break
fi fi
sleep 1 sleep 2
done done
if ! curl -sf "http://${GATEWAY_CONTROL_PLANE_BIND}/healthz" >/dev/null 2>&1; then if [[ "$healthy" -ne 1 ]]; then
echo "control-plane did not become healthy — check: docker compose logs control-plane" >&2 echo "control-plane did not become healthy — check: docker compose logs control-plane" >&2
echo "(a wrong WG_EASY_URL or wg-easy password shows up there)" >&2
exit 1 exit 1
fi fi
echo " ok: control-plane is up"
# ---- install the route into your Traefik's dynamic directory ----
mkdir -p "$GATEWAY_DYNAMIC_DIR_HOST"
cp rendered/control-plane.yml "$GATEWAY_DYNAMIC_DIR_HOST/control-plane.yml"
echo " installed $GATEWAY_DYNAMIC_DIR_HOST/control-plane.yml"
public_ok=0
for _ in $(seq 1 20); do
if curl -sf -m 10 "https://$GATEWAY_CONTROL_PLANE_HOST/healthz" >/dev/null 2>&1; then
public_ok=1
break
fi
sleep 3
done
if [[ "$public_ok" -eq 1 ]]; then
echo " ok: https://$GATEWAY_CONTROL_PLANE_HOST/healthz"
else
echo " WARNING: https://$GATEWAY_CONTROL_PLANE_HOST/healthz not reachable."
echo " Check that DNS points at your Traefik and that Traefik watches"
echo " $GATEWAY_DYNAMIC_DIR_HOST (providers.file.directory)."
fi
echo echo
echo "== done ==" echo "== done =="
echo "Still needed before a real box can connect:" echo "Before publishing apps, make sure your Traefik can reach wg-easy's peer"
echo " - Add $GATEWAY_DYNAMIC_DIR_HOST as a providers.file directory on" echo "subnet — see the README's routing section. Quick check after a box connects:"
echo " your Traefik, if it isn't already watching it." echo " docker exec <your-traefik> ping -c1 10.8.0.<box>"
echo " - Verify Traefik can actually reach wg-easy's WireGuard peer subnet" echo
echo " (the README's Prerequisites checklist has a command for this) —" echo "Connect a Furtka box: open its web UI -> Settings -> Gateway, and enter"
echo " this is the one thing that can't be checked from here." echo " Gateway URL: https://$GATEWAY_CONTROL_PLANE_HOST"
echo " - DNS: A/AAAA for $GATEWAY_CONTROL_PLANE_HOST -> wherever your" echo " Registration token: the GATEWAY_BOX_TOKEN value in $REPO_ROOT/.env"
echo " Traefik is reachable."
echo " - On the box: furtka gateway connect <this gateway's URL> <GATEWAY_BOX_TOKEN from .env> <box name>"

View file

@ -1,23 +1,18 @@
# Copy to traefik/dynamic/control-plane.yml (into whatever directory YOUR # Template for the control-plane's own Traefik route. ops/deploy.sh renders
# Traefik's file provider watches — see the root README's Prerequisites # it to rendered/control-plane.yml and installs it into the directory YOUR
# checklist) with two placeholders substituted: # Traefik's file provider watches, with two placeholders substituted:
# #
# GATEWAY_CONTROL_PLANE_HOST a real hostname you control (e.g. # GATEWAY_CONTROL_PLANE_HOST a real hostname you control (e.g.
# gw.example.com), with its own A/AAAA record # gw.example.com), with its own A/AAAA record
# pointing at this host. # pointing at this host.
# GATEWAY_CONTROL_PLANE_TARGET where YOUR Traefik actually reaches the # GATEWAY_CONTROL_PLANE_TARGET where YOUR Traefik actually reaches the
# control-plane container. If it joined your # control-plane container. If it joined your
# Traefik's docker network (ops/deploy.sh # Traefik's docker network (the recommended
# does this via `docker network connect` — # path — ops/deploy.sh sets that up), this is
# the primary, recommended path), this is # `furtka-gateway-control-plane:8090`, the
# `furtka-gateway-control-plane:8090` — the # container's fixed name (unambiguous on a
# container's fixed name, which Docker's # shared network, unlike a generic service
# embedded DNS resolves on any network it's # name like `control-plane`).
# joined to (unlike the compose *service*
# name `control-plane`, which is only
# guaranteed to resolve on networks compose
# itself manages — not one joined after the
# fact via a bare `docker network connect`).
# If instead you exposed it on a separate # If instead you exposed it on a separate
# host (GATEWAY_CONTROL_PLANE_BIND widened # host (GATEWAY_CONTROL_PLANE_BIND widened
# beyond 127.0.0.1 in docker-compose.yaml), # beyond 127.0.0.1 in docker-compose.yaml),