#!/bin/bash set -euo pipefail # Blue-Green deployment script with versioned releases # Usage: ./deploy-blue-green.sh # Source common functions SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "${SCRIPT_DIR}/common-lib.sh" # Validate required environment variables require_var "DOCKER_HOST" require_var "REPO_PROJECT_PATH" require_var "REPO_NAME_ONLY" require_var "REPO" require_var "IMAGE_TAR" require_var "ENV_FILE_BASE64" require_var "CF_PEM_CERT" require_var "CF_PEM_CA" require_var "PROD" require_var "PRODUCTION_DOMAIN" require_var "STAGING_DOMAIN" require_var "REPLICA_COUNT" validate_deployment_env echo "âš™ī¸ Docker host: $DOCKER_HOST" # Generate deployment timestamp DEPLOYMENT_TIMESTAMP=$(date +%Y%m%d_%H%M%S) NEW_RELEASE_PATH="${RELEASES_PATH}/${DEPLOYMENT_TIMESTAMP}" # Use Git SHA for image tag (if available, otherwise use timestamp) if [ -n "${GIT_SHA:-}" ]; then IMAGE_TAG="sha-${GIT_SHA:0:7}" else # Fallback for local testing without GIT_SHA IMAGE_TAG="local-${DEPLOYMENT_TIMESTAMP}" fi # Check for deployment in progress if is_deployment_in_progress; then echo "âš ī¸ ERROR: Deployment appears to be in progress (both colors are running)" echo " This might indicate a previous deployment didn't complete properly." echo " Please check the deployment status and clean up any old containers." echo " If you are sure that the deployment is complete, you can run the following command to clean up the old containers:" echo " ssh deploy 'docker compose -p pkmntrade-club-blue down && docker compose -p pkmntrade-club-green down'" exit 1 fi # Get current and new colors CURRENT_COLOR=$(get_current_color) NEW_COLOR=$(switch_color "$CURRENT_COLOR") echo "📅 Deployment version: ${DEPLOYMENT_TIMESTAMP}" echo "đŸˇī¸ Image tag: ${IMAGE_TAG}" echo "🎨 Current: $CURRENT_COLOR → New: $NEW_COLOR" echo "🚀 Enable and start docker service" retry run_on_target "sudo systemctl enable --now docker.service" echo "💾 Load the new docker image ($IMAGE_TAR)" if [ ! -f "$IMAGE_TAR" ]; then echo "Error: Docker image tar file not found: $IMAGE_TAR" exit 1 fi # Load the image - Docker handles the transfer via DOCKER_HOST echo "đŸ“Ļ Loading Docker image..." #retry docker load -i "$IMAGE_TAR" # Verify the expected image exists echo "🔍 Verifying image ${REPO}:${IMAGE_TAG} exists..." if ! docker images -q "${REPO}:${IMAGE_TAG}" | grep -q .; then echo "❌ Expected image tag ${IMAGE_TAG} not found!" echo "Available tags:" docker images "${REPO}" --format "{{.Tag}}" exit 1 fi echo "📁 Create versioned release directory" run_on_target "mkdir -p '${NEW_RELEASE_PATH}'" echo "💾 Copy new files to server" if [ -d "./server" ]; then retry scp -pr ./server/* "deploy:${NEW_RELEASE_PATH}/" else echo "âš ī¸ No server directory found, error" exit 1 fi echo "📝 Create new .env file with deployment configuration" printf "%s" "${ENV_FILE_BASE64}" | base64 -d | run_on_target "cat > '${NEW_RELEASE_PATH}/.env' && chmod 600 '${NEW_RELEASE_PATH}/.env'" # Add deployment color and image tag to .env run_on_target "echo 'DEPLOYMENT_COLOR=${NEW_COLOR}' >> '${NEW_RELEASE_PATH}/.env'" run_on_target "echo 'IMAGE_TAG=${IMAGE_TAG}' >> '${NEW_RELEASE_PATH}/.env'" # Add domain name based on environment if [ "${PROD}" = "true" ]; then DOMAIN_NAME="${PRODUCTION_DOMAIN:-pkmntrade.club}" else DOMAIN_NAME="${STAGING_DOMAIN:-staging.pkmntrade.club}" fi # if there is a third part to the domain name, remove it BASE_DOMAIN_NAME="${BASE_DOMAIN:-pkmntrade.club}" run_on_target "echo 'DOMAIN_NAME=${DOMAIN_NAME}' >> '${NEW_RELEASE_PATH}/.env'" run_on_target "echo 'BASE_DOMAIN_NAME=${BASE_DOMAIN_NAME}' >> '${NEW_RELEASE_PATH}/.env'" run_on_target "echo 'REPLICA_COUNT=${REPLICA_COUNT}' >> '${NEW_RELEASE_PATH}/.env'" echo "🔑 Set up certs" run_on_target "mkdir -p '${NEW_RELEASE_PATH}/certs' && chmod 550 '${NEW_RELEASE_PATH}/certs' && chown 99:root '${NEW_RELEASE_PATH}/certs'" printf "%s" "$CF_PEM_CERT" | run_on_target "cat > '${NEW_RELEASE_PATH}/certs/crt.pem' && chmod 440 '${NEW_RELEASE_PATH}/certs/crt.pem' && chown 99:root '${NEW_RELEASE_PATH}/certs/crt.pem'" printf "%s" "$CF_PEM_CA" | run_on_target "cat > '${NEW_RELEASE_PATH}/certs/ca.pem' && chmod 440 '${NEW_RELEASE_PATH}/certs/ca.pem' && chown 99:root '${NEW_RELEASE_PATH}/certs/ca.pem'" echo "📝 Save deployment metadata" run_on_target "echo '${DEPLOYMENT_TIMESTAMP}' > '${NEW_RELEASE_PATH}/.deployment_version'" run_on_target "echo '${PROD}' > '${NEW_RELEASE_PATH}/.deployment_is_prod'" run_on_target "echo '${NEW_COLOR}' > '${NEW_RELEASE_PATH}/.deployment_color'" run_on_target "echo '${IMAGE_TAG}' > '${NEW_RELEASE_PATH}/.image_tag'" run_on_target "echo '${GIT_SHA:-unknown}' > '${NEW_RELEASE_PATH}/.git_sha'" # Save previous version info for potential rollback run_on_target "if [ -L '${CURRENT_LINK_PATH}' ]; then readlink -f '${CURRENT_LINK_PATH}' > '${NEW_RELEASE_PATH}/.previous_version'; fi" # export PREVIOUS_RELEASE_PATH if [ "$CURRENT_COLOR" != "none" ]; then PREVIOUS_RELEASE_PATH=$(run_on_target "cat ${NEW_RELEASE_PATH}/.previous_version") else PREVIOUS_RELEASE_PATH="" fi run_on_target "export PREVIOUS_RELEASE_PATH='${PREVIOUS_RELEASE_PATH}'" echo "🔗 Update current symlink to new release" run_on_target "ln -sfn '${NEW_RELEASE_PATH}' '${CURRENT_LINK_PATH}'" # Get deployment configuration PROJECT_NAME=$(get_project_name "$NEW_COLOR") COMPOSE_FILES=$(get_compose_files) WEB_SERVICE=$(get_web_service_name) # create network if it doesn't exist echo "🔗 Creating network ${PROJECT_NAME}_network" run_on_target "docker network create ${REPO_NAME_ONLY}_network >/dev/null 2>&1 || true" # Handle core services if [ "$CURRENT_COLOR" = "none" ]; then echo "🚀 Starting core services (first deployment)" retry run_on_target "cd '${CURRENT_LINK_PATH}' && docker compose -f docker-compose_core.yml -p ${CORE_PROJECT_NAME} up -d" sleep 10 # Give core services time to start else echo "â„šī¸ Core services already running, checking for changes..." PREVIOUS_SHA1=$(run_on_target "sha1sum '${PREVIOUS_RELEASE_PATH}/docker-compose_core.yml' | awk '{print \$1}'") NEW_SHA1=$(run_on_target "sha1sum '${NEW_RELEASE_PATH}/docker-compose_core.yml' | awk '{print \$1}'") echo "PREV_SHA1: ${PREVIOUS_SHA1}" echo " NEW_SHA1: ${NEW_SHA1}" if [ -n "$PREVIOUS_SHA1" ] && [ -n "$NEW_SHA1" ]; then if [ "$PREVIOUS_SHA1" != "$NEW_SHA1" ]; then echo "🚀 Core services have changed, restarting..." retry run_on_target "cd '${CURRENT_LINK_PATH}' && docker compose -f docker-compose_core.yml -p ${CORE_PROJECT_NAME} down" retry run_on_target "cd '${CURRENT_LINK_PATH}' && docker compose -f docker-compose_core.yml -p ${CORE_PROJECT_NAME} up -d" else echo "â„šī¸ Core services have not changed, still restarting due to current folder change..." retry run_on_target "cd '${CURRENT_LINK_PATH}' && docker compose -f docker-compose_core.yml -p ${CORE_PROJECT_NAME} down" retry run_on_target "cd '${CURRENT_LINK_PATH}' && docker compose -f docker-compose_core.yml -p ${CORE_PROJECT_NAME} up -d" fi else echo "❌ Current or previous core services not found, exiting..." exit 1 fi fi echo "🚀 Start new ${NEW_COLOR} containers with image ${IMAGE_TAG}" retry run_on_target "cd '${CURRENT_LINK_PATH}' && DEPLOYMENT_COLOR=${NEW_COLOR} IMAGE_TAG=${IMAGE_TAG} docker compose $COMPOSE_FILES -p ${PROJECT_NAME} up -d" # Wait for new containers to be healthy if ! wait_for_healthy_containers "$PROJECT_NAME" "$WEB_SERVICE" "$REPLICA_COUNT"; then echo "❌ New containers failed health checks. Cancelling deployment..." run_on_target "cd '${CURRENT_LINK_PATH}' && docker compose $COMPOSE_FILES -p ${PROJECT_NAME} down" #echo "🔄 Rolling back deployment..." #TODO: implement rollback exit 1 fi echo "✅ New ${NEW_COLOR} deployment is healthy" # Refresh gatekeepers refresh_gatekeepers # Wait for traffic to stabilize wait_with_countdown 20 "âŗ Waiting for traffic to stabilize..." # Clean up old containers if this isn't the first deployment if [ "$CURRENT_COLOR" != "none" ]; then # Get the old image tag before cleanup OLD_IMAGE_TAG=$(get_deployment_image_tag "$CURRENT_COLOR") echo "📷 Old deployment was using image: ${OLD_IMAGE_TAG}" cleanup_color_containers "$CURRENT_COLOR" echo "✅ Old containers removed" fi echo "đŸ—‘ī¸ Clean up old releases (keep last 5)" run_on_target "cd '${RELEASES_PATH}' && ls -dt */ 2>/dev/null | tail -n +6 | xargs -r rm -rf || true" echo "✅ Blue-Green deployment completed" echo " Active color: ${NEW_COLOR}" echo " Image tag: ${IMAGE_TAG}"