348 lines
12 KiB
Bash
348 lines
12 KiB
Bash
#!/bin/bash
|
|
|
|
echo "#############################################################"
|
|
echo "# "
|
|
echo "# Changemaker.lite Production Setup "
|
|
echo "# "
|
|
echo "# This script will: "
|
|
echo "# 1. Create a Cloudflare tunnel "
|
|
echo "# 2. Configure DNS records "
|
|
echo "# 3. Set up access policies "
|
|
echo "# 4. Enable the cloudflared container "
|
|
echo "# "
|
|
echo "#############################################################"
|
|
echo ""
|
|
|
|
# Get script directory
|
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
ENV_FILE="$SCRIPT_DIR/.env"
|
|
DOCKER_COMPOSE_FILE="$SCRIPT_DIR/docker-compose.yml"
|
|
|
|
# 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
|
|
|
|
# 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" ]; then
|
|
echo "Error: Cloudflare credentials not properly configured in .env file."
|
|
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 ""
|
|
echo "Please run ./config.sh and configure Cloudflare settings, or manually update your .env file."
|
|
exit 1
|
|
fi
|
|
|
|
# Check if CF_ACCOUNT_ID exists in .env, if not prompt for it
|
|
if [ -z "$CF_ACCOUNT_ID" ]; then
|
|
echo "Cloudflare Account ID is required for tunnel creation."
|
|
echo "You can find it in your Cloudflare dashboard at the top right."
|
|
read -p "Enter your Cloudflare Account ID: " CF_ACCOUNT_ID
|
|
|
|
# Update .env with account ID
|
|
echo "" >> "$ENV_FILE"
|
|
echo "# Cloudflare Account ID (added by start-production.sh)" >> "$ENV_FILE"
|
|
echo "CF_ACCOUNT_ID=$CF_ACCOUNT_ID" >> "$ENV_FILE"
|
|
|
|
# Re-export the variable
|
|
export CF_ACCOUNT_ID
|
|
fi
|
|
|
|
# Check if required variables are set
|
|
if [ -z "$CF_API_TOKEN" ] || [ -z "$CF_ZONE_ID" ] || [ -z "$CF_DOMAIN" ]; then
|
|
echo "Error: Cloudflare credentials not found in .env file."
|
|
echo "Please run ./config.sh and configure Cloudflare settings."
|
|
exit 1
|
|
fi
|
|
|
|
# Check if services are running
|
|
echo "Checking if services are running..."
|
|
if ! docker compose ps | grep -q "Up"; then
|
|
echo "Error: No services are running. Please run 'docker compose up -d' first."
|
|
exit 1
|
|
fi
|
|
|
|
echo "✓ Services are running"
|
|
echo ""
|
|
|
|
# Show current service URLs
|
|
echo "Current local service URLs:"
|
|
echo " - Homepage: http://localhost:${HOMEPAGE_PORT:-3010}"
|
|
echo " - Code Server: http://localhost:${CODE_SERVER_PORT:-8888}"
|
|
echo " - Listmonk: http://localhost:${LISTMONK_PORT:-9000}"
|
|
echo " - Documentation (Dev): http://localhost:${MKDOCS_PORT:-4000}"
|
|
echo " - Documentation (Built): http://localhost:${MKDOCS_SITE_SERVER_PORT:-4001}"
|
|
echo " - n8n: http://localhost:${N8N_PORT:-5678}"
|
|
echo " - NocoDB: http://localhost:${NOCODB_PORT:-8090}"
|
|
echo " - Gitea: http://localhost:${GITEA_WEB_PORT:-3030}"
|
|
echo ""
|
|
|
|
read -p "Have you tested all services locally and are ready to go to production? (yes/no): " confirm
|
|
if [[ "$confirm" != "yes" ]]; then
|
|
echo "Please test all services locally before running production setup."
|
|
exit 0
|
|
fi
|
|
|
|
echo ""
|
|
echo "Starting production setup..."
|
|
|
|
# Install jq if not present
|
|
if ! command -v jq &> /dev/null; then
|
|
echo "Installing jq..."
|
|
sudo apt-get update && sudo apt-get install -y jq
|
|
fi
|
|
|
|
# Create Cloudflare tunnel
|
|
echo ""
|
|
echo "Creating Cloudflare tunnel..."
|
|
|
|
TUNNEL_NAME="changemaker-${HOSTNAME}-$(date +%s)"
|
|
|
|
# Create tunnel via API
|
|
TUNNEL_RESPONSE=$(curl -s -X POST "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/cfd_tunnel" \
|
|
-H "Authorization: Bearer $CF_API_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
--data "{\"name\":\"$TUNNEL_NAME\",\"config_src\":\"cloudflare\"}")
|
|
|
|
# Check if we need account ID
|
|
if echo "$TUNNEL_RESPONSE" | jq -e '.errors[] | select(.code == 0)' > /dev/null 2>&1; then
|
|
echo ""
|
|
echo "Account ID is required. You can find it in your Cloudflare dashboard."
|
|
read -p "Enter your Cloudflare Account ID: " CF_ACCOUNT_ID
|
|
|
|
# Update .env with account ID
|
|
if grep -q "^CF_ACCOUNT_ID=" "$ENV_FILE"; then
|
|
sed -i "s/^CF_ACCOUNT_ID=.*/CF_ACCOUNT_ID=$CF_ACCOUNT_ID/" "$ENV_FILE"
|
|
else
|
|
echo "CF_ACCOUNT_ID=$CF_ACCOUNT_ID" >> "$ENV_FILE"
|
|
fi
|
|
|
|
# Retry with account ID
|
|
TUNNEL_RESPONSE=$(curl -s -X POST "https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT_ID/cfd_tunnel" \
|
|
-H "Authorization: Bearer $CF_API_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
--data "{\"name\":\"$TUNNEL_NAME\",\"config_src\":\"cloudflare\"}")
|
|
fi
|
|
|
|
TUNNEL_ID=$(echo "$TUNNEL_RESPONSE" | jq -r '.result.id')
|
|
TUNNEL_TOKEN=$(echo "$TUNNEL_RESPONSE" | jq -r '.result.token')
|
|
|
|
if [ -z "$TUNNEL_ID" ] || [ "$TUNNEL_ID" == "null" ]; then
|
|
echo "Error: Failed to create tunnel"
|
|
echo "$TUNNEL_RESPONSE" | jq .
|
|
exit 1
|
|
fi
|
|
|
|
echo "✓ Created tunnel: $TUNNEL_NAME (ID: $TUNNEL_ID)"
|
|
|
|
# Update .env with tunnel information
|
|
echo "CF_TUNNEL_ID=$TUNNEL_ID" >> "$ENV_FILE"
|
|
echo "CF_TUNNEL_TOKEN=$TUNNEL_TOKEN" >> "$ENV_FILE"
|
|
echo "CF_TUNNEL_NAME=$TUNNEL_NAME" >> "$ENV_FILE"
|
|
|
|
# Configure tunnel routes
|
|
echo ""
|
|
echo "Configuring tunnel routes..."
|
|
|
|
# Define services and their configurations
|
|
declare -A SERVICES=(
|
|
["dashboard"]="homepage-changemaker:3000"
|
|
["code"]="code-server:8080"
|
|
["listmonk"]="listmonk-app:9000"
|
|
["docs"]="mkdocs:8000"
|
|
["n8n"]="n8n:5678"
|
|
["db"]="nocodb:8080"
|
|
["git"]="gitea-app:3000"
|
|
)
|
|
|
|
# Configure root domain to mkdocs-site-server
|
|
echo "Configuring route for root domain..."
|
|
ROOT_CONFIG=$(cat <<EOF
|
|
{
|
|
"tunnel_id": "$TUNNEL_ID",
|
|
"hostname": "$CF_DOMAIN",
|
|
"service": "http://mkdocs-site-server:80"
|
|
}
|
|
EOF
|
|
)
|
|
|
|
ROOT_RESPONSE=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/tunnels/$TUNNEL_ID/configurations" \
|
|
-H "Authorization: Bearer $CF_API_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
--data "$ROOT_CONFIG")
|
|
|
|
# Configure subdomain routes
|
|
for subdomain in "${!SERVICES[@]}"; do
|
|
service="${SERVICES[$subdomain]}"
|
|
echo "Configuring route for $subdomain.$CF_DOMAIN -> $service"
|
|
|
|
CONFIG=$(cat <<EOF
|
|
{
|
|
"tunnel_id": "$TUNNEL_ID",
|
|
"hostname": "$subdomain.$CF_DOMAIN",
|
|
"service": "http://$service"
|
|
}
|
|
EOF
|
|
)
|
|
|
|
RESPONSE=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/tunnels/$TUNNEL_ID/configurations" \
|
|
-H "Authorization: Bearer $CF_API_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
--data "$CONFIG")
|
|
done
|
|
|
|
# Create DNS records
|
|
echo ""
|
|
echo "Creating DNS records..."
|
|
|
|
# Function to create/update DNS record
|
|
create_dns_record() {
|
|
local name=$1
|
|
local content="$TUNNEL_ID.cfargotunnel.com"
|
|
|
|
# Check if record exists
|
|
EXISTING=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records?name=$name.$CF_DOMAIN&type=CNAME" \
|
|
-H "Authorization: Bearer $CF_API_TOKEN" \
|
|
-H "Content-Type: application/json")
|
|
|
|
RECORD_ID=$(echo "$EXISTING" | jq -r '.result[0].id')
|
|
|
|
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 for $name.$CF_DOMAIN configured"
|
|
else
|
|
echo "✗ Failed to configure DNS record for $name.$CF_DOMAIN"
|
|
echo "$RESPONSE" | jq '.errors'
|
|
fi
|
|
}
|
|
|
|
# Create DNS records for all subdomains
|
|
for subdomain in "${!SERVICES[@]}"; do
|
|
create_dns_record "$subdomain"
|
|
done
|
|
|
|
# Create root domain record
|
|
create_dns_record "@"
|
|
|
|
# Set up access policies
|
|
echo ""
|
|
echo "Setting up access policies..."
|
|
read -p "Enter admin email for protected services (dashboard, code): " ADMIN_EMAIL
|
|
|
|
if [[ "$ADMIN_EMAIL" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
|
|
# Create access applications for protected services
|
|
PROTECTED_SERVICES=("dashboard" "code")
|
|
|
|
for service in "${PROTECTED_SERVICES[@]}"; do
|
|
echo "Creating access policy for $service.$CF_DOMAIN..."
|
|
|
|
# Create 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\"
|
|
}")
|
|
|
|
APP_ID=$(echo "$APP_RESPONSE" | jq -r '.result.id')
|
|
|
|
if [ ! -z "$APP_ID" ] && [ "$APP_ID" != "null" ]; then
|
|
# Create 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\"}
|
|
}]
|
|
}")
|
|
|
|
echo "✓ Access policy created for $service.$CF_DOMAIN"
|
|
fi
|
|
done
|
|
else
|
|
echo "Invalid email format. Skipping access policy setup."
|
|
fi
|
|
|
|
# Enable cloudflared in docker-compose
|
|
echo ""
|
|
echo "Enabling cloudflared container..."
|
|
|
|
# Uncomment cloudflared service in docker-compose.yml
|
|
sed -i '/# cloudflared:/,/# gitea-app/s/^ # / /' "$DOCKER_COMPOSE_FILE"
|
|
|
|
# Start cloudflared container
|
|
docker compose up -d cloudflared
|
|
|
|
echo ""
|
|
echo "#############################################################"
|
|
echo "# "
|
|
echo "# Production Setup Complete! "
|
|
echo "# "
|
|
echo "#############################################################"
|
|
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 ""
|
|
if [ ! -z "$ADMIN_EMAIL" ]; then
|
|
echo "Protected services (login required with $ADMIN_EMAIL):"
|
|
echo " - Dashboard: https://dashboard.$CF_DOMAIN"
|
|
echo " - Code Server: https://code.$CF_DOMAIN"
|
|
else
|
|
echo "Admin services (no protection configured):"
|
|
echo " - Dashboard: https://dashboard.$CF_DOMAIN"
|
|
echo " - Code Server: https://code.$CF_DOMAIN"
|
|
fi
|
|
echo ""
|
|
echo "Note: DNS propagation may take a few minutes."
|
|
echo ""
|
|
echo "To stop production mode and return to local-only:"
|
|
echo " docker compose stop cloudflared"
|
|
echo ""
|
|
echo "To fully remove production setup:"
|
|
echo " docker compose down cloudflared" |