#!/bin/bash set -e BOLD=$(tput bold) GREEN=$(tput setaf 2) YELLOW=$(tput setaf 3) RED=$(tput setaf 1) CYAN=$(tput setaf 6) RESET=$(tput sgr0) command_exists() { command -v "$1" >/dev/null 2>&1 } generate_secret() { openssl rand -hex 16 } echo_bold() { echo "${BOLD}$1${RESET}" } echo_green() { echo "${GREEN}$1${RESET}" } echo_yellow() { echo "${YELLOW}$1${RESET}" } echo_cyan() { echo "${CYAN}$1${RESET}" } echo_bold "${CYAN}⛵ ZenSailor Production Installer${RESET}" echo "This script will set up a production-ready ZenSailor instance in /opt/zensailor." echo echo_yellow "This script requires sudo privileges to install software and manage files in /opt." sudo -v echo echo_yellow "STEP 1: Checking for dependencies..." if ! command_exists docker; then echo_yellow "Docker is not installed. Installing Docker automatically..." if curl -fsSL https://get.docker.com -o get-docker.sh; then sudo sh get-docker.sh rm get-docker.sh echo_green "✔ Docker installed successfully." else echo_bold "${RED}Error: Failed to download Docker installer.${RESET}" exit 1 fi else echo_green "✔ Docker is already installed." fi if ! docker compose version >/dev/null 2>&1; then echo_yellow "Docker Compose V2 not found. Attempting to install plugin..." sudo apt-get update 2>/dev/null || true sudo apt-get install -y docker-compose-plugin 2>/dev/null || \ sudo yum install -y docker-compose-plugin 2>/dev/null || \ echo_bold "${YELLOW}Warning: Could not auto-install Docker Compose. Please check manually.${RESET}" fi for cmd in curl tar openssl getent; do if ! command_exists $cmd; then echo_yellow "Installing missing dependency: $cmd..." if command_exists apt-get; then sudo apt-get update -qq && sudo apt-get install -y -qq $cmd elif command_exists yum; then sudo yum install -y -q $cmd elif command_exists apk; then sudo apk add -q $cmd else echo "${RED}Error: Dependency '$cmd' is missing and could not be auto-installed.${RESET}" exit 1 fi fi done echo_green "✔ All dependencies are satisfied." echo echo_yellow "STEP 2: Please provide some configuration details:" while [[ -z "$DOMAIN_NAME" || ! "$DOMAIN_NAME" =~ ^[a-zA-Z0-9.-]+$ ]]; do read -p "${BOLD}Enter the public domain for ZenSailor (e.g., zensailor.mycompany.com): ${RESET}" DOMAIN_NAME if [[ ! "$DOMAIN_NAME" =~ ^[a-zA-Z0-9.-]+$ ]]; then echo "${RED}Invalid domain format. Please try again.${RESET}" fi done while [[ -z "$ACME_EMAIL" || ! "$ACME_EMAIL" =~ ^[^@]+@[^@]+\.[^@]+$ ]]; do read -p "${BOLD}Enter your email for Let's Encrypt SSL certificates: ${RESET}" ACME_EMAIL if [[ ! "$ACME_EMAIL" =~ ^[^@]+@[^@]+\.[^@]+$ ]]; then echo "${RED}Invalid email format. Please try again.${RESET}" fi done echo INSTALL_DIR="/opt/zensailor" ZENSAILOR_TAG="latest" EFFECTIVE_USER=${SUDO_USER:-$USER} EFFECTIVE_GROUP=$(id -gn "$EFFECTIVE_USER") DOWNLOAD_BASE_URL="https://get.zensailor.com" if docker volume inspect zensailor-db-data-prod >/dev/null 2>&1; then echo_bold "${RED}WARNING: An existing database volume 'zensailor-db-data-prod' was found.${RESET}" echo "If you are reinstalling, the new random password will NOT match the old database password." echo "This will cause 'Authentication failed' errors." echo read -p "Do you want to DELETE the existing database volume to start fresh? (y/N) " DELETE_VOL if [[ "$DELETE_VOL" =~ ^[Yy]$ ]]; then echo_yellow "Removing old database volume..." docker volume rm zensailor-db-data-prod echo_green "✔ Volume removed." else echo_yellow "Keeping existing volume. Make sure your new .env password matches the old one!" fi echo fi if [ -d "$INSTALL_DIR" ]; then echo_yellow "STEP 3: Existing installation found in $INSTALL_DIR." echo "This script is for fresh installations. To update, please use the dashboard." exit 1 else echo_yellow "STEP 3: Downloading and extracting ZenSailor configuration into $INSTALL_DIR..." sudo mkdir -p "$INSTALL_DIR" ASSET_URL="${DOWNLOAD_BASE_URL}/uploads/zensailor-prod-dist.tar.gz" echo "Downloading from ${ASSET_URL}..." if ! sudo curl -sfL "${ASSET_URL}" | sudo tar -xzf - -C "$INSTALL_DIR"; then echo "${RED}Error: Failed to download or extract the ZenSailor distribution bundle.${RESET}" echo "Please check the URL and ensure a 'zensailor-prod-dist.tar.gz' file exists." fi fi echo_yellow "Creating persistent data directories..." sudo mkdir -p "$INSTALL_DIR/data/postgres" sudo mkdir -p "$INSTALL_DIR/data/registry" sudo mkdir -p "$INSTALL_DIR/data/traefik/letsencrypt" echo_green "✔ Data directories created." echo_yellow "Setting permissions for $INSTALL_DIR..." sudo chown -R "$EFFECTIVE_USER":"$EFFECTIVE_GROUP" "$INSTALL_DIR" echo_green "✔ ZenSailor is ready at $INSTALL_DIR." echo echo_yellow "STEP 4: Generating production .env file with secure secrets..." POSTGRES_USER="zensailor" POSTGRES_DB="zensailor" POSTGRES_PASSWORD=$(generate_secret) JWT_SECRET=$(generate_secret)$(generate_secret) ENCRYPTION_KEY=$(generate_secret) APP_URL="https://${DOMAIN_NAME}" REGISTRY_URL="zensailor-registry-prod:5000" UPDATES_REGISTRY_URL="https://registry-1.docker.io" if [ -S /var/run/docker.sock ]; then DOCKER_GID=$(stat -c '%g' /var/run/docker.sock) else DOCKER_GID=$(getent group docker | cut -d: -f3) fi if [ -z "$DOCKER_GID" ]; then echo "${RED}Error: Could not determine the Docker Group ID. Installation cannot proceed.${RESET}" exit 1 fi echo "Detected Docker GID: ${DOCKER_GID}" cat > "$INSTALL_DIR/.env" << EOF IMAGE_TAG=${ZENSAILOR_TAG} POSTGRES_DB=${POSTGRES_DB} POSTGRES_USER=${POSTGRES_USER} POSTGRES_PASSWORD=${POSTGRES_PASSWORD} DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@zensailor-postgres-prod:5432/${POSTGRES_DB}?schema=public APP_URL=${APP_URL} NEXT_PUBLIC_APP_URL=${APP_URL} JWT_SECRET=${JWT_SECRET} JWT_EXPIRES_IN_DAYS=7 ENCRYPTION_KEY=${ENCRYPTION_KEY} REGISTRY_URL=${REGISTRY_URL} UPDATES_REGISTRY_URL=${UPDATES_REGISTRY_URL} DIRECT_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@zensailor-postgres-prod:5432/${POSTGRES_DB}?schema=public PREMIUM_EMAILS= NATS_URL=nats://zensailor-nats-prod:4222 ACME_EMAIL=${ACME_EMAIL} ACTIVE_COLOR=blue COLOR=blue DOCKER_GID=${DOCKER_GID} EOF echo_green "✔ .env file created successfully." echo echo_yellow "STEP 5: Setting up initial Traefik configuration..." cd "$INSTALL_DIR" cat > traefik-config/dynamic/zensailor-router.yml << EOF http: routers: zensailor-dashboard: rule: "Host(\`${DOMAIN_NAME}\`)" entryPoints: - websecure service: "zensailor-control-plane-blue@docker" tls: certResolver: "letsencrypt" zensailor-dashboard-http: rule: "Host(\`${DOMAIN_NAME}\`)" entryPoints: - web middlewares: - https-redirect service: "zensailor-control-plane-blue@docker" middlewares: https-redirect: redirectScheme: scheme: https permanent: true security-headers: headers: frameDeny: true contentTypeNosniff: true browserXssFilter: true forceSTSHeader: true stsIncludeSubdomains: true stsPreload: true stsSeconds: 31536000 EOF echo_green "✔ Initial router created, pointing to 'blue' deployment." echo echo_yellow "STEP 6: Starting the ZenSailor production stack..." echo_yellow "Pulling required Docker images... (This may take a moment)" docker compose -f docker-compose.prod.yml pull echo_yellow "Starting ZenSailor services..." docker compose -f docker-compose.prod.yml up -d echo echo_green "⛵ ZenSailor installation is complete! ⛵" echo echo "Your ZenSailor dashboard should be available shortly at:" echo " ${BOLD}${CYAN}${APP_URL}${RESET}" echo echo "It may take a minute for the SSL certificate to be issued." echo "The first user to sign up will become the platform administrator." echo echo "To manage your installation, navigate to the '${BOLD}${INSTALL_DIR}${RESET}' directory." echo " - View logs: ${BOLD}cd ${INSTALL_DIR} && docker compose -f docker-compose.prod.yml logs -f${RESET}" echo " - Stop: ${BOLD}cd ${INSTALL_DIR} && docker compose -f docker-compose.prod.yml down${RESET}" echo