#!/bin/bash cat << "EOF" ██████╗██╗ ██╗ █████╗ ███╗ ██╗ ██████╗ ███████╗ ██╔════╝██║ ██║██╔══██╗████╗ ██║██╔════╝ ██╔════╝ ██║ ███████║███████║██╔██╗ ██║██║ ███╗█████╗ ██║ ██╔══██║██╔══██║██║╚██╗██║██║ ██║██╔══╝ ╚██████╗██║ ██║██║ ██║██║ ╚████║╚██████╔╝███████╗ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚══════╝ ███╗ ███╗ █████╗ ██╗ ██╗███████╗██████╗ ████╗ ████║██╔══██╗██║ ██╔╝██╔════╝██╔══██╗ ██╔████╔██║███████║█████╔╝ █████╗ ██████╔╝ ██║╚██╔╝██║██╔══██║██╔═██╗ ██╔══╝ ██╔══██╗ ██║ ╚═╝ ██║██║ ██║██║ ██╗███████╗██║ ██║ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ Start Production Wizard EOF echo "#############################################################" echo "# " echo "# Changemaker.lite Production Deployment " echo "# " echo "# This script will: " echo "# 1. Install and configure cloudflared " echo "# 2. Create a systemd service for the tunnel " echo "# 3. Configure DNS records " echo "# 4. Set up access policies " echo "# " echo "#############################################################" echo "" # Get script directory SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" ENV_FILE="$SCRIPT_DIR/.env" TUNNEL_CONFIG_FILE="$SCRIPT_DIR/configs/cloudflare/tunnel-config.yml" TUNNEL_CREDS_DIR="$SCRIPT_DIR/configs/cloudflare" SERVICE_NAME="cloudflared-changemaker" # Source environment variables if [ -f "$ENV_FILE" ]; then export $(grep -v '^#' "$ENV_FILE" | xargs) else echo "Error: .env file not found. Please run ./config.sh first." exit 1 fi # Prompt user for tunnel name suffix echo "Choose a name for your tunnel:" echo "This will create a tunnel named 'changemaker-lite-[your-input]'" echo "Examples: 'prod', 'staging', 'myproject', 'server1'" echo "" read -p "Enter tunnel name suffix (or press Enter for default 'main'): " TUNNEL_SUFFIX # Set default if empty if [ -z "$TUNNEL_SUFFIX" ]; then TUNNEL_SUFFIX="main" fi # Validate tunnel suffix (alphanumeric and hyphens only) if [[ ! "$TUNNEL_SUFFIX" =~ ^[a-zA-Z0-9-]+$ ]]; then echo "Error: Tunnel suffix can only contain letters, numbers, and hyphens." exit 1 fi TUNNEL_NAME="changemaker-lite-$TUNNEL_SUFFIX" SERVICE_NAME="cloudflared-changemaker-lite-$TUNNEL_SUFFIX" echo "Using tunnel name: $TUNNEL_NAME" echo "Using service name: $SERVICE_NAME" echo "" # Check if Cloudflare credentials are properly configured if [ -z "$CF_API_TOKEN" ] || [ "$CF_API_TOKEN" == "your_cloudflare_api_token" ] || \ [ -z "$CF_ZONE_ID" ] || [ "$CF_ZONE_ID" == "your_cloudflare_zone_id" ] || \ [ -z "$CF_DOMAIN" ] || [ -z "$CF_ACCOUNT_ID" ] || [ "$CF_ACCOUNT_ID" == "your_cloudflare_account_id" ]; then echo "Error: Cloudflare configuration incomplete." echo "" echo "Current values:" echo " CF_API_TOKEN: ${CF_API_TOKEN:-not set}" echo " CF_ZONE_ID: ${CF_ZONE_ID:-not set}" echo " CF_DOMAIN: ${CF_DOMAIN:-not set}" echo " CF_ACCOUNT_ID: ${CF_ACCOUNT_ID:-not set}" echo "" echo "Please run ./config.sh and complete the Cloudflare configuration." exit 1 fi # Check if cloudflared is installed check_cloudflared() { if ! command -v cloudflared &> /dev/null; then echo "cloudflared not found. Installing..." install_cloudflared else echo "✅ cloudflared is already installed: $(cloudflared --version | head -n1)" fi } # Install cloudflared install_cloudflared() { echo "Installing cloudflared..." # Check if we need to add the Cloudflare repository if [ ! -f /usr/share/keyrings/cloudflare-main.gpg ]; then # Add the cloudflare gpg key curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo gpg --dearmor -o /usr/share/keyrings/cloudflare-main.gpg # Add the repository echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/cloudflared.list fi # Update and install sudo apt-get update && sudo apt-get install -y cloudflared if ! command -v cloudflared &> /dev/null; then echo "Failed to install cloudflared. Please install it manually following the instructions at:" echo "https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/" exit 1 else echo "✅ cloudflared installed successfully: $(cloudflared --version | head -n1)" fi } # Authenticate with Cloudflare authenticate_cloudflare() { echo "Checking Cloudflare authentication..." # Remove --account-tag parameter as it's not supported in this version if ! cloudflared tunnel list &> /dev/null; then echo "Not authenticated with Cloudflare. Starting authentication process..." echo "Log in to Cloudflare in the browser window that opens..." # Run login if ! cloudflared tunnel login; then echo "Authentication failed. Please try again or authenticate manually using:" echo "cloudflared tunnel login" exit 1 fi else echo "✅ Already authenticated with Cloudflare" fi } # Create or get existing tunnel setup_tunnel() { echo "Setting up Cloudflare tunnel..." # Check if we have a tunnel ID in the environment local tunnel_id="" # Check if tunnel exists by name echo "Checking for existing tunnel named '$TUNNEL_NAME'..." local tunnel_list_output # Remove --account-tag parameter as it's not supported in this version tunnel_list_output=$(cloudflared tunnel list --output json 2>&1) # Check if the command was successful if [ $? -ne 0 ]; then echo "Error listing tunnels: $tunnel_list_output" echo "Please ensure you're properly authenticated." exit 1 fi # Fix for invalid JSON output - try to clean the output if needed if ! echo "$tunnel_list_output" | jq '.' &>/dev/null; then echo "Warning: Invalid JSON returned from cloudflared. Attempting to clean output..." # Try to extract valid JSON portion tunnel_list_output=$(echo "$tunnel_list_output" | grep -o '\[.*\]' || echo "[]") fi # Now try to extract the tunnel info local tunnel_info if echo "$tunnel_list_output" | jq '.' &>/dev/null; then tunnel_info=$(echo "$tunnel_list_output" | jq -r '.[] | select(.name=="'$TUNNEL_NAME'")' 2>/dev/null || echo "") else echo "Failed to parse tunnel list. Cannot check for existing tunnels." tunnel_info="" fi if [ -n "$tunnel_info" ]; then # Tunnel exists tunnel_id=$(echo "$tunnel_info" | jq -r '.id' 2>/dev/null) if [ -n "$tunnel_id" ] && [ "$tunnel_id" != "null" ]; then echo "✅ Found existing tunnel: $TUNNEL_NAME (ID: $tunnel_id)" # Update the environment variable if needed if [ "$CF_TUNNEL_ID" != "$tunnel_id" ]; then echo "Updating CF_TUNNEL_ID in .env file..." sed -i "s/CF_TUNNEL_ID=.*/CF_TUNNEL_ID=$tunnel_id/" "$ENV_FILE" # Reload the variable export CF_TUNNEL_ID="$tunnel_id" fi else echo "Warning: Found tunnel but couldn't extract ID" fi else # Create new tunnel echo "Creating new tunnel: $TUNNEL_NAME" local tunnel_create_output # Remove --account-tag parameter as it's not supported in this version tunnel_create_output=$(cloudflared tunnel create "$TUNNEL_NAME" 2>&1) echo "Tunnel creation output:" echo "$tunnel_create_output" if [ $? -ne 0 ]; then echo "Failed to create tunnel. Please check your Cloudflare credentials." exit 1 fi # Try multiple methods to extract the tunnel ID from output # Method 1: Try the original regex pattern with new tunnel name tunnel_id=$(echo "$tunnel_create_output" | grep -oP "Created tunnel $TUNNEL_NAME with id \K[a-f0-9-]+" || echo "") # Method 2: Look for UUID pattern if [ -z "$tunnel_id" ]; then tunnel_id=$(echo "$tunnel_create_output" | grep -oP '[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}' || echo "") fi # Method 3: Look for hex string that might be the ID if [ -z "$tunnel_id" ]; then tunnel_id=$(echo "$tunnel_create_output" | grep -oP '\b[a-f0-9-]{20,}\b' || echo "") fi # Interactive method as fallback if [ -z "$tunnel_id" ]; then echo "Could not automatically extract tunnel ID from output." echo "Please check the output above and enter the tunnel ID manually:" read -p "Tunnel ID: " tunnel_id if [ -z "$tunnel_id" ]; then echo "No tunnel ID provided. Please create the tunnel manually and set CF_TUNNEL_ID in the .env file." exit 1 fi fi echo "✅ Created new tunnel: $TUNNEL_NAME (ID: $tunnel_id)" # Update the environment variable sed -i "s/CF_TUNNEL_ID=.*/CF_TUNNEL_ID=$tunnel_id/" "$ENV_FILE" export CF_TUNNEL_ID="$tunnel_id" fi # Make sure credentials directory exists mkdir -p "$TUNNEL_CREDS_DIR" # Check if we need to copy the credentials file local creds_file="$HOME/.cloudflared/$tunnel_id.json" local target_creds_file="$TUNNEL_CREDS_DIR/$tunnel_id.json" if [ -f "$creds_file" ] && [ ! -f "$target_creds_file" ]; then echo "Copying tunnel credentials to project directory..." cp "$creds_file" "$target_creds_file" echo "✅ Credentials copied to $target_creds_file" elif [ ! -f "$creds_file" ]; then echo "Warning: Tunnel credentials not found at $creds_file" echo "Checking for credentials in alternative locations..." # Try to find credentials in ~/.cloudflared directory local found_creds=false for cred_file in "$HOME/.cloudflared/"*.json; do if [ -f "$cred_file" ]; then echo "Found credentials file: $cred_file" echo "Checking if this file contains credentials for our tunnel..." # Check if the file contains the tunnel ID if grep -q "$tunnel_id" "$cred_file"; then echo "✅ This appears to be the correct credentials file!" cp "$cred_file" "$target_creds_file" echo "✅ Credentials copied to $target_creds_file" found_creds=true break fi fi done if [ "$found_creds" = false ]; then echo "Error: Could not find tunnel credentials." echo "Please locate the credentials file for tunnel $tunnel_id and copy it to:" echo "$target_creds_file" echo "" echo "You can also run this command manually to create a new tunnel with credentials:" echo "cloudflared tunnel create --account-tag \"$CF_ACCOUNT_ID\" \"$tunnel_name\"" exit 1 fi fi # Update tunnel configuration file update_tunnel_config "$target_creds_file" return 0 } # Update tunnel configuration update_tunnel_config() { local creds_file="$1" echo "Updating tunnel configuration..." # Create or update the config file cat > "$TUNNEL_CONFIG_FILE" << EOL # Cloudflare Tunnel Configuration for $CF_DOMAIN # Generated by Changemaker.lite start-production.sh on $(date) tunnel: ${CF_TUNNEL_ID} credentials-file: ${creds_file} ingress: - hostname: homepage.${CF_DOMAIN} service: http://localhost:${HOMEPAGE_PORT:-3010} - hostname: code.${CF_DOMAIN} service: http://localhost:${CODE_SERVER_PORT:-8888} - hostname: listmonk.${CF_DOMAIN} service: http://localhost:${LISTMONK_PORT:-9000} - hostname: docs.${CF_DOMAIN} service: http://localhost:${MKDOCS_PORT:-4000} - hostname: ${CF_DOMAIN} service: http://localhost:${MKDOCS_SITE_SERVER_PORT:-4001} - hostname: n8n.${CF_DOMAIN} service: http://localhost:${N8N_PORT:-5678} - hostname: db.${CF_DOMAIN} service: http://localhost:${NOCODB_PORT:-8090} - hostname: git.${CF_DOMAIN} service: http://localhost:${GITEA_WEB_PORT:-3030} - hostname: map.${CF_DOMAIN} service: http://localhost:${MAP_PORT:-3000} - hostname: qr.${CF_DOMAIN} service: http://localhost:${MINI_QR_PORT:-8089} - service: http_status:404 EOL echo "✅ Tunnel configuration updated at $TUNNEL_CONFIG_FILE" } # Create systemd service for cloudflared create_systemd_service() { echo "Creating systemd service for cloudflared tunnel: $SERVICE_NAME" local cloudflared_path=$(which cloudflared) local username=$(whoami) # Create service file content local service_content="[Unit] Description=Cloudflare Tunnel for Changemaker.lite After=network.target [Service] User=$username ExecStart=$cloudflared_path tunnel --config $TUNNEL_CONFIG_FILE run Restart=always RestartSec=5 StartLimitInterval=0 [Install] WantedBy=multi-user.target" # Write service file echo "$service_content" | sudo tee /etc/systemd/system/$SERVICE_NAME.service > /dev/null # Reload systemd sudo systemctl daemon-reload # Enable and start the service sudo systemctl enable $SERVICE_NAME.service sudo systemctl restart $SERVICE_NAME.service # Check service status sleep 3 if sudo systemctl is-active --quiet $SERVICE_NAME.service; then echo "✅ Cloudflared service is running" else echo "⚠️ Cloudflared service failed to start. Checking logs..." sudo systemctl status $SERVICE_NAME.service echo "Viewing recent logs:" sudo journalctl -u $SERVICE_NAME.service --no-pager -n 20 echo "Please check the configuration and try again." exit 1 fi } # Function to create/update DNS record create_dns_record() { local name=$1 local full_name local content="$CF_TUNNEL_ID.cfargotunnel.com" # Determine the full DNS name if [ "$name" == "@" ]; then full_name="$CF_DOMAIN" else full_name="$name.$CF_DOMAIN" fi echo "Configuring DNS record for $full_name..." # Check if record exists EXISTING=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records?name=$full_name" \ -H "Authorization: Bearer $CF_API_TOKEN" \ -H "Content-Type: application/json") RECORD_ID=$(echo "$EXISTING" | jq -r '.result[] | select(.name == "'$full_name'") | .id' | head -1) if [ ! -z "$RECORD_ID" ] && [ "$RECORD_ID" != "null" ]; then # Update existing record RESPONSE=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records/$RECORD_ID" \ -H "Authorization: Bearer $CF_API_TOKEN" \ -H "Content-Type: application/json" \ --data "{ \"type\": \"CNAME\", \"name\": \"$name\", \"content\": \"$content\", \"ttl\": 1, \"proxied\": true }") else # Create new record RESPONSE=$(curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records" \ -H "Authorization: Bearer $CF_API_TOKEN" \ -H "Content-Type: application/json" \ --data "{ \"type\": \"CNAME\", \"name\": \"$name\", \"content\": \"$content\", \"ttl\": 1, \"proxied\": true }") fi if echo "$RESPONSE" | jq -e '.success' > /dev/null; then echo " ✅ DNS record configured" else echo " ❌ Failed to configure DNS record" echo "$RESPONSE" | jq '.errors' fi } # Set up access policies setup_access_policies() { echo "" echo "Setting up Cloudflare Access policies..." read -p "Enter email address for admin access to protected services: " ADMIN_EMAIL if [[ "$ADMIN_EMAIL" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then # Protected services PROTECTED_SERVICES=("homepage" "code") for service in "${PROTECTED_SERVICES[@]}"; do echo "Creating access policy for $service.$CF_DOMAIN..." # Check if application already exists EXISTING_APPS=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/access/apps" \ -H "Authorization: Bearer $CF_API_TOKEN" \ -H "Content-Type: application/json") APP_ID=$(echo "$EXISTING_APPS" | jq -r ".result[] | select(.domain == \"$service.$CF_DOMAIN\") | .id" | head -1) if [ -z "$APP_ID" ] || [ "$APP_ID" == "null" ]; then # Create new access application APP_RESPONSE=$(curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/access/apps" \ -H "Authorization: Bearer $CF_API_TOKEN" \ -H "Content-Type: application/json" \ --data "{ \"name\": \"$service.$CF_DOMAIN\", \"domain\": \"$service.$CF_DOMAIN\", \"type\": \"self_hosted\", \"session_duration\": \"24h\", \"auto_redirect_to_identity\": false }") APP_ID=$(echo "$APP_RESPONSE" | jq -r '.result.id') fi if [ ! -z "$APP_ID" ] && [ "$APP_ID" != "null" ]; then # Create/update policy POLICY_RESPONSE=$(curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/access/apps/$APP_ID/policies" \ -H "Authorization: Bearer $CF_API_TOKEN" \ -H "Content-Type: application/json" \ --data "{ \"name\": \"Email Policy\", \"decision\": \"allow\", \"include\": [{ \"email\": {\"email\": \"$ADMIN_EMAIL\"} }] }") if echo "$POLICY_RESPONSE" | jq -e '.success' > /dev/null; then echo " ✅ Access policy configured" else echo " ⚠️ Policy may already exist or update failed" fi else echo " ❌ Failed to create access application" fi done else echo "Invalid email format. Skipping access policy setup." echo "You can configure access policies manually in the Cloudflare dashboard." fi } # Check if services are running check_services() { echo "Checking if services are running..." local all_running=true # Check common ports to see if services are running if ! nc -z localhost ${HOMEPAGE_PORT:-3010} >/dev/null 2>&1; then echo "❌ Homepage service not detected on port ${HOMEPAGE_PORT:-3010}" all_running=false fi if ! nc -z localhost ${CODE_SERVER_PORT:-8888} >/dev/null 2>&1; then echo "❌ Code Server service not detected on port ${CODE_SERVER_PORT:-8888}" all_running=false fi if ! nc -z localhost ${LISTMONK_PORT:-9000} >/dev/null 2>&1; then echo "❌ Listmonk service not detected on port ${LISTMONK_PORT:-9000}" all_running=false fi # At least check that some services are running if [ "$all_running" = false ]; then echo "Warning: Some services may not be running." read -p "Continue anyway? (y/n): " continue_answer if [[ ! "$continue_answer" =~ ^[Yy]$ ]]; then echo "Please start your services with 'docker compose up -d' first." exit 1 fi else echo "✅ Services appear to be running" fi } # Add this function to verify Cloudflare API credentials verify_cloudflare_credentials() { echo "Verifying Cloudflare API credentials..." # Test the API token and Zone ID with a simple request local TEST_RESPONSE=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID" \ -H "Authorization: Bearer $CF_API_TOKEN" \ -H "Content-Type: application/json") if echo "$TEST_RESPONSE" | jq -e '.success' > /dev/null 2>&1; then echo "✅ Cloudflare API credentials verified successfully" return 0 else echo "❌ Cloudflare API credentials verification failed" echo "Error details:" echo "$TEST_RESPONSE" | jq '.errors' echo "" echo "Common issues:" echo "1. Zone ID may be incorrect - make sure you're using the correct Zone ID from the Cloudflare dashboard" echo "2. API Token may not have the required permissions - make sure it has Zone:DNS and Zone:Settings permissions" echo "" echo "Your current Zone ID: $CF_ZONE_ID" echo "To continue without configuring DNS records, enter 'continue'. To quit and fix the credentials, enter 'quit'." read -p "Continue or quit? [continue/quit]: " cf_choice if [[ "$cf_choice" == "quit" ]]; then echo "Exiting. Please update your credentials in .env and try again." exit 1 else echo "Continuing without configuring DNS records..." return 1 fi fi } # Main execution flow echo "Starting Changemaker.lite production deployment..." # Check if services are running check_services # Check if cloudflared is installed, install if needed check_cloudflared # Authenticate with Cloudflare authenticate_cloudflare # Set up tunnel setup_tunnel # Create systemd service create_systemd_service # Verify Cloudflare credentials before proceeding with DNS configuration CF_CREDS_VALID=true if ! verify_cloudflare_credentials; then CF_CREDS_VALID=false echo "Skipping DNS record creation and access policy setup due to invalid credentials" fi # Only proceed with DNS configuration if credentials are valid if [ "$CF_CREDS_VALID" = true ]; then # Create/update DNS records echo "" echo "Creating/updating DNS records..." # Define subdomains declare -A SUBDOMAINS=( ["homepage"]="Homepage" ["code"]="Code Server" ["listmonk"]="Listmonk" ["docs"]="Documentation" ["n8n"]="n8n" ["db"]="NocoDB" ["git"]="Gitea" ["map"]="Map" ["qr"]="Mini QR" ) for subdomain in "${!SUBDOMAINS[@]}"; do create_dns_record "$subdomain" done # Create root domain record create_dns_record "@" echo "" echo "✅ All DNS records configured" # Set up access policies setup_access_policies else echo "" echo "⚠️ DNS records and access policies were not configured." echo "To manually configure DNS records, you can use the Cloudflare dashboard." echo "Add CNAME records for each subdomain pointing to: $CF_TUNNEL_ID.cfargotunnel.com" echo "" fi echo "" echo "#############################################################" echo "# " echo "# Production Deployment Complete! " echo "# " echo "#############################################################" echo "" echo "Tunnel Name: $TUNNEL_NAME" echo "Tunnel ID: $CF_TUNNEL_ID" echo "Service Name: $SERVICE_NAME" echo "" echo "Your services are now accessible at:" echo "" echo "Public services:" echo " - Main Site: https://$CF_DOMAIN" echo " - Listmonk: https://listmonk.$CF_DOMAIN" echo " - Documentation: https://docs.$CF_DOMAIN" echo " - n8n: https://n8n.$CF_DOMAIN" echo " - NocoDB: https://db.$CF_DOMAIN" echo " - Gitea: https://git.$CF_DOMAIN" echo " - Map: https://map.$CF_DOMAIN" echo " - Mini QR: https://qr.$CF_DOMAIN" echo "" echo "Protected services (requires login with $ADMIN_EMAIL):" echo " - Dashboard: https://homepage.$CF_DOMAIN" echo " - Code Server: https://code.$CF_DOMAIN" echo "" echo "Cloudflared service status: sudo systemctl status $SERVICE_NAME" echo "View tunnel logs: sudo journalctl -u $SERVICE_NAME -f" echo ""