freealberta/start-production.sh

642 lines
22 KiB
Bash
Executable File

#!/bin/bash
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"
echo "Using tunnel name: $TUNNEL_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..."
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 ""
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 ""