- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
447 lines
12 KiB
Bash
Executable File
447 lines
12 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# Production Deployment Script for Custom PHP Framework
|
|
# Comprehensive deployment automation with zero-downtime strategy
|
|
#
|
|
# Usage:
|
|
# ./scripts/production-deploy.sh [initial|update|rollback]
|
|
#
|
|
# Modes:
|
|
# initial - First-time production deployment
|
|
# update - Rolling update with zero downtime
|
|
# rollback - Rollback to previous version
|
|
|
|
set -euo pipefail
|
|
|
|
# Configuration
|
|
DEPLOY_MODE="${1:-update}"
|
|
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
BACKUP_DIR="${PROJECT_ROOT}/../backups"
|
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
|
BACKUP_PATH="${BACKUP_DIR}/backup_${TIMESTAMP}"
|
|
|
|
# Colors
|
|
GREEN="\e[32m"
|
|
YELLOW="\e[33m"
|
|
RED="\e[31m"
|
|
BLUE="\e[34m"
|
|
RESET="\e[0m"
|
|
|
|
# Logging functions
|
|
log() {
|
|
echo -e "${BLUE}[$(date +'%H:%M:%S')]${RESET} $1"
|
|
}
|
|
|
|
success() {
|
|
echo -e "${GREEN}✅ $1${RESET}"
|
|
}
|
|
|
|
warning() {
|
|
echo -e "${YELLOW}⚠️ $1${RESET}"
|
|
}
|
|
|
|
error() {
|
|
echo -e "${RED}❌ $1${RESET}"
|
|
cleanup_on_error
|
|
exit 1
|
|
}
|
|
|
|
# Cleanup on error
|
|
cleanup_on_error() {
|
|
log "Cleaning up after error..."
|
|
|
|
if [[ -d "$BACKUP_PATH" ]]; then
|
|
warning "Rolling back to previous version..."
|
|
restore_backup "$BACKUP_PATH"
|
|
fi
|
|
}
|
|
|
|
# Prerequisites check
|
|
check_prerequisites() {
|
|
log "Checking prerequisites..."
|
|
|
|
# Check if running from project root
|
|
if [[ ! -f "$PROJECT_ROOT/composer.json" ]]; then
|
|
error "Must be run from project root directory"
|
|
fi
|
|
|
|
# Check Docker
|
|
if ! command -v docker &> /dev/null; then
|
|
error "Docker is not installed"
|
|
fi
|
|
|
|
# Check Docker Compose
|
|
if ! docker compose version &> /dev/null; then
|
|
error "Docker Compose is not installed"
|
|
fi
|
|
|
|
# Check .env.production exists
|
|
if [[ ! -f "$PROJECT_ROOT/.env.production" ]]; then
|
|
error ".env.production not found - copy from .env.example and configure"
|
|
fi
|
|
|
|
# Check docker-compose.production.yml exists
|
|
if [[ ! -f "$PROJECT_ROOT/docker-compose.production.yml" ]]; then
|
|
error "docker-compose.production.yml not found"
|
|
fi
|
|
|
|
# Verify VAULT_ENCRYPTION_KEY is set
|
|
if ! grep -q "VAULT_ENCRYPTION_KEY=" "$PROJECT_ROOT/.env.production" || \
|
|
grep -q "VAULT_ENCRYPTION_KEY=CHANGE_ME" "$PROJECT_ROOT/.env.production"; then
|
|
error "VAULT_ENCRYPTION_KEY not configured in .env.production"
|
|
fi
|
|
|
|
success "Prerequisites check passed"
|
|
}
|
|
|
|
# Create backup
|
|
create_backup() {
|
|
log "Creating backup..."
|
|
|
|
mkdir -p "$BACKUP_DIR"
|
|
|
|
# Backup database
|
|
if docker compose ps db | grep -q "Up"; then
|
|
log "Backing up database..."
|
|
docker compose exec -T db pg_dump -U postgres michaelschiemer_prod | \
|
|
gzip > "${BACKUP_PATH}_database.sql.gz"
|
|
success "Database backup created"
|
|
fi
|
|
|
|
# Backup .env
|
|
if [[ -f "$PROJECT_ROOT/.env" ]]; then
|
|
cp "$PROJECT_ROOT/.env" "${BACKUP_PATH}_env"
|
|
success ".env backup created"
|
|
fi
|
|
|
|
# Backup docker volumes (important directories)
|
|
if [[ -d "$PROJECT_ROOT/storage" ]]; then
|
|
tar -czf "${BACKUP_PATH}_storage.tar.gz" -C "$PROJECT_ROOT" storage
|
|
success "Storage backup created"
|
|
fi
|
|
|
|
success "Backup completed: $BACKUP_PATH"
|
|
}
|
|
|
|
# Restore from backup
|
|
restore_backup() {
|
|
local backup_path="$1"
|
|
|
|
log "Restoring from backup: $backup_path"
|
|
|
|
# Restore database
|
|
if [[ -f "${backup_path}_database.sql.gz" ]]; then
|
|
log "Restoring database..."
|
|
gunzip -c "${backup_path}_database.sql.gz" | \
|
|
docker compose exec -T db psql -U postgres michaelschiemer_prod
|
|
success "Database restored"
|
|
fi
|
|
|
|
# Restore .env
|
|
if [[ -f "${backup_path}_env" ]]; then
|
|
cp "${backup_path}_env" "$PROJECT_ROOT/.env"
|
|
success ".env restored"
|
|
fi
|
|
|
|
# Restore storage
|
|
if [[ -f "${backup_path}_storage.tar.gz" ]]; then
|
|
tar -xzf "${backup_path}_storage.tar.gz" -C "$PROJECT_ROOT"
|
|
success "Storage restored"
|
|
fi
|
|
|
|
# Restart services
|
|
docker compose -f docker-compose.yml \
|
|
-f docker-compose.production.yml \
|
|
--env-file .env.production \
|
|
restart
|
|
|
|
success "Backup restored successfully"
|
|
}
|
|
|
|
# Build Docker images
|
|
build_images() {
|
|
log "Building Docker images..."
|
|
|
|
cd "$PROJECT_ROOT"
|
|
|
|
docker compose -f docker-compose.yml \
|
|
-f docker-compose.production.yml \
|
|
--env-file .env.production \
|
|
build --no-cache
|
|
|
|
success "Docker images built"
|
|
}
|
|
|
|
# Run database migrations
|
|
run_migrations() {
|
|
log "Running database migrations..."
|
|
|
|
cd "$PROJECT_ROOT"
|
|
|
|
# Check migration status first
|
|
docker compose exec -T php php console.php db:status || true
|
|
|
|
# Run migrations
|
|
if ! docker compose exec -T php php console.php db:migrate; then
|
|
error "Database migrations failed"
|
|
fi
|
|
|
|
success "Database migrations completed"
|
|
}
|
|
|
|
# Initialize SSL certificates
|
|
init_ssl() {
|
|
log "Initializing SSL certificates..."
|
|
|
|
cd "$PROJECT_ROOT"
|
|
|
|
# Check if SSL is enabled
|
|
if grep -q "SSL_ENABLED=true" .env.production; then
|
|
log "SSL is enabled, checking certificate status..."
|
|
|
|
# Check certificate status
|
|
if docker compose exec -T php php console.php ssl:status 2>/dev/null | grep -q "Certificate is valid"; then
|
|
success "SSL certificate already exists and is valid"
|
|
else
|
|
warning "SSL certificate not found or invalid, initializing..."
|
|
|
|
if ! docker compose exec -T php php console.php ssl:init; then
|
|
error "SSL initialization failed"
|
|
fi
|
|
|
|
success "SSL certificate initialized"
|
|
fi
|
|
else
|
|
warning "SSL is disabled in .env.production"
|
|
fi
|
|
}
|
|
|
|
# Verify Vault configuration
|
|
verify_vault() {
|
|
log "Verifying Vault configuration..."
|
|
|
|
cd "$PROJECT_ROOT"
|
|
|
|
# Test Vault access
|
|
if ! docker compose exec -T php php console.php vault:list &>/dev/null; then
|
|
error "Vault not accessible - check VAULT_ENCRYPTION_KEY"
|
|
fi
|
|
|
|
success "Vault is configured correctly"
|
|
}
|
|
|
|
# Health check with retries
|
|
health_check() {
|
|
local max_retries=30
|
|
local retry_count=0
|
|
|
|
log "Running health checks..."
|
|
|
|
while [[ $retry_count -lt $max_retries ]]; do
|
|
if curl -f -s -k -H "User-Agent: Mozilla/5.0 (Deployment Health Check)" "https://localhost/health" > /dev/null 2>&1; then
|
|
success "Health check passed"
|
|
return 0
|
|
fi
|
|
|
|
retry_count=$((retry_count + 1))
|
|
log "Health check attempt $retry_count/$max_retries..."
|
|
sleep 2
|
|
done
|
|
|
|
error "Health check failed after $max_retries attempts"
|
|
}
|
|
|
|
# Initial deployment
|
|
initial_deployment() {
|
|
log "🚀 Starting initial production deployment..."
|
|
|
|
check_prerequisites
|
|
|
|
cd "$PROJECT_ROOT"
|
|
|
|
# 1. Generate Vault encryption key if not exists
|
|
if grep -q "VAULT_ENCRYPTION_KEY=CHANGE_ME" .env.production; then
|
|
log "Generating Vault encryption key..."
|
|
warning "Make sure to backup this key securely!"
|
|
# Key generation is done manually for security
|
|
error "Please generate VAULT_ENCRYPTION_KEY with: docker exec php php console.php vault:generate-key"
|
|
fi
|
|
|
|
# 2. Build images
|
|
build_images
|
|
|
|
# 3. Start services
|
|
log "Starting Docker services..."
|
|
docker compose -f docker-compose.yml \
|
|
-f docker-compose.production.yml \
|
|
--env-file .env.production \
|
|
up -d
|
|
|
|
# 4. Wait for services to be ready
|
|
log "Waiting for services to be ready..."
|
|
sleep 20
|
|
|
|
# 5. Run migrations
|
|
run_migrations
|
|
|
|
# 6. Initialize SSL
|
|
init_ssl
|
|
|
|
# 7. Verify Vault
|
|
verify_vault
|
|
|
|
# 8. Health check
|
|
health_check
|
|
|
|
# 9. Display summary
|
|
deployment_summary
|
|
|
|
success "🎉 Initial deployment completed successfully!"
|
|
}
|
|
|
|
# Update deployment (zero-downtime)
|
|
update_deployment() {
|
|
log "🔄 Starting rolling update deployment..."
|
|
|
|
check_prerequisites
|
|
create_backup
|
|
|
|
cd "$PROJECT_ROOT"
|
|
|
|
# 1. Pull latest images (if using registry)
|
|
log "Pulling latest images..."
|
|
docker compose -f docker-compose.yml \
|
|
-f docker-compose.production.yml \
|
|
--env-file .env.production \
|
|
pull || warning "Pull failed (not critical if building locally)"
|
|
|
|
# 2. Build new images
|
|
build_images
|
|
|
|
# 3. Run migrations (if any)
|
|
log "Running database migrations..."
|
|
docker compose exec -T php php console.php db:migrate || warning "No new migrations"
|
|
|
|
# 4. Rolling restart with health checks
|
|
log "Performing rolling restart..."
|
|
|
|
# Restart PHP-FPM first
|
|
docker compose -f docker-compose.yml \
|
|
-f docker-compose.production.yml \
|
|
--env-file .env.production \
|
|
up -d --no-deps --force-recreate php
|
|
|
|
sleep 10
|
|
|
|
# Restart web server
|
|
docker compose -f docker-compose.yml \
|
|
-f docker-compose.production.yml \
|
|
--env-file .env.production \
|
|
up -d --no-deps --force-recreate web
|
|
|
|
sleep 5
|
|
|
|
# Restart queue workers (graceful shutdown via stop_grace_period)
|
|
docker compose -f docker-compose.yml \
|
|
-f docker-compose.production.yml \
|
|
--env-file .env.production \
|
|
up -d --no-deps --force-recreate --scale queue-worker=2 queue-worker
|
|
|
|
# 5. Health check
|
|
health_check
|
|
|
|
# 6. Cleanup old images
|
|
log "Cleaning up old Docker images..."
|
|
docker image prune -f
|
|
|
|
# 7. Display summary
|
|
deployment_summary
|
|
|
|
success "🎉 Update deployment completed successfully!"
|
|
}
|
|
|
|
# Rollback deployment
|
|
rollback_deployment() {
|
|
log "⏪ Starting rollback..."
|
|
|
|
# Find latest backup
|
|
local latest_backup=$(find "$BACKUP_DIR" -name "backup_*_database.sql.gz" | sort -r | head -1)
|
|
|
|
if [[ -z "$latest_backup" ]]; then
|
|
error "No backup found for rollback"
|
|
fi
|
|
|
|
local backup_prefix="${latest_backup%_database.sql.gz}"
|
|
|
|
warning "Rolling back to: $backup_prefix"
|
|
read -p "Continue? (yes/no): " confirm
|
|
|
|
if [[ "$confirm" != "yes" ]]; then
|
|
log "Rollback cancelled"
|
|
exit 0
|
|
fi
|
|
|
|
restore_backup "$backup_prefix"
|
|
health_check
|
|
|
|
success "🎉 Rollback completed successfully!"
|
|
}
|
|
|
|
# Deployment summary
|
|
deployment_summary() {
|
|
echo ""
|
|
echo -e "${GREEN}========================================${RESET}"
|
|
echo -e "${GREEN} Deployment Summary${RESET}"
|
|
echo -e "${GREEN}========================================${RESET}"
|
|
echo ""
|
|
echo "📋 Mode: $DEPLOY_MODE"
|
|
echo "⏰ Timestamp: $(date)"
|
|
echo "📁 Project: $PROJECT_ROOT"
|
|
echo "💾 Backup: $BACKUP_PATH"
|
|
echo ""
|
|
echo "🐳 Docker Services:"
|
|
docker compose ps
|
|
echo ""
|
|
echo "🔒 Security Checks:"
|
|
echo " [ ] APP_ENV=production in .env.production"
|
|
echo " [ ] APP_DEBUG=false in .env.production"
|
|
echo " [ ] VAULT_ENCRYPTION_KEY configured"
|
|
echo " [ ] ADMIN_ALLOWED_IPS configured"
|
|
echo " [ ] SSL certificates valid"
|
|
echo ""
|
|
echo "📊 Health Check:"
|
|
echo " ✅ Application: https://localhost/health"
|
|
echo ""
|
|
echo "📝 Next Steps:"
|
|
echo " 1. Verify all services are running"
|
|
echo " 2. Check logs: docker compose logs -f --tail=100"
|
|
echo " 3. Test critical user flows"
|
|
echo " 4. Monitor error rates"
|
|
echo ""
|
|
echo -e "${GREEN}========================================${RESET}"
|
|
}
|
|
|
|
# Main deployment logic
|
|
main() {
|
|
case "$DEPLOY_MODE" in
|
|
initial)
|
|
initial_deployment
|
|
;;
|
|
update)
|
|
update_deployment
|
|
;;
|
|
rollback)
|
|
rollback_deployment
|
|
;;
|
|
*)
|
|
error "Invalid deployment mode: $DEPLOY_MODE. Use: initial|update|rollback"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Trap errors
|
|
trap cleanup_on_error ERR
|
|
|
|
# Run main
|
|
main "$@"
|