- 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
357 lines
9.5 KiB
Bash
Executable File
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 "$@"
|
|
|