Ctrl K
ComunidadGuías…

deploy.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/bin/bash

set -e  # Exit immediately on error

# Color definitions
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

# Script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DOCKER_SCRIPTS_DIR="$SCRIPT_DIR/docker/scripts"

# Logging helpers
log_info() {
    echo -e "${BLUE}ℹ️  $1${NC}"
}

log_success() {
    echo -e "${GREEN}✅ $1${NC}"
}

log_warning() {
    echo -e "${YELLOW}⚠️  $1${NC}"
}

log_error() {
    echo -e "${RED}❌ $1${NC}"
}

# Show help
show_help() {
    cat << EOF
PrivyDrop Docker Deployment Script

Usage: $0 [options]

Options:
  --domain DOMAIN     Specify domain (for HTTPS deployments)
  --mode MODE         Deployment mode: lan-http|lan-tls|public|full
                      lan-http: Intranet HTTP (fast start; no TLS)
                      lan-tls:  Intranet HTTPS (self-signed; dev/managed env only)
                      public:   Public HTTP + TURN server
                      full:     Domain + HTTPS (Let’s Encrypt) + TURN server
  --with-nginx        Enable Nginx reverse proxy
  --with-turn         Enable TURN server
  --with-sni443       Force enable 443 SNI routing (full; default enabled)
  --no-sni443         Disable 443 SNI routing (full; web listens directly on 443)
  --enable-web-https  In lan-tls mode, enable self-signed HTTPS on 8443 (no HSTS)
  --le-email EMAIL    Email for Let's Encrypt (recommended in full mode)
  --test-renewal      Run 'certbot renew --dry-run' and reload services (verification)
  --clean             Clean existing containers and data
  --help              Show help

Examples:
  $0 --mode lan-http                     # LAN HTTP quick start
  $0 --mode lan-http --with-turn         # LAN HTTP with TURN (NAT-friendly)
  $0 --mode lan-tls --enable-web-https   # LAN HTTPS (self-signed) on 8443 (dev/managed)
  $0 --mode public --with-turn           # Public deployment + TURN server (no domain)
  $0 --mode full --domain example.com \\
       --with-nginx --with-turn --le-email you@domain.com   # Full HTTPS deployment (LE auto-issue/renew)
  $0 --clean                            # Clean deployment

Requirements:
  - Docker Engine and Docker Compose V2 (command `docker compose`)

EOF
}

# Parse command-line arguments
parse_arguments() {
    DOMAIN_NAME=""
    DEPLOYMENT_MODE=""
    WITH_NGINX=false
    WITH_TURN=false
    CLEAN_MODE=false
    LE_EMAIL=""
    WITH_SNI443=false
    DISABLE_SNI443=false
    ENABLE_WEB_HTTPS=false
    TEST_RENEWAL=false
    
    while [[ $# -gt 0 ]]; do
        case $1 in
            --domain)
                DOMAIN_NAME="$2"
                shift 2
                ;;
            --mode)
                DEPLOYMENT_MODE="$2"
                shift 2
                ;;
            --with-nginx)
                WITH_NGINX=true
                shift
                ;;
            --with-turn)
                WITH_TURN=true
                shift
                ;;
            --with-sni443)
                WITH_SNI443=true
                shift
                ;;
            --no-sni443)
                DISABLE_SNI443=true
                shift
                ;;
            --enable-web-https)
                ENABLE_WEB_HTTPS=true
                shift
                ;;
            --le-email)
                LE_EMAIL="$2"
                shift 2
                ;;
            --test-renewal)
                TEST_RENEWAL=true
                shift
                ;;
            --clean)
                CLEAN_MODE=true
                shift
                ;;
            --help)
                show_help
                exit 0
                ;;
            *)
                log_error "Unknown argument: $1"
                show_help
                exit 1
                ;;
        esac
    done
    
    # Export variables for other scripts
    export DOMAIN_NAME
    export DEPLOYMENT_MODE
    export WITH_NGINX
    export WITH_TURN
}

