#!/bin/bash # Deploy script for Release environment with multi-architecture support # Usage: ./deploy-release.sh set -euo pipefail # Configuration readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" readonly DEPLOY_DIR="/opt/bcards-staging" readonly DOCKER_COMPOSE_FILE="docker-compose.staging.yml" readonly CONTAINER_NAME="bcards-staging" readonly HEALTH_CHECK_URL="http://localhost:8090/health" readonly MAX_HEALTH_CHECK_ATTEMPTS=10 readonly HEALTH_CHECK_INTERVAL=10 readonly ROLLBACK_TIMEOUT=300 # Colors for output readonly RED='\033[0;31m' readonly GREEN='\033[0;32m' readonly YELLOW='\033[1;33m' readonly BLUE='\033[0;34m' readonly NC='\033[0m' # No Color # Logging functions log_info() { echo -e "${BLUE}[INFO]${NC} $1" } log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1" } log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1" } log_error() { echo -e "${RED}[ERROR]${NC} $1" } # Cleanup function cleanup() { local exit_code=$? if [ $exit_code -ne 0 ]; then log_error "Deployment failed with exit code $exit_code" rollback_deployment fi exit $exit_code } # Set trap for cleanup on exit trap cleanup EXIT # Validate input parameters validate_input() { if [ $# -ne 1 ]; then log_error "Usage: $0 " exit 1 fi local image_tag="$1" if [[ ! "$image_tag" =~ ^[a-zA-Z0-9._-]+$ ]]; then log_error "Invalid image tag format: $image_tag" exit 1 fi } # Check prerequisites check_prerequisites() { log_info "Checking prerequisites..." # Check if Docker is running if ! docker info >/dev/null 2>&1; then log_error "Docker is not running or not accessible" exit 1 fi # Check if docker-compose is available if ! command -v docker-compose >/dev/null 2>&1; then log_error "docker-compose is not installed" exit 1 fi # Check if deployment directory exists if [ ! -d "$DEPLOY_DIR" ]; then log_info "Creating deployment directory: $DEPLOY_DIR" mkdir -p "$DEPLOY_DIR" fi log_success "Prerequisites check passed" } # Backup current deployment backup_current_deployment() { log_info "Backing up current deployment..." local timestamp=$(date +%Y%m%d_%H%M%S) local backup_dir="$DEPLOY_DIR/backups/$timestamp" mkdir -p "$backup_dir" # Backup environment file if exists if [ -f "$DEPLOY_DIR/.env" ]; then cp "$DEPLOY_DIR/.env" "$backup_dir/.env.backup" cp "$DEPLOY_DIR/.env" "$DEPLOY_DIR/.env.backup" log_info "Environment file backed up" fi # Backup docker-compose file if exists if [ -f "$DEPLOY_DIR/$DOCKER_COMPOSE_FILE" ]; then cp "$DEPLOY_DIR/$DOCKER_COMPOSE_FILE" "$backup_dir/${DOCKER_COMPOSE_FILE}.backup" log_info "Docker compose file backed up" fi # Get current container image for potential rollback if docker ps --format "table {{.Names}}\t{{.Image}}" | grep -q "$CONTAINER_NAME"; then local current_image=$(docker inspect --format='{{.Config.Image}}' "$CONTAINER_NAME" 2>/dev/null || echo "") if [ -n "$current_image" ]; then echo "$current_image" > "$DEPLOY_DIR/.previous_image" log_info "Current image backed up: $current_image" fi fi log_success "Backup completed: $backup_dir" } # Test MongoDB connectivity test_mongodb_connection() { log_info "Testing MongoDB connectivity..." local mongodb_host="192.168.0.100" local mongodb_port="27017" # Test basic connectivity if timeout 10 bash -c "/dev/null; then log_success "MongoDB connection test passed" else log_error "Cannot connect to MongoDB at $mongodb_host:$mongodb_port" return 1 fi # Run detailed MongoDB test script if available if [ -f "$SCRIPT_DIR/test-mongodb-connection.sh" ]; then log_info "Running detailed MongoDB connection tests..." bash "$SCRIPT_DIR/test-mongodb-connection.sh" fi } # Pull new Docker image pull_docker_image() { local image_tag="$1" local full_image="registry.redecarneir.us/bcards:$image_tag" log_info "Pulling Docker image: $full_image" # Pull the multi-arch image if docker pull "$full_image"; then log_success "Image pulled successfully" else log_error "Failed to pull image: $full_image" return 1 fi # Verify image architecture local image_arch=$(docker inspect --format='{{.Architecture}}' "$full_image" 2>/dev/null || echo "unknown") local system_arch=$(uname -m) log_info "Image architecture: $image_arch" log_info "System architecture: $system_arch" # Convert system arch format to Docker format for comparison case "$system_arch" in x86_64) system_arch="amd64" ;; aarch64) system_arch="arm64" ;; esac if [ "$image_arch" = "$system_arch" ] || [ "$image_arch" = "unknown" ]; then log_success "Image architecture is compatible" else log_warning "Image architecture ($image_arch) may not match system ($system_arch), but multi-arch support should handle this" fi } # Deploy new version deploy_new_version() { local image_tag="$1" log_info "Deploying new version with tag: $image_tag" # Copy docker-compose file to deployment directory cp "$PROJECT_ROOT/$DOCKER_COMPOSE_FILE" "$DEPLOY_DIR/" # Create/update environment file cat > "$DEPLOY_DIR/.env" << EOF IMAGE_TAG=$image_tag REGISTRY=registry.redecarneir.us MONGODB_CONNECTION_STRING=mongodb://admin:c4rn31r0@129.146.116.218:27017,141.148.162.114:27017/BCardsDB?replicaSet=rs0&authSource=admin ASPNETCORE_ENVIRONMENT=Release CERT_PASSWORD= EOF # Stop existing containers cd "$DEPLOY_DIR" if docker-compose -f "$DOCKER_COMPOSE_FILE" ps -q | grep -q .; then log_info "Stopping existing containers..." docker-compose -f "$DOCKER_COMPOSE_FILE" down --remove-orphans fi # Start new containers log_info "Starting new containers..." docker-compose -f "$DOCKER_COMPOSE_FILE" up -d # Wait for containers to start sleep 15 log_success "New version deployed" } # Health check perform_health_check() { log_info "Performing health check..." local attempt=1 while [ $attempt -le $MAX_HEALTH_CHECK_ATTEMPTS ]; do log_info "Health check attempt $attempt/$MAX_HEALTH_CHECK_ATTEMPTS" # Check if container is running if ! docker ps --format "table {{.Names}}" | grep -q "$CONTAINER_NAME"; then log_warning "Container $CONTAINER_NAME is not running" else # Check application health endpoint if curl -f -s "$HEALTH_CHECK_URL" >/dev/null 2>&1; then log_success "Health check passed" return 0 fi # Check if the application is responding on port 80 if curl -f -s "http://localhost:8090/" >/dev/null 2>&1; then log_success "Application is responding (health endpoint may not be configured)" return 0 fi fi if [ $attempt -eq $MAX_HEALTH_CHECK_ATTEMPTS ]; then log_error "Health check failed after $MAX_HEALTH_CHECK_ATTEMPTS attempts" return 1 fi log_info "Waiting $HEALTH_CHECK_INTERVAL seconds before next attempt..." sleep $HEALTH_CHECK_INTERVAL ((attempt++)) done } # Rollback deployment rollback_deployment() { log_warning "Initiating rollback..." cd "$DEPLOY_DIR" # Stop current containers if [ -f "$DOCKER_COMPOSE_FILE" ]; then docker-compose -f "$DOCKER_COMPOSE_FILE" down --remove-orphans fi # Restore previous environment if backup exists if [ -f ".env.backup" ]; then mv ".env.backup" ".env" log_info "Previous environment restored" fi # Try to start previous version if image is available if [ -f ".previous_image" ]; then local previous_image=$(cat ".previous_image") log_info "Attempting to restore previous image: $previous_image" # Update .env with previous image tag local previous_tag=$(echo "$previous_image" | cut -d':' -f2) sed -i "s/IMAGE_TAG=.*/IMAGE_TAG=$previous_tag/" .env 2>/dev/null || true # Try to start previous version if docker-compose -f "$DOCKER_COMPOSE_FILE" up -d; then log_success "Rollback completed successfully" else log_error "Rollback failed - manual intervention required" fi else log_warning "No previous version found for rollback" fi } # Cleanup old images and containers cleanup_old_resources() { log_info "Cleaning up old Docker resources..." # Remove dangling images if docker images -f "dangling=true" -q | head -1 | grep -q .; then docker rmi $(docker images -f "dangling=true" -q) || true log_info "Dangling images removed" fi # Remove old backups (keep last 5) if [ -d "$DEPLOY_DIR/backups" ]; then find "$DEPLOY_DIR/backups" -maxdepth 1 -type d -name "20*" | sort -r | tail -n +6 | xargs rm -rf || true log_info "Old backups cleaned up" fi log_success "Cleanup completed" } # Display deployment summary display_summary() { local image_tag="$1" log_success "Deployment Summary:" echo "==================================" echo "🚀 Image Tag: $image_tag" echo "🌐 Environment: Release (Staging)" echo "🔗 Application URL: http://localhost:8090" echo "🔗 Health Check: $HEALTH_CHECK_URL" echo "🗄️ MongoDB: 192.168.0.100:27017" echo "📁 Deploy Directory: $DEPLOY_DIR" echo "🐳 Container: $CONTAINER_NAME" # Show container status echo "" echo "Container Status:" docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep -E "(NAMES|$CONTAINER_NAME)" || true # Show image info echo "" echo "Image Information:" docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.CreatedAt}}\t{{.Size}}" | grep -E "(REPOSITORY|bcards)" | head -5 || true echo "==================================" } # Main deployment function main() { local image_tag="$1" log_info "Starting deployment process for BCards Release environment" log_info "Target image tag: $image_tag" log_info "Target architecture: $(uname -m)" log_info "Deploy directory: $DEPLOY_DIR" # Execute deployment steps validate_input "$@" check_prerequisites test_mongodb_connection backup_current_deployment pull_docker_image "$image_tag" deploy_new_version "$image_tag" # Perform health check (rollback handled by trap if this fails) if perform_health_check; then cleanup_old_resources display_summary "$image_tag" log_success "Deployment completed successfully!" else log_error "Health check failed - rollback will be triggered" exit 1 fi } # Run main function with all arguments main "$@"