#!/usr/bin/env bash # # AI Stack Deployer - Automated Deployment Script # Deploys the AI Stack Deployer application to Dokploy # # Usage: ./scripts/deploy-to-dokploy.sh [options] # Options: # --registry REGISTRY Docker registry to push to (default: local Docker) # --domain DOMAIN Domain name for deployment (default: portal.ai.flexinit.nl) # --project-name NAME Dokploy project name (default: ai-stack-deployer-portal) # --skip-build Skip Docker build (use existing image) # --skip-test Skip local testing # set -euo pipefail # Configuration SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="${SCRIPT_DIR}/.." TIMESTAMP=$(date +%Y%m%d-%H%M%S) LOG_FILE="${PROJECT_ROOT}/deployment-${TIMESTAMP}.log" # Default values REGISTRY="" DOMAIN="portal.ai.flexinit.nl" PROJECT_NAME="ai-stack-deployer-portal" APP_NAME="ai-stack-deployer-web" IMAGE_NAME="ai-stack-deployer" SKIP_BUILD=false SKIP_TEST=false # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color # Logging functions log_info() { echo -e "${GREEN}[INFO]${NC} $*" | tee -a "${LOG_FILE}" } log_warn() { echo -e "${YELLOW}[WARN]${NC} $*" | tee -a "${LOG_FILE}" } log_error() { echo -e "${RED}[ERROR]${NC} $*" | tee -a "${LOG_FILE}" } log_step() { echo -e "\n${GREEN}==>${NC} $*\n" | tee -a "${LOG_FILE}" } # Error handler error_exit() { log_error "$1" exit 1 } # Parse arguments while [[ $# -gt 0 ]]; do case $1 in --registry) REGISTRY="$2" shift 2 ;; --domain) DOMAIN="$2" shift 2 ;; --project-name) PROJECT_NAME="$2" shift 2 ;; --skip-build) SKIP_BUILD=true shift ;; --skip-test) SKIP_TEST=true shift ;; *) echo "Unknown option: $1" exit 1 ;; esac done log_info "Deployment started at ${TIMESTAMP}" log_info "Target domain: ${DOMAIN}" log_info "Project name: ${PROJECT_NAME}" cd "${PROJECT_ROOT}" # Load environment variables if [ ! -f .env ]; then error_exit ".env file not found. Please create it from .env.example" fi source .env if [ -z "${DOKPLOY_API_TOKEN}" ]; then error_exit "DOKPLOY_API_TOKEN not set in .env" fi if [ -z "${DOKPLOY_URL}" ]; then error_exit "DOKPLOY_URL not set in .env" fi # Phase 1: Pre-deployment checks log_step "Phase 1: Pre-deployment Verification" log_info "Checking Dokploy API connectivity..." HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ -H "x-api-key: ${DOKPLOY_API_TOKEN}" \ "${DOKPLOY_URL}/api/project.all" || echo "000") if [ "${HTTP_CODE}" != "200" ]; then error_exit "Dokploy API unreachable or unauthorized (HTTP ${HTTP_CODE})" fi log_info "✓ Dokploy API accessible" # Check Docker if ! command -v docker &> /dev/null; then error_exit "Docker not installed" fi # Try docker with sg if needed if ! docker ps &> /dev/null; then if sg docker -c "docker ps" &> /dev/null; then log_info "Using 'sg docker' for Docker commands" DOCKER_CMD="sg docker -c" else error_exit "Docker not accessible. Check permissions." fi else DOCKER_CMD="" fi log_info "✓ Docker accessible" # Phase 2: Build Docker image if [ "${SKIP_BUILD}" = false ]; then log_step "Phase 2: Building Docker Image" log_info "Building image: ${IMAGE_NAME}:${TIMESTAMP}" ${DOCKER_CMD} docker build \ -t "${IMAGE_NAME}:${TIMESTAMP}" \ -t "${IMAGE_NAME}:latest" \ . 2>&1 | tee -a "${LOG_FILE}" || error_exit "Docker build failed" # Get image info IMAGE_ID=$(${DOCKER_CMD} docker images -q "${IMAGE_NAME}:latest") IMAGE_SIZE=$(${DOCKER_CMD} docker images "${IMAGE_NAME}:latest" --format "{{.Size}}") log_info "✓ Image built successfully" log_info " Image ID: ${IMAGE_ID}" log_info " Size: ${IMAGE_SIZE}" else log_warn "Skipping build (--skip-build specified)" fi # Phase 3: Local testing if [ "${SKIP_TEST}" = false ]; then log_step "Phase 3: Local Container Testing" log_info "Starting test container..." # Stop existing test container if any ${DOCKER_CMD} docker stop ai-stack-deployer-test 2>/dev/null || true ${DOCKER_CMD} docker rm ai-stack-deployer-test 2>/dev/null || true # Start test container ${DOCKER_CMD} docker run -d \ --name ai-stack-deployer-test \ -p 3001:3000 \ --env-file .env \ "${IMAGE_NAME}:latest" || error_exit "Failed to start test container" log_info "Waiting for container to be healthy..." sleep 5 # Test health endpoint if ! curl -sf http://localhost:3001/health > /dev/null; then ${DOCKER_CMD} docker logs ai-stack-deployer-test | tail -20 ${DOCKER_CMD} docker stop ai-stack-deployer-test ${DOCKER_CMD} docker rm ai-stack-deployer-test error_exit "Health check failed" fi log_info "✓ Container healthy" # Cleanup ${DOCKER_CMD} docker stop ai-stack-deployer-test ${DOCKER_CMD} docker rm ai-stack-deployer-test log_info "✓ Local testing complete" else log_warn "Skipping local test (--skip-test specified)" fi # Phase 4: Registry push (if specified) if [ -n "${REGISTRY}" ]; then log_step "Phase 4: Pushing to Registry" FULL_IMAGE_NAME="${REGISTRY}/${IMAGE_NAME}:latest" log_info "Tagging image for registry: ${FULL_IMAGE_NAME}" ${DOCKER_CMD} docker tag "${IMAGE_NAME}:latest" "${FULL_IMAGE_NAME}" log_info "Pushing to registry..." ${DOCKER_CMD} docker push "${FULL_IMAGE_NAME}" || error_exit "Failed to push to registry" log_info "✓ Image pushed to registry" IMAGE_FOR_DOKPLOY="${FULL_IMAGE_NAME}" else log_warn "No registry specified - Dokploy must have access to local Docker" IMAGE_FOR_DOKPLOY="${IMAGE_NAME}:latest" fi # Phase 5: Deploy to Dokploy log_step "Phase 5: Deploying to Dokploy" # Check if project exists log_info "Checking for existing project..." EXISTING_PROJECT=$(curl -s \ -H "x-api-key: ${DOKPLOY_API_TOKEN}" \ "${DOKPLOY_URL}/api/project.all" | \ jq -r ".[]? | select(.name==\"${PROJECT_NAME}\") | .projectId" || echo "") if [ -n "${EXISTING_PROJECT}" ]; then log_info "✓ Found existing project: ${EXISTING_PROJECT}" PROJECT_ID="${EXISTING_PROJECT}" else log_info "Creating new project..." CREATE_PROJECT_RESPONSE=$(curl -s -X POST \ -H "x-api-key: ${DOKPLOY_API_TOKEN}" \ -H "Content-Type: application/json" \ "${DOKPLOY_URL}/api/project.create" \ -d "{ \"name\": \"${PROJECT_NAME}\", \"description\": \"Self-service portal for deploying AI stacks\" }") || error_exit "Failed to create project" PROJECT_ID=$(echo "${CREATE_PROJECT_RESPONSE}" | jq -r '.project.projectId // empty') if [ -z "${PROJECT_ID}" ]; then log_error "API Response: ${CREATE_PROJECT_RESPONSE}" error_exit "Failed to extract project ID" fi log_info "✓ Created project: ${PROJECT_ID}" fi # Create/Update application log_info "Creating application..." # Prepare environment variables ENV_VARS="DOKPLOY_URL=${DOKPLOY_URL} DOKPLOY_API_TOKEN=${DOKPLOY_API_TOKEN} PORT=3000 HOST=0.0.0.0 STACK_DOMAIN_SUFFIX=${STACK_DOMAIN_SUFFIX:-ai.flexinit.nl} STACK_IMAGE=${STACK_IMAGE:-git.app.flexinit.nl/flexinit/agent-stack:latest} RESERVED_NAMES=${RESERVED_NAMES:-admin,api,www,root,system,test,demo,portal}" CREATE_APP_RESPONSE=$(curl -s -X POST \ -H "x-api-key: ${DOKPLOY_API_TOKEN}" \ -H "Content-Type: application/json" \ "${DOKPLOY_URL}/api/application.create" \ -d "{ \"name\": \"${APP_NAME}\", \"projectId\": \"${PROJECT_ID}\", \"dockerImage\": \"${IMAGE_FOR_DOKPLOY}\", \"env\": \"${ENV_VARS}\" }") || error_exit "Failed to create application" APP_ID=$(echo "${CREATE_APP_RESPONSE}" | jq -r '.application.applicationId // .applicationId // empty') if [ -z "${APP_ID}" ]; then log_error "API Response: ${CREATE_APP_RESPONSE}" error_exit "Failed to extract application ID" fi log_info "✓ Created application: ${APP_ID}" # Configure domain log_info "Configuring domain: ${DOMAIN}" curl -s -X POST \ -H "x-api-key: ${DOKPLOY_API_TOKEN}" \ -H "Content-Type: application/json" \ "${DOKPLOY_URL}/api/domain.create" \ -d "{ \"domain\": \"${DOMAIN}\", \"applicationId\": \"${APP_ID}\", \"https\": true, \"port\": 3000 }" || log_warn "Domain configuration may have failed (might already exist)" log_info "✓ Domain configured" # Deploy application log_info "Triggering deployment..." DEPLOY_RESPONSE=$(curl -s -X POST \ -H "x-api-key: ${DOKPLOY_API_TOKEN}" \ -H "Content-Type: application/json" \ "${DOKPLOY_URL}/api/application.deploy" \ -d "{ \"applicationId\": \"${APP_ID}\" }") || error_exit "Failed to trigger deployment" DEPLOY_ID=$(echo "${DEPLOY_RESPONSE}" | jq -r '.deploymentId // "unknown"') log_info "✓ Deployment triggered: ${DEPLOY_ID}" log_info "Monitor at: ${DOKPLOY_URL}/project/${PROJECT_ID}" # Phase 6: Verification log_step "Phase 6: Deployment Verification" log_info "Waiting for deployment (60 seconds)..." sleep 60 log_info "Testing health endpoint..." if curl -sf "https://${DOMAIN}/health" > /dev/null 2>&1; then log_info "✓ Application is healthy at https://${DOMAIN}" # Get health status HEALTH_RESPONSE=$(curl -s "https://${DOMAIN}/health") echo "${HEALTH_RESPONSE}" | jq . || echo "${HEALTH_RESPONSE}" else log_warn "Health check failed - deployment may still be in progress" log_info "Check status at: ${DOKPLOY_URL}/project/${PROJECT_ID}" fi # Save deployment record log_step "Deployment Summary" cat > "deployment-record-${TIMESTAMP}.txt" << EOF Deployment Completed: $(date -Iseconds) =========================================== Configuration: - Project Name: ${PROJECT_NAME} - Application Name: ${APP_NAME} - Domain: https://${DOMAIN} - Image: ${IMAGE_FOR_DOKPLOY} Dokploy IDs: - Project ID: ${PROJECT_ID} - Application ID: ${APP_ID} - Deployment ID: ${DEPLOY_ID} Management: - Dokploy Console: ${DOKPLOY_URL}/project/${PROJECT_ID} - Application URL: https://${DOMAIN} - Health Check: https://${DOMAIN}/health Build Info: - Timestamp: ${TIMESTAMP} - Image ID: ${IMAGE_ID:-N/A} - Image Size: ${IMAGE_SIZE:-N/A} Status: SUCCESS EOF log_info "Deployment record saved: deployment-record-${TIMESTAMP}.txt" log_info "Deployment log saved: ${LOG_FILE}" echo "" echo -e "${GREEN}========================================${NC}" echo -e "${GREEN} Deployment Completed Successfully!${NC}" echo -e "${GREEN}========================================${NC}" echo "" echo -e " 🌐 Application URL: ${GREEN}https://${DOMAIN}${NC}" echo -e " ⚙️ Dokploy Console: ${DOKPLOY_URL}/project/${PROJECT_ID}" echo -e " 📊 Health Check: https://${DOMAIN}/health" echo "" exit 0