# Check dependencies
check_dependencies() {
    log_info "Checking dependencies..."
    
    local missing_deps=()
    
    if ! command -v docker &> /dev/null; then
        missing_deps+=("docker")
    fi
    
    if ! docker compose version &> /dev/null; then
        missing_deps+=("docker compose (V2)")
    fi
    
    if ! command -v curl &> /dev/null; then
        missing_deps+=("curl")
    fi
    
    if ! command -v openssl &> /dev/null; then
        missing_deps+=("openssl")
    fi
    
    if [[ ${#missing_deps[@]} -gt 0 ]]; then
        log_error "Missing dependencies: ${missing_deps[*]}"
        echo ""
        echo "Please install the missing dependencies:"
        for dep in "${missing_deps[@]}"; do
            case $dep in
                docker)
                    echo "  Docker: https://docs.docker.com/get-docker/"
                    ;;
                "docker compose (V2)")
                    echo "  Docker Compose V2 plugin: https://docs.docker.com/compose/install/"
                    ;;
                curl)
                    echo "  curl: sudo apt-get install curl (Ubuntu/Debian)"
                    ;;
                openssl)
                    echo "  openssl: sudo apt-get install openssl (Ubuntu/Debian)"
                    ;;
            esac
        done
        exit 1
    fi
    
    log_success "Dependency checks passed"
}

# Install and prepare Let's Encrypt (certbot)
ensure_certbot() {
    if command -v certbot >/dev/null 2>&1; then
        return 0
    fi
    log_info "Installing certbot (requires sudo)..."
    if command -v apt-get >/dev/null 2>&1; then
        sudo apt-get update -y && sudo apt-get install -y certbot
    else
        log_error "apt-get not found. Please install certbot manually or run on a supported system"
        exit 1
    fi
}

# Write certbot deploy hook: copy certs and hot-reload services after renewal
install_certbot_deploy_hook() {
    local repo_dir="$SCRIPT_DIR"
    local hook_dir="/etc/letsencrypt/renewal-hooks/deploy"
    local hook_file="$hook_dir/privydrop-reload.sh"
    local compose_file="$repo_dir/docker-compose.yml"

    sudo mkdir -p "$hook_dir"
    sudo bash -c "cat > '$hook_file'" << EOF
#!/bin/bash
set -e
REPO_DIR="$repo_dir"
COMPOSE_FILE="$compose_file"

# RENEWED_LINEAGE is provided by certbot and points to live/<domain>
if [[ -z "\$RENEWED_LINEAGE" ]]; then
  exit 0
fi

cp "\$RENEWED_LINEAGE/fullchain.pem" "\$REPO_DIR/docker/ssl/server-cert.pem"
cp "\$RENEWED_LINEAGE/privkey.pem" "\$REPO_DIR/docker/ssl/server-key.pem"
chmod 600 "\$REPO_DIR/docker/ssl/server-key.pem" || true

# Hot-reload nginx; restart if it fails
docker compose -f "\$COMPOSE_FILE" exec -T nginx nginx -s reload 2>/dev/null || \
docker compose -f "\$COMPOSE_FILE" restart nginx || true

# Prefer sending HUP to coturn; restart if needed (ignore if disabled)
docker compose -f "\$COMPOSE_FILE" exec -T coturn sh -c 'kill -HUP 1' 2>/dev/null || \
docker compose -f "\$COMPOSE_FILE" restart coturn || true
EOF
    sudo chmod +x "$hook_file"

    # Attempt to enable systemd timer
    if command -v systemctl >/dev/null 2>&1; then
        sudo systemctl enable --now certbot.timer 2>/dev/null || true
    fi
}

# Ensure renewal is scheduled daily: prefer systemd timer; fallback to cron
ensure_renewal_scheduler() {
    if command -v systemctl >/dev/null 2>&1; then
        sudo systemctl enable --now certbot.timer 2>/dev/null || true
        return 0
    fi
    # Fallback: cron job every 12 hours
    if [ -w /etc/cron.d ] || sudo test -d /etc/cron.d; then
        sudo bash -c 'cat > /etc/cron.d/certbot' << 'EOF'
# Auto-renew Let's Encrypt certificates for PrivyDrop
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
0 */12 * * * root certbot renew -q
EOF
    fi
}

