Replace oh-my-opencode-free references with the new consolidated flexinit/agent-stack image in source code and documentation.
387 lines
11 KiB
Bash
Executable File
387 lines
11 KiB
Bash
Executable File
#!/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
|