#!/usr/bin/env bash set -euo pipefail DEPLOY_DIR="${DEPLOY_DIR:-/opt/phoboswg}" UI_PORT="${UI_PORT:-51831}" WG_HOST="${WG_HOST:-}" WG_EASY_IMAGE="${WG_EASY_IMAGE:-ghcr.io/ground-zerro/phobos:latest}" REPO_RAW_BASE="${REPO_RAW_BASE:-https://raw.githubusercontent.com/Ground-Zerro/Phobos/main}" COMPOSE_FILE="docker-compose.yml" COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-phoboswg}" export COMPOSE_PROJECT_NAME INIT_ENABLED="${INIT_ENABLED:-false}" INIT_USERNAME="${INIT_USERNAME:-admin}" INIT_PASSWORD="${INIT_PASSWORD:-}" log() { printf '\e[1;34m==>\e[0m %s\n' "$*"; } ok() { printf '\e[1;32m ✓\e[0m %s\n' "$*"; } fail() { printf '\e[1;31mERROR:\e[0m %s\n' "$*" >&2; exit 1; } trap 'printf "\e[1;31mERROR:\e[0m Deploy failed at line %s\n" "$LINENO" >&2' ERR require_root() { [ "$(id -u)" -eq 0 ] || fail "Run as root: sudo bash $0" } detect_distro() { if [ -f /etc/os-release ]; then . /etc/os-release echo "${ID:-unknown}" else echo "unknown" fi } random_password() { local prev_pipefail password prev_pipefail="$(set -o | awk '$1=="pipefail"{print $2}')" set +o pipefail password="$(tr -dc 'A-Za-z0-9!@#%^*_+=' < /dev/urandom | head -c 20 || true)" if [ "$prev_pipefail" = "on" ]; then set -o pipefail fi [ -n "$password" ] || password="Phobos$(date +%s)Aa1!" printf '%s' "$password" } install_docker() { log "Installing Docker" local distro distro="$(detect_distro)" case "$distro" in ubuntu|debian) export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get install -y -qq ca-certificates curl install -m 0755 -d /etc/apt/keyrings curl -fsSL "https://download.docker.com/linux/${distro}/gpg" -o /etc/apt/keyrings/docker.asc chmod a+r /etc/apt/keyrings/docker.asc echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/${distro} $(. /etc/os-release && echo "$VERSION_CODENAME") stable" > /etc/apt/sources.list.d/docker.list apt-get update -qq apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin ;; centos|rhel|fedora|rocky|almalinux) dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin ;; *) curl -fsSL https://get.docker.com | sh ;; esac systemctl enable --now docker ok "Docker installed: $(docker --version)" } ensure_docker() { if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then ok "Docker already present: $(docker --version)" return fi install_docker } fix_networkd_docker_bridge() { systemctl is-active --quiet systemd-networkd || return 0 local net_drop="/etc/systemd/network/05-docker-unmanaged.network" local new_content="[Match] Name=veth* docker0 br-* [Link] Unmanaged=yes" if [ "$(cat "$net_drop" 2>/dev/null)" = "$new_content" ]; then ok "systemd-networkd docker bridge fix already applied" return fi printf '%s\n' "$new_content" > "$net_drop" networkctl reload 2>/dev/null || systemctl restart systemd-networkd ok "systemd-networkd configured to ignore docker bridges" } load_wireguard_module() { if lsmod | grep -q wireguard 2>/dev/null; then ok "WireGuard kernel module already loaded" return fi if modprobe wireguard 2>/dev/null; then ok "WireGuard kernel module loaded" else log "Warning: could not load wireguard module — wireguard-go (userspace) will be used" fi } detect_public_ip() { local ip ip=$( curl -fsSL --connect-timeout 5 https://api.ipify.org 2>/dev/null || curl -fsSL --connect-timeout 5 https://ifconfig.me 2>/dev/null || curl -fsSL --connect-timeout 5 https://icanhazip.com 2>/dev/null || true ) echo "${ip:-127.0.0.1}" } download_stack_files() { log "Downloading deployment files" mkdir -p "$DEPLOY_DIR" curl -fsSL "${REPO_RAW_BASE}/docker-compose.yml" -o "${DEPLOY_DIR}/docker-compose.yml" ok "docker-compose.yml downloaded" } wait_healthy() { local ctr="${WAIT_HEALTHY_CONTAINER:-phobos}" local poll_s="${WAIT_HEALTHY_POLL_SEC:-2}" local timeout_s="${WAIT_HEALTHY_TIMEOUT_SEC:-360}" local start_ts now_ts elapsed i=0 status start_ts=$(date +%s) log "Waiting for container ${ctr} (poll ${poll_s}s, timeout ${timeout_s}s)" while :; do status=$(docker inspect "${ctr}" --format "{{if .State.Health}}{{.State.Health.Status}}{{else}}nohealth{{end}}" 2>/dev/null || echo "missing") now_ts=$(date +%s) elapsed=$((now_ts - start_ts)) case "$status" in healthy) ok "Container is healthy after ${elapsed}s" docker ps --filter "name=${ctr}" --format " {{.Names}} | {{.Status}} | {{.Ports}}" return 0 ;; unhealthy) fail "Container reported unhealthy after ${elapsed}s. Logs:\n$(docker logs --tail 120 "${ctr}" 2>/dev/null || true)" ;; missing) fail "Container is missing" ;; *) if [ $((i % 5)) -eq 0 ]; then printf ' status=%s elapsed=%ss/%ss\n' "${status}" "${elapsed}" "${timeout_s}" fi ;; esac if [ "${elapsed}" -ge "${timeout_s}" ]; then fail "Container did not become healthy in ${timeout_s}s (last status: ${status}). Logs:\n$(docker logs --tail 150 "${ctr}" 2>/dev/null || true)" fi i=$((i + 1)) sleep "${poll_s}" done } require_root ensure_docker fix_networkd_docker_bridge load_wireguard_module if [ -z "$WG_HOST" ]; then log "Detecting public IP" WG_HOST="$(detect_public_ip)" ok "Public IP: $WG_HOST" fi if [ "$INIT_ENABLED" = "true" ] && [ -z "$INIT_PASSWORD" ]; then INIT_PASSWORD="$(random_password)" fi download_stack_files cd "$DEPLOY_DIR" log "Writing .env" if [ "$INIT_ENABLED" = "true" ]; then cat > "${DEPLOY_DIR}/.env" < "${DEPLOY_DIR}/.env" <>> Open in browser to log in: <<<\e[0m\n' printf '\e[1;37m http://%s:%s/\e[0m\n' "$WG_HOST" "$UI_PORT" printf '\n' printf ' Username : %s\n' "$INIT_USERNAME" printf ' Password : %s\n' "$INIT_PASSWORD" printf '\n' printf '\e[0;33m Note: to configure domain and TLS certificate go to\e[0m\n' printf '\e[0;33m Admin → Interface after login.\e[0m\n' printf '\e[0;33m Credentials above apply on first deploy only (new database).\e[0m\n' else printf '\e[1;33m >>> Open in browser to complete initial setup: <<<\e[0m\n' printf '\e[1;37m http://%s:%s/\e[0m\n' "$WG_HOST" "$UI_PORT" printf '\n' printf ' The setup wizard will guide you through:\n' printf ' 1. Create admin account (username + password)\n' printf ' 2. Set server host (IP address or domain name)\n' printf ' 3. Configure TLS certificate (self-signed / import / skip)\n' fi printf '\n' printf ' Admin UI : %s:%s\n' "$WG_HOST" "$UI_PORT" printf ' Obfuscator : UDP %s:51822-51921 (preset range)\n' "$WG_HOST" printf ' Image : %s\n' "$WG_EASY_IMAGE" printf ' Deploy dir : %s\n' "$DEPLOY_DIR" printf '\e[1;32m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\e[0m\n' printf '\n' docker ps --format " {{.Names}} | {{.Status}} | {{.Ports}}" printf '\n'