# Dry-run renewal test
test_renewal() {
    log_info "Running certbot dry-run renewal test..."
    if ! command -v certbot >/dev/null 2>&1; then
        log_error "certbot not installed. Run full mode issuance first or install certbot manually"
        return 1
    fi
    sudo certbot renew --dry-run || {
        log_error "certbot dry-run failed"
        return 1
    }
    # Attempt hot-reload of nginx and coturn similar to deploy-hook
    docker compose exec -T nginx nginx -s reload 2>/dev/null || docker compose restart nginx || true
    docker compose exec -T coturn sh -c 'kill -HUP 1' 2>/dev/null || docker compose restart coturn || true
    log_success "Dry-run renewal completed; services reloaded"
}

# Issue via webroot and enable 443 config
provision_letsencrypt_cert() {
    # Only in full mode with nginx enabled and domain set
    if [[ "$DEPLOYMENT_MODE" != "full" || "$WITH_NGINX" != "true" ]]; then
        return 0
    fi
    if [[ -z "$DOMAIN_NAME" ]]; then
        log_warning "Full mode without --domain; skipping Let's Encrypt"
        return 0
    fi
    if [[ -z "$LE_EMAIL" ]]; then
        log_warning "No --le-email specified; using --register-unsafely-without-email"
    fi

    ensure_certbot
    install_certbot_deploy_hook
    ensure_renewal_scheduler

    mkdir -p docker/letsencrypt-www docker/ssl

    local cert_domains=("$DOMAIN_NAME")
    local lineage_name="$DOMAIN_NAME"
    local turn_domain=""
    if [[ "$DOMAIN_NAME" == www.* ]]; then
        local bare_domain="${DOMAIN_NAME#www.}"
        cert_domains+=("$bare_domain")
        lineage_name="$DOMAIN_NAME"
        turn_domain="turn.$bare_domain"
    else
        cert_domains+=("www.$DOMAIN_NAME")
        turn_domain="turn.$DOMAIN_NAME"
    fi
    cert_domains+=("$turn_domain")

    # If certificates already exist (including -0001 lineage), skip issuance
    if [[ -f "/etc/letsencrypt/live/$lineage_name/fullchain.pem" ]] || ls -1d /etc/letsencrypt/live/${lineage_name}* >/dev/null 2>&1; then
        log_info "Detected existing certificates/lineage; skipping initial issuance"
    else
        log_info "Issuing Let's Encrypt certificate via webroot..."
        local email_args="--email $LE_EMAIL"
        if [[ -z "$LE_EMAIL" ]]; then
            email_args="--register-unsafely-without-email"
        fi
        local domain_args=()
        local domain
        for domain in "${cert_domains[@]}"; do
            domain_args+=(-d "$domain")
        done
        # Requires port 80 reachable and nginx running
        sudo certbot certonly --webroot -w "$(pwd)/docker/letsencrypt-www" \
            "${domain_args[@]}" \
            $email_args --agree-tos --non-interactive || {
              log_error "Certificate issuance failed; please check certbot output"
              return 1
            }
    fi

    # Resolve lineage directory (supports -0001/-0002 suffixes) and copy to docker/ssl
    local lineage_dir
    lineage_dir=$(readlink -f "/etc/letsencrypt/live/$lineage_name" 2>/dev/null || true)
    if [[ -z "$lineage_dir" || ! -d "$lineage_dir" ]]; then
        lineage_dir=$(ls -1d /etc/letsencrypt/live/${lineage_name}* 2>/dev/null | sort | tail -1)
    fi
    if [[ -z "$lineage_dir" || ! -f "$lineage_dir/fullchain.pem" ]]; then
        log_error "No valid certificate lineage directory found. Check /etc/letsencrypt/live/${lineage_name}*"
        return 1
    fi

    sudo cp "$lineage_dir/fullchain.pem" docker/ssl/server-cert.pem
    sudo cp "$lineage_dir/privkey.pem" docker/ssl/server-key.pem
    sudo chmod 600 docker/ssl/server-key.pem || true

    # Enable 443 config (certs ready): append only; pass SNI flag (enabled by default in full)
    local gen_args=(--mode full --domain "$DOMAIN_NAME" --no-clean --ssl-mode letsencrypt)
    [[ "$WITH_SNI443" == "true" ]] && gen_args+=(--enable-sni443)
    [[ "$DISABLE_SNI443" == "true" ]] && gen_args+=(--no-sni443)
    bash "$DOCKER_SCRIPTS_DIR/generate-config.sh" "${gen_args[@]}" || true

    # Hot-reload nginx to enable 443
    docker compose exec -T nginx nginx -s reload || docker compose restart nginx
}

