#!/usr/bin/env bash set -Eeuo pipefail umask 077 hostname_value="${TPROXY_HOSTNAME:-}" public_ip="${MTPROXY_PUBLIC_IP:-}" npm_network="${NPM_NETWORK_NAME:-}" assume_yes=false usage() { cat <<'USAGE' Usage: ./install.sh [options] Options: --hostname DOMAIN Public proxy hostname --public-ip IPV4 Public IPv4 address of this Docker host --npm-network NAME Existing Docker network used by Nginx Proxy Manager --yes Accept detected defaults without confirmation -h, --help Show this help Without options the installer asks only for values it cannot detect safely. The WEB proxy secret and relay signing key are generated locally and are never sent to Gitea or written to .env. USAGE } while (($#)); do case "$1" in --hostname) hostname_value="${2:-}" shift 2 ;; --public-ip) public_ip="${2:-}" shift 2 ;; --npm-network) npm_network="${2:-}" shift 2 ;; --yes) assume_yes=true shift ;; -h | --help) usage exit 0 ;; *) echo "Unknown option: $1" >&2 usage >&2 exit 2 ;; esac done if [[ "$(uname -s)" != Linux ]] || [[ "$(uname -m)" != x86_64 ]]; then echo "This installer requires an x86_64 Linux Docker host." >&2 exit 1 fi if ! command -v docker >/dev/null 2>&1; then echo "Docker Engine with the Compose plugin is required." >&2 exit 1 fi docker_command=(docker) if ! docker info >/dev/null 2>&1; then if command -v sudo >/dev/null 2>&1 && sudo -n docker info >/dev/null 2>&1; then docker_command=(sudo docker) else echo "The current user cannot access Docker. Run as root or configure Docker access." >&2 exit 1 fi fi if ! "${docker_command[@]}" compose version >/dev/null 2>&1; then echo "The Docker Compose plugin is required." >&2 exit 1 fi valid_hostname() { local value=$1 label [[ ${#value} -le 253 ]] || return 1 [[ "$value" == *.* ]] || return 1 [[ "$value" =~ ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ ]] || return 1 local IFS=. read -r -a labels <<<"$value" for label in "${labels[@]}"; do [[ -n "$label" && ${#label} -le 63 ]] || return 1 [[ "$label" != -* && "$label" != *- ]] || return 1 done } valid_ipv4() { local ip=$1 octet local -a octets local IFS=. read -r -a octets <<<"$ip" [[ ${#octets[@]} -eq 4 ]] || return 1 for octet in "${octets[@]}"; do [[ "$octet" =~ ^[0-9]{1,3}$ ]] || return 1 ((10#$octet <= 255)) || return 1 done } public_ipv4() { local ip=$1 local -a octets local IFS=. valid_ipv4 "$ip" || return 1 read -r -a octets <<<"$ip" ((10#${octets[0]} != 0)) || return 1 ((10#${octets[0]} != 10)) || return 1 ((10#${octets[0]} != 127)) || return 1 ! ((10#${octets[0]} == 100 && 10#${octets[1]} >= 64 && 10#${octets[1]} <= 127)) || return 1 ! ((10#${octets[0]} == 169 && 10#${octets[1]} == 254)) || return 1 ! ((10#${octets[0]} == 172 && 10#${octets[1]} >= 16 && 10#${octets[1]} <= 31)) || return 1 ! ((10#${octets[0]} == 192 && 10#${octets[1]} == 0 && (10#${octets[2]} == 0 || 10#${octets[2]} == 2))) || return 1 ! ((10#${octets[0]} == 192 && 10#${octets[1]} == 88 && 10#${octets[2]} == 99)) || return 1 ! ((10#${octets[0]} == 192 && 10#${octets[1]} == 168)) || return 1 ! ((10#${octets[0]} == 198 && (10#${octets[1]} == 18 || 10#${octets[1]} == 19))) || return 1 ! ((10#${octets[0]} == 198 && 10#${octets[1]} == 51 && 10#${octets[2]} == 100)) || return 1 ! ((10#${octets[0]} == 203 && 10#${octets[1]} == 0 && 10#${octets[2]} == 113)) || return 1 ((10#${octets[0]} < 224)) || return 1 } if [[ -z "$hostname_value" ]]; then if [[ ! -t 0 ]]; then echo "--hostname is required in non-interactive mode." >&2 exit 2 fi read -r -p "Public hostname (for example, proxy.example.com): " hostname_value fi hostname_value="$(printf '%s' "$hostname_value" | tr '[:upper:]' '[:lower:]')" if ! valid_hostname "$hostname_value"; then echo "Invalid hostname: $hostname_value" >&2 exit 2 fi if [[ -z "$public_ip" ]]; then if [[ ! -t 0 ]]; then echo "--public-ip is required in non-interactive mode." >&2 exit 2 fi read -r -p "Public IPv4 of this Docker host: " public_ip fi if ! public_ipv4 "$public_ip"; then echo "Not a usable public IPv4 address: $public_ip" >&2 exit 2 fi if [[ -z "$npm_network" ]]; then npm_container="$("${docker_command[@]}" ps --format '{{.Names}}' | awk 'tolower($0) ~ /(nginx.*proxy|npm)/ { print; exit }')" if [[ -n "$npm_container" ]]; then npm_network="$("${docker_command[@]}" inspect \ --format '{{range $name, $_ := .NetworkSettings.Networks}}{{println $name}}{{end}}' \ "$npm_container" | awk '$0 != "bridge" && $0 != "host" && $0 != "none" { print; exit }')" fi fi if [[ -z "$npm_network" ]]; then for candidate in nginx-proxy-manager_default npm_default; do if "${docker_command[@]}" network inspect "$candidate" >/dev/null 2>&1; then npm_network=$candidate break fi done fi if [[ -z "$npm_network" ]]; then if [[ ! -t 0 ]]; then echo "--npm-network is required when Nginx Proxy Manager cannot be detected." >&2 exit 2 fi read -r -p "Existing Nginx Proxy Manager Docker network: " npm_network fi if ! "${docker_command[@]}" network inspect "$npm_network" >/dev/null 2>&1; then echo "Docker network does not exist: $npm_network" >&2 exit 2 fi repository="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" environment_file="$repository/.env" temporary_environment="$(mktemp "$repository/.env.new.XXXXXX")" trap 'rm -f "$temporary_environment"' EXIT cat >"$temporary_environment" </dev/null || true)" if [[ "$health" == healthy ]]; then break fi if [[ "$health" == unhealthy || "$health" == exited || "$health" == dead ]]; then break fi sleep 2 done if [[ "$health" != healthy ]]; then echo "The proxy did not become healthy (state: ${health:-unknown})." >&2 "${docker_command[@]}" logs --tail 100 tg-web-proxy >&2 || true exit 1 fi client_secret="$("${docker_command[@]}" exec tg-web-proxy \ sh -c 'tr -d "\\r\\n" < /data/client-secret')" if [[ ! "$client_secret" =~ ^[0-9a-f]{32}$ ]]; then echo "The proxy is healthy, but its client secret could not be read." >&2 exit 1 fi resolved_ip="$(getent ahostsv4 "$hostname_value" 2>/dev/null | awk 'NR == 1 { print $1 }')" cat <&2 fi