#!/usr/bin/env bash # Sets up furtka-gateway on an empty server: wg-easy + Traefik + # control-plane, all from this folder's docker-compose.yaml. See README.md # here for the server and DNS requirements. # # Safe to re-run — existing .env values become the defaults for each # prompt (press Enter to keep them), and secrets are generated once. # # Usage: # ./deploy.sh prompts, writes .env + dynamic/, starts # the stack, health-checks it # ./deploy.sh --non-interactive same, but no prompts: every value comes # from .env (fails if one is missing) # ./deploy.sh --render-only only (re)write .env and # dynamic/control-plane.yml; starts nothing set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" RENDER_ONLY=0 INTERACTIVE=1 for arg in "$@"; do case "$arg" in --render-only) RENDER_ONLY=1 ;; --non-interactive) INTERACTIVE=0 ;; *) echo "unknown option: $arg" >&2; exit 2 ;; esac done ENV_FILE=.env # Deliberately not `source`d: bash would expand `$` in values. load_env_file() { local file="$1" line key value [[ -f "$file" ]] || return 0 while IFS= read -r line || [[ -n "$line" ]]; do [[ -z "$line" || "$line" == \#* ]] && continue key="${line%%=*}" value="${line#*=}" printf -v "$key" '%s' "$value" done < "$file" } load_env_file "$ENV_FILE" ask() { # ask VARNAME "Prompt text" "fallback-default" local __var="$1" __prompt="$2" __fallback="${3:-}" local __current="${!__var:-}" local __default="${__current:-$__fallback}" local __ans="" if [[ "$INTERACTIVE" -eq 1 ]]; then if [[ -n "$__default" ]]; then read -r -p "$__prompt [$__default]: " __ans else read -r -p "$__prompt: " __ans fi fi printf -v "$__var" '%s' "${__ans:-$__default}" if [[ -z "${!__var}" ]]; then echo "$__var is required" >&2 exit 1 fi } require_cmd() { command -v "$1" >/dev/null 2>&1 || { echo "missing required command: $1" >&2; exit 1; } } require_cmd openssl if [[ "$RENDER_ONLY" -eq 0 ]]; then require_cmd docker require_cmd curl docker compose version >/dev/null 2>&1 || { echo "missing: docker compose plugin" >&2; exit 1; } fi echo "== furtka-gateway: new server setup ==" echo ask GATEWAY_DOMAIN "Your domain (boxes publish apps as .)" "" ask GATEWAY_PUBLIC_HOST "WireGuard endpoint hostname" "vpn.$GATEWAY_DOMAIN" ask GATEWAY_CONTROL_PLANE_HOST "Gateway API hostname (what you enter on the box)" "gw.$GATEWAY_DOMAIN" ask GATEWAY_ACME_EMAIL "Email for Let's Encrypt certificate notices" "" # Secrets: generated once, kept on re-run. WG_EASY_ADMIN_PASSWORD="${WG_EASY_ADMIN_PASSWORD:-$(openssl rand -hex 16)}" GATEWAY_BOX_TOKEN="${GATEWAY_BOX_TOKEN:-$(openssl rand -hex 32)}" GATEWAY_ADMIN_TOKEN="${GATEWAY_ADMIN_TOKEN:-$(openssl rand -hex 32)}" echo echo "Writing $ENV_FILE" { echo "# Generated by deploy.sh — re-run it to change any of these." echo "GATEWAY_DOMAIN=$GATEWAY_DOMAIN" echo "GATEWAY_PUBLIC_HOST=$GATEWAY_PUBLIC_HOST" echo "GATEWAY_CONTROL_PLANE_HOST=$GATEWAY_CONTROL_PLANE_HOST" echo "GATEWAY_ACME_EMAIL=$GATEWAY_ACME_EMAIL" echo "WG_EASY_ADMIN_PASSWORD=$WG_EASY_ADMIN_PASSWORD" echo "GATEWAY_BOX_TOKEN=$GATEWAY_BOX_TOKEN" echo "GATEWAY_ADMIN_TOKEN=$GATEWAY_ADMIN_TOKEN" [[ -n "${GATEWAY_CONTROL_PLANE_IMAGE:-}" ]] && echo "GATEWAY_CONTROL_PLANE_IMAGE=$GATEWAY_CONTROL_PLANE_IMAGE" } > "$ENV_FILE" chmod 600 "$ENV_FILE" echo "Writing dynamic/control-plane.yml" mkdir -p dynamic cat > dynamic/control-plane.yml </dev/null | awk 'NR==1{print $1}')" if [[ -z "$resolved" ]]; then echo " WARNING: $host does not resolve yet" elif [[ -n "$SERVER_IP" && "$resolved" != "$SERVER_IP" ]]; then echo " WARNING: $host -> $resolved, but this server is $SERVER_IP" else echo " ok: $host -> $resolved" fi done echo echo "== docker compose up -d --build ==" docker compose up -d --build echo echo "== health check ==" healthy=0 for _ in $(seq 1 60); do if curl -sf http://127.0.0.1:8090/healthz >/dev/null 2>&1; then healthy=1 break fi sleep 2 done if [[ "$healthy" -ne 1 ]]; then echo "control-plane did not become healthy — check: docker compose logs control-plane wg-easy" >&2 exit 1 fi echo " ok: control-plane is up" 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 (valid certificate)" else echo " WARNING: https://$GATEWAY_CONTROL_PLANE_HOST/healthz not reachable yet." echo " Usually DNS or the certificate is still pending — check: docker compose logs traefik" fi echo echo "== done ==" echo "Connect a Furtka box: open its web UI -> Settings -> Gateway, and enter" echo " Gateway URL: https://$GATEWAY_CONTROL_PLANE_HOST" echo " Registration token: the GATEWAY_BOX_TOKEN value in $SCRIPT_DIR/.env" echo echo "wg-easy admin UI (optional): ssh -L 51821:127.0.0.1:51821 ," echo "then open http://localhost:51821 — user 'admin', password WG_EASY_ADMIN_PASSWORD in .env."