# Clean existing deployment
clean_deployment() {
    if [[ "$CLEAN_MODE" == "true" ]]; then
        log_warning "Cleaning existing deployment..."
        
        # Stop and remove containers
        if [[ -f "docker-compose.yml" ]]; then
            docker compose down -v --remove-orphans 2>/dev/null || true
        fi
        # After graceful stop, force-clean named containers as fallback
        docker stop -t 10 privydrop-nginx privydrop-coturn 2>/dev/null || true
        docker rm -f privydrop-nginx privydrop-coturn 2>/dev/null || true
        # Fallback: remove project network (if present)
        docker network rm privydrop_privydrop-network 2>/dev/null || true
        
        # Remove images
        docker images | grep privydrop | awk '{print $3}' | xargs -r docker rmi -f 2>/dev/null || true
        
        # Clean configuration files
        rm -rf docker/nginx/conf.d/*.conf docker/ssl/* logs/* .env 2>/dev/null || true
        
        log_success "Cleanup complete"
        
        if [[ $# -eq 1 ]]; then  # If only --clean parameter
            exit 0
        fi
    fi
}

# Ensure TURN service starts when requested (--with-turn)
ensure_turn_running() {
    if [[ "$WITH_TURN" != "true" ]]; then
        return 0
    fi
    # If not running, start coturn via profile
    if ! docker compose ps | grep -q "privydrop-coturn"; then
        log_info "Starting TURN service (profile: turn)..."
        docker compose --profile turn up -d coturn || true
    fi
}

# Environment detection and configuration generation
setup_environment() {
    log_info "Setting up environment..."
    
    # Ensure scripts are executable
    chmod +x "$DOCKER_SCRIPTS_DIR"/*.sh 2>/dev/null || true
    
    # Run environment detection
    local detect_args=""
    [[ -n "$DOMAIN_NAME" ]] && detect_args="--domain $DOMAIN_NAME"
    [[ -n "$DEPLOYMENT_MODE" ]] && detect_args="$detect_args --mode $DEPLOYMENT_MODE"
    [[ "$WITH_NGINX" == "true" ]] && detect_args="$detect_args --with-nginx"
    [[ "$WITH_SNI443" == "true" ]] && detect_args="$detect_args --enable-sni443"
    [[ "$DISABLE_SNI443" == "true" ]] && detect_args="$detect_args --no-sni443"
    [[ "$ENABLE_WEB_HTTPS" == "true" ]] && detect_args="$detect_args --enable-web-https"
    
    if ! bash "$DOCKER_SCRIPTS_DIR/detect-environment.sh" $detect_args; then
        log_error "Environment detection failed"
        exit 1
    fi
    
    # Generate configuration files
    if ! bash "$DOCKER_SCRIPTS_DIR/generate-config.sh" $detect_args; then
        log_error "Configuration generation failed"
        exit 1
    fi
    
    log_success "Environment setup complete"
}

# Build and start services
deploy_services() {
    log_info "Building and starting services..."

    # Ensure log directories exist and relax permissions so containers (coturn/nginx etc.) can write logs
    mkdir -p logs logs/nginx logs/backend logs/frontend logs/coturn 2>/dev/null || true
    chmod 777 -R logs 2>/dev/null || true
    log_info "Log directories prepared and permissions set: ./logs (mode 777)"

    # Stop existing services
    if docker compose ps | grep -q "Up"; then
        log_info "Stopping existing services..."
        docker compose down
    fi
    
    # Determine enabled services (Compose V2 requires --profile before the subcommand)
    local profiles=""
    if [[ "$WITH_NGINX" == "true" ]]; then
        profiles="$profiles --profile nginx"
    fi
    if [[ "$WITH_TURN" == "true" ]]; then
        profiles="$profiles --profile turn"
    fi
    
    # Build images (parallel first, fall back to serial on failure)
    log_info "Building Docker images..."
    set +e
    docker compose build --parallel
    local build_status=$?
    set -e
    if [[ $build_status -ne 0 ]]; then
        log_warning "Parallel build failed; falling back to serial build..."
        docker compose build
    fi
    
    # Start services (--profile must precede up)
    log_info "Starting services..."
    # shellcheck disable=SC2086
    docker compose $profiles up -d
    
    log_success "Services started"
}

# Wait for services to be ready
wait_for_services() {
    log_info "Waiting for services to be ready..."
    
    local max_attempts=60
    local attempt=0
    local services_ready=false
    
    while [[ $attempt -lt $max_attempts ]]; do
        local backend_ready=false
        local frontend_ready=false
        
        # Check backend health
        if curl -f http://localhost:3001/health &> /dev/null; then
            backend_ready=true
        fi
        
        # Check frontend health
        if curl -f http://localhost:3002/api/health &> /dev/null; then
            frontend_ready=true
        fi
        
        if [[ "$backend_ready" == "true" ]] && [[ "$frontend_ready" == "true" ]]; then
            services_ready=true
            break
        fi
        
        attempt=$((attempt + 1))
        echo -n "."
        sleep 2
    done
    
    echo ""
    
    if [[ "$services_ready" == "true" ]]; then
        log_success "All services are ready"
        return 0
    else
        log_error "Service startup timed out"
        log_info "View service status: docker compose ps"
        log_info "View service logs: docker compose logs -f"
        return 1
    fi
}

# Run post-deployment checks
post_deployment_checks() {
    log_info "Running post-deployment checks..."
    
    # Check container status
    log_info "Checking container status..."
    docker compose ps
    
    # In full+nginx, add HTTPS health check (if domain defined)
    if [[ -f ".env" ]]; then
        local dep_mode="$(grep "DEPLOYMENT_MODE=" .env | cut -d'=' -f2)"
        local dname="$(grep "DOMAIN_NAME=" .env | cut -d'=' -f2)"
        if [[ "$dep_mode" == "full" && -n "$dname" ]]; then
            log_info "Test: HTTPS health check https://$dname/api/health"
            if curl -fsS "https://$dname/api/health" >/dev/null; then
                log_success "HTTPS health check passed"
            else
                log_warning "HTTPS health check failed. If the certificate was just issued, wait a bit or run: bash docker/scripts/generate-config.sh --mode full --domain $dname --no-clean && docker compose exec -T nginx nginx -s reload"
            fi
        fi
    fi
    
    # Run health-check tests
    if [[ -f "test-health-apis.sh" ]]; then
        log_info "Running health-check tests..."
        if bash test-health-apis.sh; then
            log_success "Health-check tests passed"
        else
            log_warning "Health-check tests failed, but services may still be working"
        fi
    fi
    
    log_success "Post-deployment checks complete"
}

# Show deployment results
show_deployment_info() {
    echo ""
    echo -e "${GREEN}🎉 PrivyDrop deployment complete!${NC}"
    echo ""
    
    # Read configuration
    local local_ip=""
    local public_ip=""
    local frontend_port=""
    local backend_port=""
    local https_port=""
    local deployment_mode=""
    local network_mode=""
    local domain_name=""
    local turn_enabled_env=""
    
    if [[ -f ".env" ]]; then
        local_ip=$(grep "LOCAL_IP=" .env | cut -d'=' -f2)
        public_ip=$(grep "PUBLIC_IP=" .env | cut -d'=' -f2)
        frontend_port=$(grep "FRONTEND_PORT=" .env | cut -d'=' -f2)
        backend_port=$(grep "BACKEND_PORT=" .env | cut -d'=' -f2)
        https_port=$(grep "HTTPS_PORT=" .env | cut -d'=' -f2)
        deployment_mode=$(grep "DEPLOYMENT_MODE=" .env | cut -d'=' -f2)
        network_mode=$(grep "NETWORK_MODE=" .env | cut -d'=' -f2)
        domain_name=$(grep "DOMAIN_NAME=" .env | cut -d'=' -f2)
        turn_enabled_env=$(grep "TURN_ENABLED=" .env | cut -d'=' -f2)
    fi
    
    echo -e "${BLUE}📋 Access Info:${NC}"

    # Determine if public scenario (public/full)
    local is_public="false"
    if [[ "$deployment_mode" == "public" || "$deployment_mode" == "full" || "$network_mode" == "public" ]]; then
        is_public="true"
    fi

    if [[ "$is_public" == "true" ]]; then
        # For public scenarios, prefer domain, then public IP
        if [[ -n "$domain_name" ]]; then
            if [[ "$WITH_NGINX" == "true" || "$deployment_mode" == "full" ]]; then
                echo "   Public access: https://$domain_name"
                echo "   API: https://$domain_name"
            else
                echo "   Public access: http://$domain_name:${frontend_port:-3002}"
                echo "   API: http://$domain_name:${backend_port:-3001}"
            fi
        elif [[ -n "$public_ip" ]]; then
            if [[ "$WITH_NGINX" == "true" ]]; then
                echo "   Public access: http://$public_ip"
                echo "   API: http://$public_ip"
            else
                echo "   Public access: http://$public_ip:${frontend_port:-3002}"
                echo "   API: http://$public_ip:${backend_port:-3001}"
            fi
        else
            # Fallback: show LAN and localhost if public IP is unavailable
            echo "   Frontend: http://localhost:${frontend_port:-3002}"
            echo "   Backend API: http://localhost:${backend_port:-3001}"
        fi
    else
        # Private/basic: localhost + LAN
        if [[ "$WITH_NGINX" == "true" ]]; then
            # When Nginx is enabled and frontend uses same-origin API, prefer the gateway as the primary entry
            echo "   Frontend: http://localhost"
            echo "   API: http://localhost"
        else
            echo "   Frontend: http://localhost:${frontend_port:-3002}"
            echo "   Backend API: http://localhost:${backend_port:-3001}"
        fi
        if [[ -n "$local_ip" ]] && [[ "$local_ip" != "127.0.0.1" ]]; then
            echo ""
            echo -e "${BLUE}🌐 LAN Access:${NC}"
            if [[ "$WITH_NGINX" == "true" ]]; then
                echo "   Frontend: http://$local_ip"
                echo "   API: http://$local_ip"
            else
                echo "   Frontend: http://$local_ip:${frontend_port:-3002}"
                echo "   Backend API: http://$local_ip:${backend_port:-3001}"
            fi
        fi
    fi
    
    if [[ "$WITH_NGINX" == "true" ]]; then
        echo ""
        echo -e "${BLUE}🔀 Nginx Proxy:${NC}"
        if [[ -n "$domain_name" ]]; then
            echo "   HTTP: http://$domain_name"
            if [[ -f "docker/ssl/server-cert.pem" ]]; then
                echo "   HTTPS: https://$domain_name"
            fi
        elif [[ -n "$public_ip" ]]; then
            echo "   HTTP: http://$public_ip"
            if [[ -f "docker/ssl/server-cert.pem" ]]; then
                # In non-domain cases, show HTTPS with explicit port (e.g., lan-tls uses 8443)
                if [[ -n "$https_port" && "$https_port" != "443" ]]; then
                    echo "   HTTPS: https://$public_ip:$https_port"
                else
                    echo "   HTTPS: https://$public_ip"
                fi
            fi
        else
            echo "   HTTP: http://localhost"
            if [[ -f "docker/ssl/server-cert.pem" ]]; then
                # Show correct HTTPS endpoint based on configured port
                if [[ -n "$https_port" && "$https_port" != "443" ]]; then
                    echo "   HTTPS: https://localhost:$https_port"
                    if [[ -n "$local_ip" && "$local_ip" != "127.0.0.1" ]]; then
                        echo "   HTTPS (LAN): https://$local_ip:$https_port"
                    fi
                else
                    echo "   HTTPS: https://localhost"
                fi
            fi
        fi
    fi
    
    echo ""
    echo -e "${BLUE}🔧 Management Commands:${NC}"
    echo "   Status: docker compose ps"
    echo "   Logs: docker compose logs -f [service]"
    echo "   Restart: docker compose restart [service]"
    echo "   Stop: docker compose down"
    echo "   Full cleanup: $0 --clean"
    
    if [[ -f "docker/ssl/ca-cert.pem" ]]; then
        echo ""
        echo -e "${BLUE}🔒 SSL Certificates:${NC}"
        echo "   CA certificate: docker/ssl/ca-cert.pem"
        echo "   To trust HTTPS, import the CA certificate into your browser"
    fi
    
    if [[ "$WITH_TURN" == "true" || "$turn_enabled_env" == "true" ]]; then
        local turn_username=""
        local turn_realm=""
        if [[ -f ".env" ]]; then
            turn_username=$(grep "TURN_USERNAME=" .env | cut -d'=' -f2)
            turn_realm=$(grep "TURN_REALM=" .env | cut -d'=' -f2)
        fi
        
        echo ""
        echo -e "${BLUE}🔄 TURN Server:${NC}"
        # Prefer domain for TURN info; otherwise show public IP
        if [[ -n "$domain_name" ]]; then
            echo "   STUN: stun:${domain_name}:3478"
            echo "   TURN (UDP): turn:${domain_name}:3478"
            echo "   TURN (TLS): turns:turn.${domain_name}:443 (if 443 SNI split is configured)"
        elif [[ -n "$public_ip" ]]; then
            echo "   STUN: stun:${public_ip}:3478"
            echo "   TURN: turn:${public_ip}:3478"
        else
            echo "   STUN: stun:${local_ip}:3478"
            echo "   TURN: turn:${local_ip}:3478"
        fi
        echo "   Username: ${turn_username:-privydrop}"
        echo "   Password: (stored in .env)"
    fi
    
    echo ""
    echo -e "${YELLOW}💡 Tips:${NC}"
    echo "   - First run may take several minutes to download and build images"
    echo "   - If issues occur, check logs: docker compose logs -f"
    echo "   - More help: $0 --help"
    echo ""

    # Public scenario: for domain + HTTPS setup steps, see docs
    if [[ "$is_public" == "true" && -z "$domain_name" ]]; then
        echo -e "${BLUE}🌍 Domain + HTTPS guide:${NC} see docs/DEPLOYMENT_docker.md or docs/DEPLOYMENT_docker.zh-CN.md"
    fi
}

# Main function
main() {
    echo -e "${BLUE}=== PrivyDrop Docker One-Click Deployment ===${NC}"
    echo ""
    
    # Parse command-line arguments
    parse_arguments "$@"
    
    # Check dependencies
    check_dependencies
    echo ""

    # If only testing renewal, run and exit
    if [[ "$TEST_RENEWAL" == "true" ]]; then
        test_renewal && exit 0 || exit 1
    fi
    
    # Clean mode
    clean_deployment
    # If only cleaning (no other args), exit early to skip env detection
    if [[ "$CLEAN_MODE" == "true" && -z "$DEPLOYMENT_MODE" && "$WITH_NGINX" == "false" && "$WITH_TURN" == "false" && -z "$DOMAIN_NAME" ]]; then
        log_success "Cleanup complete (clean-only mode). Exiting."
        exit 0
    fi
    
    # Environment setup
    setup_environment
    echo ""
    
    # Deploy services
    deploy_services
    echo ""

    # If full + nginx, automatically issue certs and enable 443
    provision_letsencrypt_cert || true
    if [[ "$DEPLOYMENT_MODE" == "full" && "$WITH_NGINX" == "true" ]]; then
        log_info "Refreshing edge containers to apply generated HTTPS/SNI config..."
        docker compose up -d --force-recreate nginx >/dev/null
        if [[ "$WITH_TURN" == "true" ]]; then
            docker compose up -d --force-recreate coturn >/dev/null
        fi
    fi
    # Ensure TURN is running (when requested with --with-turn)
    ensure_turn_running || true
    
    # Wait for services to be ready
    if wait_for_services; then
        echo ""
        post_deployment_checks
        show_deployment_info
    else
        log_error "Deployment failed. Please check logs: docker compose logs"
        exit 1
    fi
}

# Trap interrupt signals
trap 'log_warning "Deployment interrupted"; exit 1' INT TERM

# Run main function
main "$@"