# SSH Deployment Guide Comprehensive guide for deploying the Custom PHP Framework using SSH-based deployment scripts. ## Overview This deployment system uses simple SSH/SCP-based scripts to deploy the framework to staging and production environments. It replaces Gitea Actions with a straightforward bash script approach. **Key Features**: - ✅ Simple SSH/SCP deployment (no CI/CD platform dependency) - ✅ Automatic Docker image building and registry pushing - ✅ Database backups before production deployments - ✅ Automatic rollback on deployment failure - ✅ Health checks and smoke tests - ✅ Timestamped backup retention - ✅ Color-coded output for easy monitoring ## Prerequisites ### Required Software **Local Machine**: - Docker (for building images) - Docker Compose (for compose file validation) - SSH client (openssh-client) - SCP client (usually bundled with SSH) - Bash shell **Remote Servers** (staging/production): - Docker and Docker Compose installed - SSH server running - Docker private registry accessible (localhost:5000 or custom) - Deployment user with Docker permissions - Directory structure: `/opt/framework-staging/` or `/opt/framework-production/` ### SSH Key Setup Generate SSH keys for deployment (if not already done): ```bash # Generate deployment SSH key ssh-keygen -t rsa -b 4096 -f ~/.ssh/framework-deploy \ -C "framework-deployment" -N "" # Copy public key to staging server ssh-copy-id -i ~/.ssh/framework-deploy.pub deploy@staging.michaelschiemer.de # Copy public key to production server ssh-copy-id -i ~/.ssh/framework-deploy.pub deploy@michaelschiemer.de # Test connection ssh -i ~/.ssh/framework-deploy deploy@staging.michaelschiemer.de "echo 'SSH connection successful'" ``` **SSH Config** (~/.ssh/config): ``` # Staging Server Host staging.michaelschiemer.de User deploy IdentityFile ~/.ssh/framework-deploy Port 22 # Production Server Host michaelschiemer.de User deploy IdentityFile ~/.ssh/framework-deploy Port 22 ``` ### Environment Variables **Staging Deployment**: ```bash export STAGING_HOST=staging.michaelschiemer.de export STAGING_USER=deploy export STAGING_SSH_PORT=22 ``` **Production Deployment**: ```bash export PRODUCTION_HOST=michaelschiemer.de export PRODUCTION_USER=deploy export PRODUCTION_SSH_PORT=22 ``` **Optional Configuration**: ```bash # Docker Registry (default: localhost:5000) export REGISTRY=your-registry.com # Image Configuration export IMAGE_NAME=framework export IMAGE_TAG=latest # or staging # Production Options export SKIP_BACKUP=false # Skip database backup (not recommended) export FORCE_REBUILD=false # Force Docker rebuild ``` **Persistent Configuration** (.bashrc or .zshrc): ```bash # Add to ~/.bashrc or ~/.zshrc export STAGING_HOST=staging.michaelschiemer.de export STAGING_USER=deploy export PRODUCTION_HOST=michaelschiemer.de export PRODUCTION_USER=deploy ``` ## Deployment Scripts ### 1. Staging Deployment **Script**: `deployment/scripts/deploy-staging.sh` **Purpose**: Deploy to staging environment for testing **Usage**: ```bash # Basic deployment ./deployment/scripts/deploy-staging.sh # With custom configuration STAGING_HOST=custom.staging.com ./deployment/scripts/deploy-staging.sh ``` **What It Does**: 1. Builds Docker image with `ENV=staging` 2. Pushes image to private registry 3. Creates timestamped backup of current deployment 4. Copies deployment files via SCP 5. Stops existing containers 6. Starts new containers 7. Waits 30 seconds for services to initialize 8. Performs health checks 9. Automatic rollback on failure **Backup Retention**: Keeps last 5 backups, deletes older **Deployment Path**: `/opt/framework-staging/current/` **Expected Output**: ``` ================================================== 🚀 Starting Staging Deployment ================================================== Registry: localhost:5000 Image: framework:staging Remote: deploy@staging.michaelschiemer.de:22 Path: /opt/framework-staging [1/7] Building Docker image... [2/7] Pushing image to registry... [3/7] Preparing deployment files... [4/7] Creating remote directory and backup... Backing up current deployment... Backup created: backup_20250124_153022 [5/7] Copying deployment files to server... [6/7] Executing deployment on server... ================================================== Starting Staging Deployment on Server ================================================== [1/5] Pulling latest Docker images... [2/5] Stopping existing containers... [3/5] Starting new containers... [4/5] Waiting for services to be healthy... [5/5] Verifying deployment... ================================================== ✅ Staging Deployment Complete ================================================== [7/7] Performing health checks... Waiting 30 seconds for services to initialize... Checking container status... ✅ Health check complete! ================================================== ✅ Staging Deployment Successful ================================================== URL: https://staging.michaelschiemer.de Deployed at: Thu Jan 24 15:30:45 CET 2025 ``` ### 2. Production Deployment **Script**: `deployment/scripts/deploy-production.sh` **Purpose**: Deploy to production environment **⚠️ WARNING**: Production deployments include: - Automatic database backup (mandatory unless skipped) - 60-second service initialization wait - Smoke tests for main page and API health - Automatic rollback on any failure **Usage**: ```bash # Standard production deployment ./deployment/scripts/deploy-production.sh # Skip database backup (NOT RECOMMENDED) SKIP_BACKUP=true ./deployment/scripts/deploy-production.sh # Force Docker rebuild FORCE_REBUILD=true ./deployment/scripts/deploy-production.sh ``` **What It Does**: 1. Builds Docker image with `ENV=production` 2. Pushes image to private registry 3. **Creates database backup** (aborts if backup fails) 4. Creates timestamped backup of current deployment 5. Copies deployment files via SCP 6. Stops existing containers gracefully 7. Starts new containers 8. Waits 60 seconds for services to initialize 9. Runs database migrations with `--force` 10. Performs comprehensive health checks: - Container status - PHP-FPM process check - Redis connection test 11. **Runs smoke tests**: - Main page accessibility (https://michaelschiemer.de/) - API health endpoint (https://michaelschiemer.de/api/health) 12. Automatic rollback on any failure **Backup Retention**: Keeps last 10 backups, deletes older **Deployment Path**: `/opt/framework-production/current/` **Database Backup Location**: `/var/www/html/storage/backups/backup_YYYYMMDD_HHMMSS.sql` **Expected Output**: ``` ================================================== 🚀 Starting Production Deployment ================================================== Registry: localhost:5000 Image: framework:latest Remote: deploy@michaelschiemer.de:22 Path: /opt/framework-production Skip Backup: false [1/8] Building Docker image... [2/8] Pushing image to registry... [3/8] Preparing deployment files... [4/8] Creating remote directory and backup... [5/8] Copying deployment files to server... [6/8] Executing deployment on server... ================================================== Starting Production Deployment on Server ================================================== [0/6] Creating database backup... ✅ Database backup created: backup_20250124_153045.sql [1/6] Pulling latest Docker images... [2/6] Stopping existing containers (graceful shutdown)... [3/6] Starting new containers... [4/6] Waiting for services to be healthy... [5/6] Running database migrations... [6/6] Verifying deployment... ================================================== ✅ Production Deployment Complete ================================================== [7/8] Performing health checks... Waiting 60 seconds for services to initialize... Checking container status... ✅ All health checks passed! [8/8] Running smoke tests... ✅ Main page accessible ✅ API health check passed ✅ Smoke tests completed successfully ================================================== ✅ Production Deployment Successful ================================================== URL: https://michaelschiemer.de Deployed at: Thu Jan 24 15:32:15 CET 2025 ``` ### 3. Rollback Script **Script**: `deployment/scripts/rollback.sh` **Purpose**: Restore previous deployment from backup **Usage**: ```bash # Rollback staging to latest backup ./deployment/scripts/rollback.sh staging # Rollback production to latest backup ./deployment/scripts/rollback.sh production # Rollback to specific backup ./deployment/scripts/rollback.sh production backup_20250124_143022 ``` **What It Does**: 1. Lists available backups 2. Confirms rollback operation (requires "yes") 3. Stops current deployment 4. Archives failed deployment as `failed_YYYYMMDD_HHMMSS` 5. Restores specified backup 6. Starts restored deployment 7. Performs health checks **Arguments**: - `environment`: `staging` or `production` (required) - `backup_name`: Specific backup to restore (optional, defaults to latest) **Example Session**: ```bash $ ./deployment/scripts/rollback.sh production ================================================== 🔄 Starting Rollback: production ================================================== Remote: deploy@michaelschiemer.de:22 Path: /opt/framework-production Target Backup: Latest available ⚠️ WARNING: This will rollback the production deployment Current deployment will be stopped and replaced with backup Are you sure you want to continue? (yes/no): yes [1/5] Listing available backups... Available backups: backup_20250124_153045 backup_20250124_120000 backup_20250123_183015 [2/5] Determining backup to restore... Using latest backup: backup_20250124_153045 ✅ Backup backup_20250124_153045 verified [3/5] Stopping current deployment... ✅ Current deployment stopped [4/5] Restoring backup... Archiving failed deployment as failed_20250124_154512... Restoring backup backup_20250124_153045... ✅ Backup restored [5/5] Starting restored deployment... Starting containers... Waiting for services to start... ✅ Restored deployment is running ================================================== ✅ Rollback Complete ================================================== Environment: production Restored: backup_20250124_153045 Completed at: Thu Jan 24 15:45:30 CET 2025 Failed deployment archived as: failed_20250124_154512 ``` ## Deployment Workflows ### Staging Deployment Workflow **Step-by-Step Process**: 1. **Prepare Changes**: ```bash # Make code changes locally git add . git commit -m "feat: new feature" git push origin staging ``` 2. **Deploy to Staging**: ```bash # Set environment variables (if not in ~/.bashrc) export STAGING_HOST=staging.michaelschiemer.de export STAGING_USER=deploy # Run deployment ./deployment/scripts/deploy-staging.sh ``` 3. **Verify Deployment**: ```bash # Check application curl -k https://staging.michaelschiemer.de/health # Monitor logs ssh deploy@staging.michaelschiemer.de \ "cd /opt/framework-staging/current && docker-compose logs -f" # Check container status ssh deploy@staging.michaelschiemer.de \ "cd /opt/framework-staging/current && docker-compose ps" ``` 4. **Test Application**: - Perform manual testing - Run automated tests - Verify feature functionality - Check performance 5. **If Issues Found**: ```bash # Rollback staging ./deployment/scripts/rollback.sh staging # Or continue testing for non-critical issues ``` ### Production Deployment Workflow **Step-by-Step Process**: 1. **Pre-Deployment Checklist**: - [ ] Code reviewed and approved - [ ] Successfully deployed and tested in staging - [ ] Database migrations tested - [ ] Backup plan confirmed - [ ] Rollback plan confirmed - [ ] Team notified of deployment window 2. **Prepare Production Branch**: ```bash # Merge staging to main git checkout main git merge staging git push origin main ``` 3. **Verify Environment Variables**: ```bash # Required variables echo $PRODUCTION_HOST # Should be: michaelschiemer.de echo $PRODUCTION_USER # Should be: deploy # If not set export PRODUCTION_HOST=michaelschiemer.de export PRODUCTION_USER=deploy ``` 4. **Deploy to Production**: ```bash # IMPORTANT: Do NOT skip database backup ./deployment/scripts/deploy-production.sh # Monitor output carefully for any errors ``` 5. **Post-Deployment Verification**: ```bash # 1. Check main application curl -k https://michaelschiemer.de/ # 2. Check API health curl -k https://michaelschiemer.de/api/health # 3. Monitor logs for errors ssh deploy@michaelschiemer.de \ "cd /opt/framework-production/current && docker-compose logs -f --tail=100" # 4. Check container status ssh deploy@michaelschiemer.de \ "cd /opt/framework-production/current && docker-compose ps" # 5. Verify database migrations applied ssh deploy@michaelschiemer.de \ "cd /opt/framework-production/current && \ docker-compose exec production-app php console.php db:status" ``` 6. **Smoke Testing**: - Test critical user paths - Verify authentication - Test key API endpoints - Check database connectivity - Verify external integrations 7. **If Deployment Fails**: ```bash # Automatic rollback should have occurred # If manual rollback needed: ./deployment/scripts/rollback.sh production # Monitor rollback ssh deploy@michaelschiemer.de \ "cd /opt/framework-production/current && docker-compose logs -f" ``` 8. **Post-Deployment**: - Monitor application metrics - Watch error logs for 30 minutes - Notify team of successful deployment - Document any issues encountered ## Troubleshooting ### SSH Connection Issues **Problem**: `Permission denied (publickey)` **Solutions**: ```bash # Verify SSH key exists ls -la ~/.ssh/framework-deploy* # Test SSH connection ssh -i ~/.ssh/framework-deploy deploy@staging.michaelschiemer.de "echo 'SSH works'" # Check SSH config cat ~/.ssh/config # Re-copy public key ssh-copy-id -i ~/.ssh/framework-deploy.pub deploy@staging.michaelschiemer.de # Check server-side authorized_keys ssh deploy@staging.michaelschiemer.de "cat ~/.ssh/authorized_keys" ``` ### Docker Build Failures **Problem**: Docker build fails during deployment **Solutions**: ```bash # Check Docker is running docker info # Test build locally docker build \ --file docker/php/Dockerfile \ --tag localhost:5000/framework:test \ --build-arg ENV=staging \ . # Check Dockerfile syntax docker build --file docker/php/Dockerfile --no-cache . # Clear Docker cache docker system prune -a ``` ### Registry Push Failures **Problem**: `docker push` fails **Solutions**: ```bash # Check registry is accessible curl http://localhost:5000/v2/ # Verify image exists locally docker images | grep framework # Test manual push docker push localhost:5000/framework:staging # Check registry logs docker logs registry # If running registry as container ``` ### Deployment Script Fails **Problem**: Deployment script exits with error **Solutions**: ```bash # Run with bash debug mode bash -x ./deployment/scripts/deploy-staging.sh # Check remote directory exists ssh deploy@staging.michaelschiemer.de "ls -la /opt/framework-staging" # Verify Docker Compose files ssh deploy@staging.michaelschiemer.de \ "cd /opt/framework-staging/current && docker-compose config" # Check deployment logs on server ssh deploy@staging.michaelschiemer.de \ "cd /opt/framework-staging/current && docker-compose logs" ``` ### Health Check Failures **Problem**: Health checks fail but containers are running **Solutions**: ```bash # Check container logs ssh deploy@staging.michaelschiemer.de \ "cd /opt/framework-staging/current && docker-compose logs --tail=50" # Check PHP-FPM status ssh deploy@staging.michaelschiemer.de \ "cd /opt/framework-staging/current && \ docker-compose exec staging-app pgrep php-fpm" # Test health endpoint manually ssh deploy@staging.michaelschiemer.de \ "curl -k http://localhost/health" # Check Nginx configuration ssh deploy@staging.michaelschiemer.de \ "cd /opt/framework-staging/current && \ docker-compose exec staging-nginx nginx -t" ``` ### Rollback Issues **Problem**: Rollback script fails **Solutions**: ```bash # List available backups ssh deploy@production \ "cd /opt/framework-production && ls -dt backup_*" # Manually restore backup ssh deploy@production " cd /opt/framework-production docker-compose -f current/docker-compose.base.yml \ -f current/docker-compose.prod.yml down rm -rf current cp -r backup_20250124_153045 current cd current docker-compose -f docker-compose.base.yml \ -f docker-compose.prod.yml up -d " # Check failed deployment archive ssh deploy@production "ls -dt /opt/framework-production/failed_*" ``` ### Database Migration Failures **Problem**: Migrations fail during deployment **Solutions**: ```bash # Check migration status ssh deploy@production \ "cd /opt/framework-production/current && \ docker-compose exec production-app php console.php db:status" # Manually run migrations ssh deploy@production \ "cd /opt/framework-production/current && \ docker-compose exec production-app php console.php db:migrate --force" # Rollback migrations ssh deploy@production \ "cd /opt/framework-production/current && \ docker-compose exec production-app php console.php db:rollback" # Check database connectivity ssh deploy@production \ "cd /opt/framework-production/current && \ docker-compose exec production-app php console.php db:check" ``` ## Security Best Practices ### SSH Key Management **✅ Do**: - Use 4096-bit RSA keys minimum - Generate separate keys for staging and production - Store private keys securely (never commit to git) - Rotate keys quarterly - Use SSH config for key management **❌ Don't**: - Use password-only authentication - Share keys between environments - Commit private keys to version control - Use personal SSH keys for deployments ### Environment Variables **✅ Do**: - Use environment variables for secrets - Document required variables - Use different credentials per environment - Validate variables before deployment **❌ Don't**: - Hard-code credentials in scripts - Commit .env files with secrets - Use production credentials in staging ### Deployment User Permissions **Recommended Setup**: ```bash # On remote server # Create deployment user sudo useradd -m -s /bin/bash deploy # Add to docker group sudo usermod -aG docker deploy # Set directory ownership sudo chown -R deploy:deploy /opt/framework-staging sudo chown -R deploy:deploy /opt/framework-production # Restrict sudo (if needed) # Add to /etc/sudoers.d/deploy deploy ALL=(ALL) NOPASSWD: /usr/bin/docker, /usr/bin/docker-compose ``` ### Backup Management **✅ Do**: - Automate database backups - Keep multiple backup versions - Test backup restoration regularly - Monitor backup disk space **❌ Don't**: - Skip backups in production - Keep unlimited backups (disk space) - Store backups only on deployment server ## Monitoring and Maintenance ### Health Monitoring **Automated Checks**: ```bash # Cron job for health monitoring # Add to crontab -e on deployment server */5 * * * * curl -f -k https://michaelschiemer.de/health || echo "Health check failed" | mail -s "Production Health Alert" admin@michaelschiemer.de ``` **Manual Checks**: ```bash # Check all services ssh deploy@production \ "cd /opt/framework-production/current && docker-compose ps" # Check resource usage ssh deploy@production "docker stats --no-stream" # Check disk space ssh deploy@production "df -h /opt/framework-production" ``` ### Log Management **View Logs**: ```bash # Follow logs ssh deploy@production \ "cd /opt/framework-production/current && docker-compose logs -f" # View specific service logs ssh deploy@production \ "cd /opt/framework-production/current && \ docker-compose logs -f production-app" # Last 100 lines ssh deploy@production \ "cd /opt/framework-production/current && \ docker-compose logs --tail=100" ``` ### Backup Cleanup **Manual Cleanup**: ```bash # List backups by size ssh deploy@production "du -sh /opt/framework-production/backup_* | sort -h" # Remove specific old backup ssh deploy@production "rm -rf /opt/framework-production/backup_20240101_000000" # Keep only last 5 backups ssh deploy@staging " cd /opt/framework-staging ls -dt backup_* | tail -n +6 | xargs rm -rf " ``` ## Appendix ### Directory Structure **Local Project**: ``` michaelschiemer/ ├── deployment/ │ ├── scripts/ │ │ ├── deploy-staging.sh # Staging deployment │ │ ├── deploy-production.sh # Production deployment │ │ └── rollback.sh # Rollback script │ ├── docs/ │ │ └── DEPLOYMENT_GUIDE.md # This file │ └── legacy/ │ └── gitea-workflows/ # Archived Gitea workflows ├── docker-compose.base.yml ├── docker-compose.staging.yml ├── docker-compose.prod.yml └── docker/ └── php/ └── Dockerfile ``` **Remote Server**: ``` /opt/framework-staging/ or /opt/framework-production/ ├── current/ # Active deployment │ ├── docker-compose.base.yml │ ├── docker-compose.staging.yml │ ├── docker/ │ └── deploy.sh ├── backup_20250124_153045/ # Timestamped backups ├── backup_20250124_120000/ ├── backup_20250123_183015/ └── failed_20250124_154512/ # Failed deployment (if rollback occurred) ``` ### Environment Variable Reference | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `STAGING_HOST` | Yes* | staging.michaelschiemer.de | Staging server hostname/IP | | `STAGING_USER` | No | deploy | Staging SSH user | | `STAGING_SSH_PORT` | No | 22 | Staging SSH port | | `PRODUCTION_HOST` | Yes* | michaelschiemer.de | Production server hostname/IP | | `PRODUCTION_USER` | No | deploy | Production SSH user | | `PRODUCTION_SSH_PORT` | No | 22 | Production SSH port | | `REGISTRY` | No | localhost:5000 | Docker registry URL | | `IMAGE_NAME` | No | framework | Docker image name | | `IMAGE_TAG` | No | staging/latest | Docker image tag | | `SKIP_BACKUP` | No | false | Skip database backup (production) | | `FORCE_REBUILD` | No | false | Force Docker image rebuild | *Required for respective deployment type ### Common Commands Reference **Local Commands**: ```bash # Deploy staging ./deployment/scripts/deploy-staging.sh # Deploy production ./deployment/scripts/deploy-production.sh # Rollback staging ./deployment/scripts/rollback.sh staging # Rollback production ./deployment/scripts/rollback.sh production # Test SSH connection ssh deploy@staging.michaelschiemer.de "echo 'SSH works'" ``` **Remote Commands** (via SSH): ```bash # View logs docker-compose logs -f # Check status docker-compose ps # Restart services docker-compose restart # Stop services docker-compose down # Start services docker-compose up -d # Execute command in container docker-compose exec production-app php console.php db:status # View container logs docker-compose logs production-app --tail=50 ``` --- **Last Updated**: 2025-01-24 **Framework Version**: 2.x **Deployment Method**: SSH-based deployment scripts