Files
portfolio/scripts/safe-deploy.sh
denshooter 976a6360fd feat: Website-Rework mit verbessertem Design, Sicherheit und Deployment
- Neue About/Skills-Sektion hinzugefügt
- Verbesserte UI/UX für alle Komponenten
- Enhanced Contact Form mit Validierung
- Verbesserte Security Headers und Middleware
- Sichere Deployment-Skripte (safe-deploy.sh)
- Zero-Downtime Deployment Support
- Verbesserte Docker-Sicherheit
- Umfassende Sicherheits-Dokumentation
- Performance-Optimierungen
- Accessibility-Verbesserungen
2025-11-22 19:24:49 +01:00

357 lines
9.5 KiB
Bash
Executable File

#!/bin/bash
# Safe Deployment Script for dk0.dev
# Ensures secure, zero-downtime deployments with proper error handling and rollback
set -euo pipefail # Exit on error, undefined vars, pipe failures
# Configuration
IMAGE_NAME="portfolio-app"
NEW_TAG="latest"
OLD_TAG="previous"
BACKUP_TAG="backup-$(date +%Y%m%d-%H%M%S)"
COMPOSE_FILE="docker-compose.production.yml"
HEALTH_CHECK_URL="http://localhost:3000/api/health"
HEALTH_CHECK_TIMEOUT=180
HEALTH_CHECK_INTERVAL=5
MAX_ROLLBACK_ATTEMPTS=3
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 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"
}
# Cleanup function
cleanup() {
local exit_code=$?
if [ $exit_code -ne 0 ]; then
error "Deployment failed with exit code $exit_code"
rollback
fi
}
# Health check function
check_health() {
local url=$1
local max_attempts=$((HEALTH_CHECK_TIMEOUT / HEALTH_CHECK_INTERVAL))
local attempt=0
log "Performing health check on $url..."
while [ $attempt -lt $max_attempts ]; do
if curl -f -s -m 5 "$url" > /dev/null 2>&1; then
return 0
fi
attempt=$((attempt + 1))
sleep $HEALTH_CHECK_INTERVAL
echo -n "."
done
echo ""
return 1
}
# Rollback function
rollback() {
error "Deployment failed. Initiating rollback..."
local rollback_attempt=0
while [ $rollback_attempt -lt $MAX_ROLLBACK_ATTEMPTS ]; do
rollback_attempt=$((rollback_attempt + 1))
log "Rollback attempt $rollback_attempt/$MAX_ROLLBACK_ATTEMPTS"
# Try to restore from backup tag first
if docker images | grep -q "${IMAGE_NAME}:${OLD_TAG}"; then
log "Restoring from previous image..."
docker tag "${IMAGE_NAME}:${OLD_TAG}" "${IMAGE_NAME}:${NEW_TAG}" || {
warning "Failed to tag previous image"
continue
}
elif docker images | grep -q "${IMAGE_NAME}:${BACKUP_TAG}"; then
log "Restoring from backup image..."
docker tag "${IMAGE_NAME}:${BACKUP_TAG}" "${IMAGE_NAME}:${NEW_TAG}" || {
warning "Failed to tag backup image"
continue
}
else
error "No backup image found for rollback"
return 1
fi
# Restart container with previous image
log "Restarting container with previous image..."
docker-compose -f "$COMPOSE_FILE" up -d --force-recreate portfolio || {
warning "Failed to restart container"
continue
}
# Wait for health check
sleep 10
if check_health "$HEALTH_CHECK_URL"; then
success "Rollback successful!"
return 0
else
warning "Rollback attempt $rollback_attempt failed health check"
fi
done
error "All rollback attempts failed"
return 1
}
# Pre-deployment checks
pre_deployment_checks() {
log "Running pre-deployment checks..."
# 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 && ! docker compose version &> /dev/null; then
error "docker-compose is not installed"
exit 1
fi
# Check if .env file exists
if [ ! -f .env ]; then
error ".env file not found. Please create it before deploying."
exit 1
fi
# Check if required environment variables are set
local required_vars=("DATABASE_URL" "NEXT_PUBLIC_BASE_URL")
for var in "${required_vars[@]}"; do
if ! grep -q "^${var}=" .env 2>/dev/null; then
warning "Required environment variable $var not found in .env"
fi
done
# Check disk space (at least 2GB free)
local available_space=$(df -BG . | tail -1 | awk '{print $4}' | sed 's/G//')
if [ "$available_space" -lt 2 ]; then
error "Insufficient disk space. At least 2GB required, but only ${available_space}GB available."
exit 1
fi
success "Pre-deployment checks passed"
}
# Build application
build_application() {
log "Building application..."
# Build Next.js application
log "Building Next.js application..."
npm ci --prefer-offline --no-audit || {
error "npm install failed"
exit 1
}
npm run build || {
error "Build failed"
exit 1
}
success "Application built successfully"
}
# Build Docker image
build_docker_image() {
log "Building Docker image..."
# Backup current image if it exists
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)"
}
docker tag "${IMAGE_NAME}:${NEW_TAG}" "${IMAGE_NAME}:${BACKUP_TAG}" || true
fi
# Build new image
docker build -t "${IMAGE_NAME}:${NEW_TAG}" . || {
error "Failed to build Docker image"
exit 1
}
# Verify new image
if ! docker images | grep -q "${IMAGE_NAME}:${NEW_TAG}"; then
error "New image not found after build"
exit 1
fi
success "Docker image built successfully"
}
# Deploy application
deploy_application() {
log "Deploying application..."
# Start new container
if command -v docker-compose &> /dev/null; then
docker-compose -f "$COMPOSE_FILE" up -d --force-recreate portfolio || {
error "Failed to start new container"
exit 1
}
else
docker compose -f "$COMPOSE_FILE" up -d --force-recreate portfolio || {
error "Failed to start new container"
exit 1
}
fi
# Wait for container to start
log "Waiting for container to start..."
sleep 15
# Check if container is running
if ! docker ps | grep -q "portfolio-app"; then
error "Container failed to start"
log "Container logs:"
docker logs portfolio-app --tail=50
exit 1
fi
success "Container started successfully"
}
# Run database migrations
run_migrations() {
log "Running database migrations..."
# Wait for database to be ready
local db_ready=false
for i in {1..30}; do
if docker exec portfolio-app npx prisma db push --skip-generate --accept-data-loss 2>&1 | grep -q "Database.*ready\|Everything is in sync"; then
db_ready=true
break
fi
sleep 2
done
if [ "$db_ready" = false ]; then
warning "Database might not be ready, but continuing..."
fi
# Run migrations
docker exec portfolio-app npx prisma db push --skip-generate || {
warning "Database migration had issues, but continuing..."
}
success "Database migrations completed"
}
# Verify deployment
verify_deployment() {
log "Verifying deployment..."
# Health check
if ! check_health "$HEALTH_CHECK_URL"; then
error "Health check failed"
log "Container logs:"
docker logs portfolio-app --tail=100
return 1
fi
# Test main page
if ! curl -f -s -m 10 "http://localhost:3000/" > /dev/null 2>&1; then
error "Main page is not accessible"
return 1
fi
# Test API endpoint
if ! curl -f -s -m 10 "http://localhost:3000/api/health" > /dev/null 2>&1; then
error "API health endpoint is not accessible"
return 1
fi
success "Deployment verification successful"
}
# Cleanup old images
cleanup_old_images() {
log "Cleaning up old images..."
# Keep last 5 versions
docker images "${IMAGE_NAME}" --format "{{.Tag}}" | \
grep -E "^(backup-|previous|failed-)" | \
sort -r | \
tail -n +6 | \
while read -r tag; do
docker rmi "${IMAGE_NAME}:${tag}" 2>/dev/null || true
done
success "Cleanup completed"
}
# Main deployment flow
main() {
log "Starting safe deployment for dk0.dev..."
# Set up error handling
trap cleanup ERR
# Run deployment steps
pre_deployment_checks
build_application
build_docker_image
deploy_application
run_migrations
# Verify deployment
if verify_deployment; then
cleanup_old_images
# Show deployment info
log "Deployment completed successfully!"
log "Container status:"
if command -v docker-compose &> /dev/null; then
docker-compose -f "$COMPOSE_FILE" ps
else
docker compose -f "$COMPOSE_FILE" ps
fi
log "Resource usage:"
docker stats --no-stream --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}" portfolio-app
success "Application is available at: http://localhost:3000/"
success "Health check endpoint: $HEALTH_CHECK_URL"
# Disable error trap (deployment successful)
trap - ERR
else
error "Deployment verification failed"
exit 1
fi
}
# Run main function
main "$@"