diff --git a/.env.production b/.env.production index b9a601bf..535bc3a7 100644 --- a/.env.production +++ b/.env.production @@ -24,6 +24,55 @@ SECURITY_RATE_LIMIT_PER_MINUTE=30 SECURITY_RATE_LIMIT_BURST=5 SESSION_LIFETIME=1800 +# Docker Production Configuration +RESTART_POLICY=always +VOLUME_MODE=ro +LOG_DRIVER=json-file +LOG_MAX_SIZE=10m +LOG_MAX_FILE=3 +LOG_LABELS=environment=production + +# PHP Production Settings +PHP_USER=www-data:www-data +PHP_IDE_CONFIG="" +XDEBUG_MODE=off +COMPOSER_INSTALL_FLAGS=--no-dev --optimize-autoloader --classmap-authoritative + +# Resource Limits (Production) +WEB_MEMORY_LIMIT=256M +WEB_CPU_LIMIT=0.5 +WEB_MEMORY_RESERVATION=128M +WEB_CPU_RESERVATION=0.25 + +PHP_MEMORY_LIMIT=512M +PHP_CPU_LIMIT=1.0 +PHP_MEMORY_RESERVATION=256M +PHP_CPU_RESERVATION=0.5 + +DB_MEMORY_LIMIT=1G +DB_CPU_LIMIT=1.0 +DB_MEMORY_RESERVATION=512M +DB_CPU_RESERVATION=0.5 + +REDIS_MEMORY_LIMIT=256M +REDIS_CPU_LIMIT=0.5 +REDIS_MEMORY_RESERVATION=128M +REDIS_CPU_RESERVATION=0.25 + +# Network Security (Production) +NETWORK_BACKEND_INTERNAL=true +NETWORK_CACHE_INTERNAL=true + +# Production-specific configs +DB_PORT= +REDIS_CONFIG_PATH=./docker/redis/redis-secure.conf +DB_CONFIG_PATH=./docker/mysql/conf.d/security.cnf +HEALTHCHECK_START_PERIOD=30s + +# Production ports (only HTTPS) +APP_PORT= +APP_SSL_PORT=443 + # External APIs (Production) SHOPIFY_WEBHOOK_SECRET=SECURE_WEBHOOK_SECRET_HERE RAPIDMAIL_USERNAME=production_username diff --git a/deploy-direct.sh b/deploy-direct.sh new file mode 100755 index 00000000..2cc2680b --- /dev/null +++ b/deploy-direct.sh @@ -0,0 +1,176 @@ +#!/bin/bash + +# Direct Docker-based production deployment +# Bypasses Ansible for immediate deployment + +set -euo pipefail + +SERVER_USER="deploy" +SERVER_IP="94.16.110.151" +REMOTE_PATH="/home/deploy/michaelschiemer" +SSH_OPTS="-o StrictHostKeyChecking=no" + +# Colors for output +GREEN="\e[32m" +YELLOW="\e[33m" +RED="\e[31m" +RESET="\e[0m" + +log_info() { + echo -e "${YELLOW}[INFO]${RESET} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${RESET} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${RESET} $1" +} + +# Check prerequisites +check_prerequisites() { + log_info "Checking prerequisites..." + + if ! ssh $SSH_OPTS "$SERVER_USER@$SERVER_IP" "echo 'SSH connection successful'" 2>/dev/null; then + log_error "Cannot connect to production server. Please configure SSH key authentication." + log_error "Run: ssh-copy-id -i ~/.ssh/production $SERVER_USER@$SERVER_IP" + exit 1 + fi + + if ! command -v docker &> /dev/null; then + log_error "Docker not found locally. Install Docker first." + exit 1 + fi + + log_success "Prerequisites check passed" +} + +# Build and push production image +build_and_push() { + local image_tag="$(git rev-parse --short HEAD)" + log_info "Building production image with tag: $image_tag" + + # Build production image + docker build -t "michaelschiemer/php-framework:$image_tag" \ + -f docker/php/Dockerfile \ + --target=production \ + --build-arg ENV=production \ + . + + # Tag as latest for production + docker tag "michaelschiemer/php-framework:$image_tag" \ + "michaelschiemer/php-framework:production" + + log_success "Production image built successfully" + echo "$image_tag" +} + +# Deploy to production server +deploy_to_server() { + local image_tag="$1" + + log_info "Deploying to production server..." + + # Create deployment directory + ssh $SSH_OPTS "$SERVER_USER@$SERVER_IP" "mkdir -p $REMOTE_PATH" + + # Copy docker-compose files + scp $SSH_OPTS docker-compose.yml "$SERVER_USER@$SERVER_IP:$REMOTE_PATH/" + scp $SSH_OPTS deployment/applications/docker-compose.production.yml "$SERVER_USER@$SERVER_IP:$REMOTE_PATH/" + + # Copy environment file template + scp $SSH_OPTS .env.production "$SERVER_USER@$SERVER_IP:$REMOTE_PATH/.env" 2>/dev/null || { + log_info "Creating production environment file..." + ssh $SSH_OPTS "$SERVER_USER@$SERVER_IP" "cat > $REMOTE_PATH/.env << 'EOF' +APP_ENV=production +APP_DEBUG=false +DB_HOST=db +DB_DATABASE=framework +DB_USERNAME=framework +DB_PASSWORD=secure_password_change_me +REDIS_HOST=redis +REDIS_PORT=6379 +EOF" + } + + # Save Docker image and transfer + log_info "Transferring Docker image..." + docker save "michaelschiemer/php-framework:$image_tag" | \ + ssh $SSH_OPTS "$SERVER_USER@$SERVER_IP" "docker load" + + # Deploy with Docker Compose + ssh $SSH_OPTS "$SERVER_USER@$SERVER_IP" " + cd $REMOTE_PATH + + # Stop existing services + docker compose -f docker-compose.yml -f docker-compose.production.yml down --remove-orphans || true + + # Start services with production configuration + IMAGE_TAG=$image_tag docker compose -f docker-compose.yml -f docker-compose.production.yml up -d + + # Wait for services to be healthy + sleep 30 + + # Run health check + if docker compose -f docker-compose.yml -f docker-compose.production.yml ps | grep -q 'healthy\\|Up'; then + echo 'Deployment successful!' + else + echo 'Health check failed!' + docker compose -f docker-compose.yml -f docker-compose.production.yml logs --tail=50 + exit 1 + fi + " + + log_success "Deployment completed successfully!" +} + +# Validate deployment +validate_deployment() { + log_info "Validating production deployment..." + + # Test HTTPS endpoint + if curl -f -k -H "User-Agent: Mozilla/5.0" "https://$SERVER_IP/health" >/dev/null 2>&1; then + log_success "HTTPS health check passed" + else + log_error "HTTPS health check failed" + return 1 + fi + + # Check that debug routes are blocked + local debug_response=$(curl -s -o /dev/null -w '%{http_code}' -k -H "User-Agent: Mozilla/5.0" "https://$SERVER_IP/debug" 2>/dev/null || echo 'connection_failed') + if [ "$debug_response" = '404' ]; then + log_success "Debug routes properly blocked" + else + log_error "WARNING: Debug routes not properly blocked (got: $debug_response)" + fi + + log_success "Deployment validation completed" +} + +# Main execution +main() { + log_info "Starting direct production deployment..." + + # Check prerequisites + check_prerequisites + + # Build and get image tag + local image_tag + image_tag=$(build_and_push) + + # Deploy to server + deploy_to_server "$image_tag" + + # Validate deployment + validate_deployment + + log_success "Production deployment completed successfully!" + log_info "Application available at: https://michaelschiemer.de" + log_info "Deployed commit: $(git rev-parse HEAD)" +} + +# Execute if run directly +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi \ No newline at end of file diff --git a/deployment/applications/docker-compose.production.yml b/deployment/applications/docker-compose.production.yml index f6ed3d1c..10222c67 100644 --- a/deployment/applications/docker-compose.production.yml +++ b/deployment/applications/docker-compose.production.yml @@ -182,21 +182,21 @@ networks: ipam: driver: default config: - - subnet: 172.20.0.0/24 + - subnet: 172.24.0.0/24 backend: driver: bridge internal: true # Backend network is internal-only ipam: driver: default config: - - subnet: 172.21.0.0/24 + - subnet: 172.25.0.0/24 cache: driver: bridge internal: true # Cache network is internal-only ipam: driver: default config: - - subnet: 172.22.0.0/24 + - subnet: 172.26.0.0/24 volumes: redis_data: diff --git a/docker-compose.yml b/docker-compose.yml index de2c1142..9f6e7041 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,20 +16,34 @@ services: interval: 30s timeout: 10s retries: 3 - start_period: 10s + start_period: ${HEALTHCHECK_START_PERIOD:-10s} + logging: + driver: "${LOG_DRIVER:-local}" + options: + max-size: "${LOG_MAX_SIZE:-5m}" + max-file: "${LOG_MAX_FILE:-2}" + labels: "${LOG_LABELS:-}" volumes: - - ./:/var/www/html:cached + - ./:/var/www/html:${VOLUME_MODE:-cached} #- ./ssl:/etc/nginx/ssl:ro # SSL-Zertifikate mounten - ./ssl:/var/www/ssl:ro depends_on: php: condition: service_started - restart: unless-stopped + restart: ${RESTART_POLICY:-unless-stopped} networks: - frontend - backend env_file: - .env + deploy: + resources: + limits: + memory: ${WEB_MEMORY_LIMIT:-256M} + cpus: ${WEB_CPU_LIMIT:-0.5} + reservations: + memory: ${WEB_MEMORY_RESERVATION:-128M} + cpus: ${WEB_CPU_RESERVATION:-0.25} php: container_name: php @@ -39,67 +53,92 @@ services: args: - ENV=${APP_ENV:-dev} - COMPOSER_INSTALL_FLAGS=${COMPOSER_INSTALL_FLAGS:---no-scripts --no-autoloader} - user: "1000:1000" + user: "${PHP_USER:-1000:1000}" logging: - driver: "local" + driver: "${LOG_DRIVER:-local}" options: - max-size: "5m" - max-file: "2" + max-size: "${LOG_MAX_SIZE:-5m}" + max-file: "${LOG_MAX_FILE:-2}" + labels: "${LOG_LABELS:-}" volumes: # Shared Volume für Composer-Cache über Container-Neustarts hinweg - composer-cache:/root/.composer/cache # Bindet das Projektverzeichnis für Produktivbetrieb ein #- project-data:/var/www/html:cached # Variante mit mounting: - - ./:/var/www/html:cached + - ./:/var/www/html:${VOLUME_MODE:-cached} # Verhindert Überschreiben der Vendor-Verzeichnisse #- /var/www/html/vendor # Storage-Verzeichnisse als Docker-Volumes (keine Host-Mounts) - storage-data:/var/www/html/storage:rw - var-data:/var/www/html/var:rw environment: - PHP_IDE_CONFIG: "serverName=docker" + PHP_IDE_CONFIG: "${PHP_IDE_CONFIG:-serverName=docker}" APP_ENV: ${APP_ENV:-development} APP_DEBUG: ${APP_DEBUG:-true} + XDEBUG_MODE: ${XDEBUG_MODE:-debug} healthcheck: test: [ "CMD", "php", "-v" ] interval: 30s timeout: 10s retries: 3 - restart: unless-stopped + restart: ${RESTART_POLICY:-unless-stopped} networks: - backend - cache env_file: - .env + deploy: + resources: + limits: + memory: ${PHP_MEMORY_LIMIT:-512M} + cpus: ${PHP_CPU_LIMIT:-1.0} + reservations: + memory: ${PHP_MEMORY_RESERVATION:-256M} + cpus: ${PHP_CPU_RESERVATION:-0.5} db: container_name: db image: mariadb:latest - restart: unless-stopped + restart: ${RESTART_POLICY:-unless-stopped} environment: MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-StartRoot2024!} MYSQL_DATABASE: ${DB_DATABASE:-michaelschiemer} MYSQL_USER: ${DB_USERNAME:-mdb-user} MYSQL_PASSWORD: ${DB_PASSWORD:-StartSimple2024!} ports: - - "33060:3306" + - "${DB_PORT:-33060}:3306" volumes: - db_data:/var/lib/mysql + - "${DB_CONFIG_PATH:-./docker/mysql/conf.d}:/etc/mysql/conf.d:ro" healthcheck: test: [ "CMD", "mariadb-admin", "ping", "-h", "127.0.0.1", "-u", "root", "-p${DB_ROOT_PASSWORD:-StartRoot2024!}" ] interval: 10s timeout: 5s retries: 5 - start_period: 30s + start_period: 60s + logging: + driver: "${LOG_DRIVER:-local}" + options: + max-size: "${LOG_MAX_SIZE:-5m}" + max-file: "${LOG_MAX_FILE:-2}" + labels: "${LOG_LABELS:-}" networks: - backend + deploy: + resources: + limits: + memory: ${DB_MEMORY_LIMIT:-1G} + cpus: ${DB_CPU_LIMIT:-1.0} + reservations: + memory: ${DB_MEMORY_RESERVATION:-512M} + cpus: ${DB_CPU_RESERVATION:-0.5} redis: container_name: redis image: redis:7-alpine volumes: - - ./docker/redis/redis.conf:/usr/local/etc/redis/redis.conf + - "${REDIS_CONFIG_PATH:-./docker/redis/redis.conf}:/usr/local/etc/redis/redis.conf:ro" - redis_data:/data command: ["redis-server", "/usr/local/etc/redis/redis.conf"] healthcheck: @@ -107,11 +146,26 @@ services: interval: 30s timeout: 5s retries: 3 - restart: unless-stopped + start_period: 30s + restart: ${RESTART_POLICY:-unless-stopped} + logging: + driver: "${LOG_DRIVER:-local}" + options: + max-size: "${LOG_MAX_SIZE:-5m}" + max-file: "${LOG_MAX_FILE:-2}" + labels: "${LOG_LABELS:-}" networks: - cache env_file: - .env + deploy: + resources: + limits: + memory: ${REDIS_MEMORY_LIMIT:-256M} + cpus: ${REDIS_CPU_LIMIT:-0.5} + reservations: + memory: ${REDIS_MEMORY_RESERVATION:-128M} + cpus: ${REDIS_CPU_RESERVATION:-0.25} queue-worker: container_name: queue-worker @@ -169,8 +223,10 @@ networks: driver: bridge backend: driver: bridge + internal: ${NETWORK_BACKEND_INTERNAL:-false} cache: driver: bridge + internal: ${NETWORK_CACHE_INTERNAL:-false} volumes: redis_data: