#!/bin/bash # Zero-Downtime Deployment Script for dk0.dev # This script ensures safe, zero-downtime deployments with rollback capability set -e # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Configuration IMAGE_NAME="portfolio-app" NEW_TAG="latest" OLD_TAG="previous" COMPOSE_FILE="docker-compose.production.yml" HEALTH_CHECK_URL="http://localhost:3000/api/health" HEALTH_CHECK_TIMEOUT=120 HEALTH_CHECK_INTERVAL=5 # Logging functions log() { echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" } error() { echo -e "${RED}[ERROR]${NC} $1" >&2 } success() { echo -e "${GREEN}[SUCCESS]${NC} $1" } warning() { echo -e "${YELLOW}[WARNING]${NC} $1" } # Check if running as root if [[ $EUID -eq 0 ]]; then error "This script should not be run as root" exit 1 fi # Check if Docker is running if ! docker info > /dev/null 2>&1; then error "Docker is not running. Please start Docker and try again." exit 1 fi # Check if docker-compose is available if ! command -v docker-compose &> /dev/null; then error "docker-compose is not installed" exit 1 fi # Health check function check_health() { local url=$1 local max_attempts=$((HEALTH_CHECK_TIMEOUT / HEALTH_CHECK_INTERVAL)) local attempt=0 while [ $attempt -lt $max_attempts ]; do if curl -f -s "$url" > /dev/null 2>&1; then return 0 fi attempt=$((attempt + 1)) sleep $HEALTH_CHECK_INTERVAL echo -n "." done return 1 } # Rollback function rollback() { error "Deployment failed. Rolling back..." # Tag current image as failed if docker images | grep -q "${IMAGE_NAME}:${NEW_TAG}"; then docker tag "${IMAGE_NAME}:${NEW_TAG}" "${IMAGE_NAME}:failed-$(date +%s)" || true fi # Restore previous image if docker images | grep -q "${IMAGE_NAME}:${OLD_TAG}"; then log "Restoring previous image..." docker tag "${IMAGE_NAME}:${OLD_TAG}" "${IMAGE_NAME}:${NEW_TAG}" docker-compose -f "$COMPOSE_FILE" up -d --force-recreate portfolio || { error "Failed to rollback" exit 1 } if check_health "$HEALTH_CHECK_URL"; then success "Rollback successful" else error "Rollback completed but health check failed" exit 1 fi else error "No previous image found for rollback" exit 1 fi } # Trap errors for automatic rollback trap rollback ERR log "Starting zero-downtime deployment..." # Step 1: Backup current image if docker images | grep -q "${IMAGE_NAME}:${NEW_TAG}"; then log "Backing up current image..." docker tag "${IMAGE_NAME}:${NEW_TAG}" "${IMAGE_NAME}:${OLD_TAG}" || { warning "Could not backup current image (this might be the first deployment)" } fi # Step 2: Build new image log "Building new image..." docker build -t "${IMAGE_NAME}:${NEW_TAG}" . || { error "Failed to build image" exit 1 } # Step 3: Verify new image log "Verifying new image..." if ! docker images | grep -q "${IMAGE_NAME}:${NEW_TAG}"; then error "New image not found after build" exit 1 fi # Step 4: Start new container in background (blue-green deployment) log "Starting new container..." docker-compose -f "$COMPOSE_FILE" up -d --force-recreate portfolio || { error "Failed to start new container" exit 1 } # Step 5: Wait for health check log "Waiting for health check..." if check_health "$HEALTH_CHECK_URL"; then success "New container is healthy!" else error "Health check failed for new container" exit 1 fi # Step 6: Run database migrations (if needed) log "Running database migrations..." docker exec portfolio-app npx prisma db push --skip-generate || { warning "Database migration failed, but continuing..." } # Step 7: Final verification log "Performing final verification..." if check_health "$HEALTH_CHECK_URL"; then success "Deployment successful!" # Show container status log "Container status:" docker-compose -f "$COMPOSE_FILE" ps # Cleanup old images (keep last 3 versions) log "Cleaning up old images..." docker images "${IMAGE_NAME}" --format "{{.Tag}}" | grep -E "^(failed-|previous)" | sort -r | tail -n +4 | while read tag; do docker rmi "${IMAGE_NAME}:${tag}" 2>/dev/null || true done success "Zero-downtime deployment completed successfully!" log "Application is available at: http://localhost:3000/" log "Health check endpoint: $HEALTH_CHECK_URL" else error "Final verification failed" exit 1 fi # Disable error trap (deployment successful) trap - ERR success "All done!"