Files
PrivyDrop/deploy.sh
T
2026-03-26 00:14:28 +08:00

770 lines
26 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 (Lets 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
# If certificates already exist (including -0001 lineage), skip issuance
if [[ -f "/etc/letsencrypt/live/$DOMAIN_NAME/fullchain.pem" ]] || ls -1d /etc/letsencrypt/live/${DOMAIN_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
# Requires port 80 reachable and nginx running
sudo certbot certonly --webroot -w "$(pwd)/docker/letsencrypt-www" \
-d "$DOMAIN_NAME" -d "turn.$DOMAIN_NAME" \
$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/$DOMAIN_NAME" 2>/dev/null || true)
if [[ -z "$lineage_dir" || ! -d "$lineage_dir" ]]; then
lineage_dir=$(ls -1d /etc/letsencrypt/live/${DOMAIN_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/${DOMAIN_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 "$@"