# New Deployment Architecture **Created**: 2025-11-24 **Status**: Design Phase - Implementation Pending ## Executive Summary This document defines the redesigned deployment architecture using Docker Compose for all environments (local, staging, production). The architecture addresses all issues identified in `legacy/ARCHITECTURE_ANALYSIS.md` and provides a clear, maintainable deployment strategy. ## Architecture Principles ### 1. Docker Compose for All Environments - **No Docker Swarm**: Use Docker Compose exclusively for simplicity - **Environment-Specific Files**: One `docker-compose.{env}.yml` per environment - **Shared Base**: Common configuration in `docker-compose.base.yml` - **Override Pattern**: Environment files override base configuration ### 2. Clear Separation of Concerns - **Ansible**: Server provisioning ONLY (install Docker, setup users, configure firewall) - **Gitea Actions**: Application deployment via CI/CD pipelines - **Docker Compose**: Runtime orchestration and service management ### 3. Explicit Configuration - **Absolute Paths**: No relative paths in volume mounts - **Named Volumes**: For persistent data (databases, caches) - **Environment Variables**: Clear `.env.{environment}` files - **Docker Secrets**: File-based secrets via `*_FILE` pattern ### 4. Network Isolation - **traefik-public**: External network for Traefik ingress - **backend**: Internal network for application services - **cache**: Isolated network for Redis - **app-internal**: External network for shared PostgreSQL ## Service Architecture ### Core Services ``` ┌─────────────────────────────────────────────────────────────┐ │ Internet │ └───────────────────────────┬─────────────────────────────────┘ │ ┌───────▼────────┐ │ Traefik │ (traefik-public) │ Reverse Proxy │ └───────┬────────┘ │ ┌───────────────────┼───────────────────┐ │ │ │ ┌───▼────┐ ┌──────▼──────┐ ┌──────▼──────┐ │ Web │ │ PHP │ │ Queue │ │ Nginx │◄─────│ PHP-FPM │ │ Worker │ └────────┘ └──────┬──────┘ └──────┬──────┘ │ │ (backend network) │ │ │ │ ┌──────────────────┼───────────────────┤ │ │ │ ┌───▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐ │ Redis │ │ PostgreSQL │ │ MinIO │ │ Cache │ │ (External) │ │ Storage │ └──────────┘ └─────────────┘ └─────────────┘ ``` ### Service Responsibilities **web** (Nginx): - Static file serving - PHP-FPM proxy - HTTPS termination (via Traefik) - Security headers **php** (PHP-FPM): - Application runtime - Framework code execution - Database connections - Queue job dispatching **postgres** (PostgreSQL): - Primary database - **External Stack**: Shared across environments via `app-internal` network - Backup automation via separate container **redis** (Redis): - Session storage - Cache layer - Queue backend **queue-worker** (PHP CLI): - Background job processing - Scheduled task execution - Async operations **minio** (S3-compatible storage): - File uploads - Asset storage - Backup storage **traefik** (Reverse Proxy): - Dynamic routing - SSL/TLS termination - Let's Encrypt automation - Load balancing ## Environment Specifications ### docker-compose.local.yml (Development) **Purpose**: Fast local development with debugging enabled **Key Features**: - Development ports: 8888:80, 443:443, 5433:5432 - Host volume mounts for live code editing: `./ → /var/www/html` - Xdebug enabled: `XDEBUG_MODE=debug` - Debug flags: `APP_DEBUG=true` - Docker socket access: `/var/run/docker.sock` (for Docker management) - Relaxed resource limits **Services**: ```yaml services: web: ports: - "8888:80" - "443:443" environment: - APP_ENV=development volumes: - ./:/var/www/html:cached restart: unless-stopped php: volumes: - ./:/var/www/html:cached - /var/run/docker.sock:/var/run/docker.sock:ro environment: - APP_DEBUG=true - XDEBUG_MODE=debug - DB_HOST=postgres # External PostgreSQL Stack - DB_PASSWORD_FILE=/run/secrets/db_user_password secrets: - db_user_password - redis_password - app_key networks: - backend - app-internal # External PostgreSQL Stack redis: command: redis-server --requirepass $(cat /run/secrets/redis_password) secrets: - redis_password ``` **Networks**: - `backend`: Internal communication (web ↔ php) - `cache`: Redis isolation - `app-internal`: **External** - connects to PostgreSQL Stack **Secrets**: File-based in `./secrets/` directory (gitignored) ### docker-compose.staging.yml (Staging) **Purpose**: Production-like environment for testing deployments **Key Features**: - Traefik with Let's Encrypt **staging** certificates - Production-like resource limits (moderate) - External PostgreSQL via `app-internal` network - No host mounts - code baked into Docker image - Moderate logging (JSON format) **Services**: ```yaml services: web: image: registry.michaelschiemer.de/web:${GIT_COMMIT} networks: - traefik-public - backend labels: - "traefik.enable=true" - "traefik.http.routers.web-staging.rule=Host(`staging.michaelschiemer.de`)" - "traefik.http.routers.web-staging.entrypoints=websecure" - "traefik.http.routers.web-staging.tls.certresolver=letsencrypt-staging" deploy: resources: limits: memory: 256M cpus: "0.5" reservations: memory: 128M php: image: registry.michaelschiemer.de/php:${GIT_COMMIT} environment: - APP_ENV=staging - APP_DEBUG=false - XDEBUG_MODE=off - DB_HOST=postgres - DB_PASSWORD_FILE=/run/secrets/db_user_password_staging secrets: - db_user_password_staging - redis_password_staging - app_key_staging networks: - backend - app-internal deploy: resources: limits: memory: 512M cpus: "1.0" traefik: image: traefik:v3.0 command: - "--certificatesresolvers.letsencrypt-staging.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory" networks: - traefik-public ``` **Networks**: - `traefik-public`: **External** - shared Traefik network - `backend`: Internal application network - `app-internal`: **External** - shared PostgreSQL network **Image Strategy**: Pre-built images from Gitea registry, tagged with Git commit SHA ### docker-compose.prod.yml (Production) **Purpose**: Hardened production environment with full security **Key Features**: - Production SSL certificates (Let's Encrypt production CA) - Strict security: `APP_DEBUG=false`, `XDEBUG_MODE=off` - Resource limits: production-grade (higher than staging) - Health checks for all services - Read-only root filesystem where possible - No-new-privileges security option - Comprehensive logging **Services**: ```yaml services: web: image: registry.michaelschiemer.de/web:${GIT_TAG} read_only: true security_opt: - no-new-privileges:true networks: - traefik-public - backend labels: - "traefik.enable=true" - "traefik.http.routers.web-prod.rule=Host(`michaelschiemer.de`) || Host(`www.michaelschiemer.de`)" - "traefik.http.routers.web-prod.entrypoints=websecure" - "traefik.http.routers.web-prod.tls.certresolver=letsencrypt" deploy: resources: limits: memory: 512M cpus: "1.0" reservations: memory: 256M healthcheck: test: ["CMD", "curl", "-f", "http://localhost/health"] interval: 30s timeout: 10s retries: 3 start_period: 40s php: image: registry.michaelschiemer.de/php:${GIT_TAG} security_opt: - no-new-privileges:true environment: - APP_ENV=production - APP_DEBUG=false - XDEBUG_MODE=off - DB_HOST=postgres - DB_PASSWORD_FILE=/run/secrets/db_user_password_prod secrets: - db_user_password_prod - redis_password_prod - app_key_prod networks: - backend - app-internal deploy: resources: limits: memory: 1G cpus: "2.0" reservations: memory: 512M healthcheck: test: ["CMD", "php-fpm-healthcheck"] interval: 30s timeout: 10s retries: 3 traefik: image: traefik:v3.0 command: - "--certificatesresolvers.letsencrypt.acme.caserver=https://acme-v02.api.letsencrypt.org/directory" networks: - traefik-public ``` **Image Strategy**: Release-tagged images from Gitea registry (semantic versioning) **Security Hardening**: - Read-only root filesystem - No privilege escalation - AppArmor/SELinux profiles - Resource quotas enforced ## Volume Strategy ### Named Volumes (Persistent Data) **Database Volumes**: ```yaml volumes: postgres-data: driver: local redis-data: driver: local minio-data: driver: local ``` **Characteristics**: - Managed by Docker - Persisted across container restarts - Backed up regularly ### Bind Mounts (Development Only) **Local Development**: ```yaml volumes: - /absolute/path/to/project:/var/www/html:cached - /absolute/path/to/storage/logs:/var/www/html/storage/logs:rw ``` **Rules**: - **Absolute paths ONLY** - no relative paths - Development environment only - Not used in staging/production ### Volume Mount Patterns **Application Code**: - **Local**: Bind mount (`./:/var/www/html`) for live editing - **Staging/Prod**: Baked into Docker image (no mount) **Logs**: - **All Environments**: Named volume or bind mount to host for persistence **Uploads/Assets**: - **All Environments**: MinIO for S3-compatible storage ## Secret Management ### Docker Secrets via File Pattern **Framework Support**: Custom PHP Framework supports `*_FILE` environment variable pattern **Example**: ```yaml # Environment variable points to secret file environment: - DB_PASSWORD_FILE=/run/secrets/db_password # Secret definition secrets: db_password: file: ./secrets/db_password.txt ``` ### Secret Files Structure ``` deployment/ ├── secrets/ # Gitignored! │ ├── local/ │ │ ├── db_password.txt │ │ ├── redis_password.txt │ │ └── app_key.txt │ ├── staging/ │ │ ├── db_password.txt │ │ ├── redis_password.txt │ │ └── app_key.txt │ └── production/ │ ├── db_password.txt │ ├── redis_password.txt │ └── app_key.txt ``` **Security**: - **NEVER commit secrets** to version control - Add `secrets/` to `.gitignore` - Use Ansible Vault or external secret manager for production secrets - Rotate secrets regularly ### Framework Integration Framework automatically loads secrets via `EncryptedEnvLoader`: ```php // Framework automatically resolves *_FILE variables $dbPassword = $env->get('DB_PASSWORD'); // Reads from DB_PASSWORD_FILE $redisPassword = $env->get('REDIS_PASSWORD'); // Reads from REDIS_PASSWORD_FILE ``` ## Environment Variables Strategy ### .env Files per Environment **Structure**: ``` deployment/ ├── .env.local # Local development ├── .env.staging # Staging environment ├── .env.production # Production environment └── .env.example # Template (committed to git) ``` **Composition Command**: ```bash # Local docker compose -f docker-compose.base.yml -f docker-compose.local.yml --env-file .env.local up # Staging docker compose -f docker-compose.base.yml -f docker-compose.staging.yml --env-file .env.staging up # Production docker compose -f docker-compose.base.yml -f docker-compose.prod.yml --env-file .env.production up ``` ### Variable Categories **Application**: ```bash APP_ENV=production APP_DEBUG=false APP_NAME="Michael Schiemer" APP_URL=https://michaelschiemer.de ``` **Database**: ```bash DB_HOST=postgres DB_PORT=5432 DB_DATABASE=michaelschiemer DB_USERNAME=postgres # DB_PASSWORD via secrets: DB_PASSWORD_FILE=/run/secrets/db_password ``` **Cache**: ```bash REDIS_HOST=redis REDIS_PORT=6379 # REDIS_PASSWORD via secrets: REDIS_PASSWORD_FILE=/run/secrets/redis_password ``` **Image Tags** (Staging/Production): ```bash GIT_COMMIT=abc123def456 # Staging GIT_TAG=v2.1.0 # Production ``` ## Service Dependencies and Startup Order ### Dependency Graph ``` traefik (independent) ↓ postgres (external stack) ↓ redis (independent) ↓ php (depends: postgres, redis) ↓ web (depends: php) ↓ queue-worker (depends: postgres, redis) ↓ minio (independent) ``` ### docker-compose.yml Dependency Specification ```yaml services: php: depends_on: postgres: condition: service_healthy redis: condition: service_started web: depends_on: php: condition: service_started queue-worker: depends_on: php: condition: service_started postgres: condition: service_healthy redis: condition: service_started ``` **Health Checks**: - PostgreSQL: `pg_isready` check - Redis: `redis-cli PING` check - PHP-FPM: Custom health check script - Nginx: `curl http://localhost/health` ## CI/CD Pipeline Design ### Gitea Actions Workflows **Directory Structure**: ``` .gitea/ └── workflows/ ├── build-app.yml # Build & Test ├── deploy-staging.yml # Deploy to Staging └── deploy-production.yml # Deploy to Production ``` ### Workflow 1: Build & Test (`build-app.yml`) **Triggers**: - Push to any branch - Pull request to `develop` or `main` **Steps**: 1. Checkout code 2. Setup PHP 8.5, Node.js 3. Install dependencies (`composer install`, `npm install`) 4. Run PHP tests (`./vendor/bin/pest`) 5. Run JS tests (`npm test`) 6. Build frontend assets (`npm run build`) 7. Build Docker images (`docker build -t registry.michaelschiemer.de/php:${COMMIT_SHA} .`) 8. Push to Gitea registry 9. Security scan (Trivy) **Artifacts**: Docker images tagged with Git commit SHA ### Workflow 2: Deploy to Staging (`deploy-staging.yml`) **Triggers**: - Merge to `develop` branch (automatic) - Manual trigger via Gitea UI **Steps**: 1. Checkout code 2. Pull Docker images from registry (`registry.michaelschiemer.de/php:${COMMIT_SHA}`) 3. SSH to staging server 4. Export environment variables (`GIT_COMMIT=${COMMIT_SHA}`) 5. Run docker compose: `docker compose -f docker-compose.base.yml -f docker-compose.staging.yml --env-file .env.staging up -d` 6. Wait for health checks 7. Run smoke tests 8. Notify via webhook (success/failure) **Rollback**: Keep previous image tag, redeploy on failure ### Workflow 3: Deploy to Production (`deploy-production.yml`) **Triggers**: - Git tag push (e.g., `v2.1.0`) - **manual approval required** - Manual trigger via Gitea UI **Steps**: 1. **Manual Approval Gate** - require approval from maintainer 2. Checkout code at tag 3. Pull Docker images from registry (`registry.michaelschiemer.de/php:${GIT_TAG}`) 4. SSH to production server 5. Create backup of current deployment 6. Export environment variables (`GIT_TAG=${TAG}`) 7. Run docker compose: `docker compose -f docker-compose.base.yml -f docker-compose.prod.yml --env-file .env.production up -d` 8. Wait for health checks (extended timeout) 9. Run smoke tests 10. Monitor metrics for 5 minutes 11. Notify via webhook (success/failure) **Rollback Procedure**: 1. Detect deployment failure (health checks fail) 2. Automatically revert to previous Git tag 3. Run deployment with previous image 4. Notify team of rollback ### Deployment Safety **Blue-Green Deployment** (Future Enhancement): - Run new version alongside old version - Switch traffic via Traefik routing - Instant rollback by switching back **Canary Deployment** (Future Enhancement): - Route 10% traffic to new version - Monitor error rates - Gradually increase to 100% ## Network Architecture ### Network Definitions ```yaml networks: traefik-public: external: true name: traefik-public backend: internal: true driver: bridge cache: internal: true driver: bridge app-internal: external: true name: app-internal ``` ### Network Isolation **traefik-public** (External): - Services: traefik, web - Purpose: Ingress from internet - Isolation: Public-facing only **backend** (Internal): - Services: web, php, queue-worker - Purpose: Application communication - Isolation: No external access **cache** (Internal): - Services: redis - Purpose: Cache isolation - Isolation: Only accessible via backend network bridge **app-internal** (External): - Services: php, queue-worker, postgres (external stack) - Purpose: Shared PostgreSQL access across environments - Isolation: Multi-environment shared resource ### Service Discovery Docker DNS automatically resolves service names: - `php` resolves to PHP-FPM container IP - `redis` resolves to Redis container IP - `postgres` resolves to external PostgreSQL stack IP No manual IP configuration required. ## Migration from Legacy System ### Migration Steps 1. ✅ **COMPLETED** - Archive legacy deployment to `deployment/legacy/` 2. ✅ **COMPLETED** - Document legacy issues in `ARCHITECTURE_ANALYSIS.md` 3. ✅ **COMPLETED** - Design new architecture (this document) 4. ⏳ **NEXT** - Implement `docker-compose.base.yml` 5. ⏳ **NEXT** - Implement `docker-compose.local.yml` 6. ⏳ **NEXT** - Test local environment 7. ⏳ **PENDING** - Implement `docker-compose.staging.yml` 8. ⏳ **PENDING** - Deploy to staging server 9. ⏳ **PENDING** - Implement `docker-compose.prod.yml` 10. ⏳ **PENDING** - Setup Gitea Actions workflows 11. ⏳ **PENDING** - Deploy to production via CI/CD ### Data Migration **Database**: - Export from legacy PostgreSQL: `pg_dump` - Import to new PostgreSQL: `pg_restore` - Verify data integrity **Secrets**: - Extract secrets from legacy Ansible Vault - Create new secret files in `deployment/secrets/` - Update environment variables **SSL Certificates**: - Reuse existing Let's Encrypt certificates (copy `acme.json`) - Or regenerate via Traefik ACME ## Comparison: Legacy vs New | Aspect | Legacy System | New Architecture | |--------|---------------|------------------| | **Orchestration** | Docker Swarm + Docker Compose (confused) | Docker Compose only | | **Deployment** | Ansible playbooks (unclear responsibility) | Gitea Actions CI/CD | | **Environment Files** | Scattered stack files (9+ directories) | 3 environment files (local/staging/prod) | | **Volume Mounts** | Relative paths (causing failures) | Absolute paths + named volumes | | **Secrets** | Docker Swarm secrets (not working) | File-based secrets via `*_FILE` | | **Networks** | Unclear dependencies | Explicit network definitions | | **SSL** | Let's Encrypt (working) | Let's Encrypt (preserved) | | **PostgreSQL** | Embedded in each stack | External shared stack | ## Benefits of New Architecture 1. **Clarity**: Single source of truth per environment 2. **Maintainability**: Clear separation of concerns (Ansible vs CI/CD) 3. **Debuggability**: Explicit configuration, no hidden magic 4. **Scalability**: Easy to add new environments or services 5. **Security**: File-based secrets, network isolation 6. **CI/CD Integration**: Automated deployments via Gitea Actions 7. **Rollback Safety**: Git-tagged releases, health checks ## Next Steps 1. **Implement Base Configuration**: Create `docker-compose.base.yml` 2. **Test Local Environment**: Verify `docker-compose.local.yml` works 3. **Setup Staging**: Deploy to staging server, test deployment pipeline 4. **Production Deployment**: Manual approval, monitoring 5. **Documentation**: Update README with new deployment procedures --- **References**: - Legacy system analysis: `deployment/legacy/ARCHITECTURE_ANALYSIS.md` - Docker Compose documentation: https://docs.docker.com/compose/ - Traefik v3 documentation: https://doc.traefik.io/traefik/ - Gitea Actions: https://docs.gitea.com/usage/actions/overview