feat: CI/CD pipeline setup complete - Ansible playbooks updated, secrets configured, workflow ready
This commit is contained in:
40
deployment/stacks/application/.env.example
Normal file
40
deployment/stacks/application/.env.example
Normal file
@@ -0,0 +1,40 @@
|
||||
# Application Stack Configuration
|
||||
# Copy this file to .env and adjust values
|
||||
|
||||
# Timezone
|
||||
TZ=Europe/Berlin
|
||||
|
||||
# Application Domain
|
||||
APP_DOMAIN=michaelschiemer.de
|
||||
|
||||
# Application Settings
|
||||
APP_ENV=production
|
||||
APP_DEBUG=false
|
||||
APP_URL=https://michaelschiemer.de
|
||||
|
||||
# Database Configuration
|
||||
# Note: MySQL runs in Stack 5 (PostgreSQL) or external server
|
||||
DB_HOST=mysql
|
||||
DB_PORT=3306
|
||||
DB_NAME=michaelschiemer
|
||||
DB_USER=appuser
|
||||
DB_PASS=<generate-with-openssl-rand-base64-32>
|
||||
|
||||
# Redis Configuration
|
||||
# Generate password with: openssl rand -base64 32
|
||||
REDIS_PASSWORD=<generate-with-openssl-rand-base64-32>
|
||||
|
||||
# Cache Configuration
|
||||
CACHE_DRIVER=redis
|
||||
CACHE_PREFIX=app
|
||||
|
||||
# Session Configuration
|
||||
SESSION_DRIVER=redis
|
||||
SESSION_LIFETIME=120
|
||||
|
||||
# Queue Worker Configuration
|
||||
QUEUE_DRIVER=redis
|
||||
QUEUE_CONNECTION=default
|
||||
QUEUE_WORKER_SLEEP=3
|
||||
QUEUE_WORKER_TRIES=3
|
||||
QUEUE_WORKER_TIMEOUT=60
|
||||
918
deployment/stacks/application/README.md
Normal file
918
deployment/stacks/application/README.md
Normal file
@@ -0,0 +1,918 @@
|
||||
# Application Stack - PHP Application with Nginx, Redis, Queue & Scheduler
|
||||
|
||||
## Overview
|
||||
|
||||
Production-ready PHP application stack with multi-service architecture for high-performance web applications.
|
||||
|
||||
**Features**:
|
||||
- PHP-FPM 8.3+ application runtime
|
||||
- Nginx web server with optimized configuration
|
||||
- Redis for caching, sessions, and queue backend
|
||||
- Dedicated queue worker for background job processing
|
||||
- Scheduler for cron job execution
|
||||
- SSL via Traefik with automatic Let's Encrypt certificates
|
||||
- Private Docker Registry integration
|
||||
- Health checks and automatic restart policies
|
||||
|
||||
## Services
|
||||
|
||||
- **app** (PHP-FPM) - Application runtime handling PHP code execution
|
||||
- **nginx** (Nginx 1.25) - Web server proxying requests to PHP-FPM
|
||||
- **redis** (Redis 7) - Cache, session, and queue backend
|
||||
- **queue-worker** - Background job processor
|
||||
- **scheduler** - Cron job executor
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### 1. Traefik Stack Running
|
||||
```bash
|
||||
cd ../traefik
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### 2. DNS Configuration
|
||||
Point `michaelschiemer.de` to your server IP (94.16.110.151)
|
||||
|
||||
### 3. Docker Registry Access
|
||||
```bash
|
||||
# Login to private registry
|
||||
docker login registry.michaelschiemer.de
|
||||
|
||||
# Verify access
|
||||
docker pull registry.michaelschiemer.de/michaelschiemer-app:latest
|
||||
```
|
||||
|
||||
### 4. Application Image Built
|
||||
```bash
|
||||
# Build and push application image
|
||||
docker build -t registry.michaelschiemer.de/michaelschiemer-app:latest .
|
||||
docker push registry.michaelschiemer.de/michaelschiemer-app:latest
|
||||
```
|
||||
|
||||
### 5. Database Available
|
||||
Stack 5 (PostgreSQL/MySQL) must be running or external database configured.
|
||||
|
||||
## Configuration
|
||||
|
||||
### 1. Create Environment File
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
### 2. Generate Passwords
|
||||
|
||||
```bash
|
||||
# Database password
|
||||
openssl rand -base64 32
|
||||
|
||||
# Redis password
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
Update `.env`:
|
||||
```env
|
||||
DB_PASS=<generated-database-password>
|
||||
REDIS_PASSWORD=<generated-redis-password>
|
||||
```
|
||||
|
||||
### 3. Configure Database Connection
|
||||
|
||||
**If using Stack 5 (PostgreSQL)**:
|
||||
```env
|
||||
DB_HOST=postgres
|
||||
DB_PORT=5432
|
||||
DB_NAME=michaelschiemer
|
||||
DB_USER=appuser
|
||||
```
|
||||
|
||||
**If using Stack 2 Gitea MySQL**:
|
||||
```env
|
||||
DB_HOST=mysql
|
||||
DB_PORT=3306
|
||||
DB_NAME=michaelschiemer
|
||||
DB_USER=appuser
|
||||
```
|
||||
|
||||
**If using external database**:
|
||||
```env
|
||||
DB_HOST=<external-host>
|
||||
DB_PORT=<port>
|
||||
DB_NAME=<database>
|
||||
DB_USER=<username>
|
||||
```
|
||||
|
||||
### 4. Adjust Queue Worker Settings (Optional)
|
||||
|
||||
```env
|
||||
# Queue worker configuration
|
||||
QUEUE_WORKER_SLEEP=3 # Sleep between job checks (seconds)
|
||||
QUEUE_WORKER_TRIES=3 # Max attempts per job
|
||||
QUEUE_WORKER_TIMEOUT=60 # Max execution time per job (seconds)
|
||||
```
|
||||
|
||||
### 5. Configure Application Settings
|
||||
|
||||
```env
|
||||
APP_ENV=production # production, staging, or development
|
||||
APP_DEBUG=false # Enable debug mode (false for production)
|
||||
APP_URL=https://michaelschiemer.de
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Initial Setup
|
||||
|
||||
```bash
|
||||
# Ensure Traefik is running
|
||||
docker network inspect traefik-public
|
||||
|
||||
# Create .env file
|
||||
cp .env.example .env
|
||||
# Edit .env with generated passwords
|
||||
|
||||
# Start application stack
|
||||
docker compose up -d
|
||||
|
||||
# Check logs
|
||||
docker compose logs -f
|
||||
|
||||
# Verify health
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
### Verify Deployment
|
||||
|
||||
```bash
|
||||
# Test application endpoint
|
||||
curl https://michaelschiemer.de/health
|
||||
# Expected: HTTP 200 "healthy"
|
||||
|
||||
# Check service status
|
||||
docker compose ps
|
||||
# All services should show "healthy" status
|
||||
|
||||
# View logs
|
||||
docker compose logs app # Application logs
|
||||
docker compose logs nginx # Web server logs
|
||||
docker compose logs redis # Redis logs
|
||||
docker compose logs queue-worker # Queue worker logs
|
||||
docker compose logs scheduler # Scheduler logs
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Accessing the Application
|
||||
|
||||
**Main Application**:
|
||||
- URL: https://michaelschiemer.de
|
||||
- SSL: Automatic via Traefik + Let's Encrypt
|
||||
- Auth: Configured in application
|
||||
|
||||
### Managing Services
|
||||
|
||||
```bash
|
||||
# Start stack
|
||||
docker compose up -d
|
||||
|
||||
# Stop stack
|
||||
docker compose down
|
||||
|
||||
# Restart specific service
|
||||
docker compose restart app
|
||||
docker compose restart nginx
|
||||
|
||||
# View logs (follow mode)
|
||||
docker compose logs -f app
|
||||
docker compose logs -f queue-worker
|
||||
|
||||
# Execute commands in app container
|
||||
docker compose exec app php console.php db:migrate
|
||||
docker compose exec app php console.php cache:clear
|
||||
|
||||
# Access Redis CLI
|
||||
docker compose exec redis redis-cli -a <REDIS_PASSWORD>
|
||||
```
|
||||
|
||||
### Queue Management
|
||||
|
||||
```bash
|
||||
# Monitor queue worker
|
||||
docker compose logs -f queue-worker
|
||||
|
||||
# Restart queue worker (e.g., after code changes)
|
||||
docker compose restart queue-worker
|
||||
|
||||
# Check queue status
|
||||
docker compose exec app php console.php queue:status
|
||||
|
||||
# Process specific queue
|
||||
docker compose exec app php console.php queue:work --queue=emails
|
||||
|
||||
# Clear failed jobs
|
||||
docker compose exec app php console.php queue:retry-failed
|
||||
```
|
||||
|
||||
### Scheduler Management
|
||||
|
||||
```bash
|
||||
# View scheduler logs
|
||||
docker compose logs -f scheduler
|
||||
|
||||
# List scheduled tasks
|
||||
docker compose exec app php console.php scheduler:list
|
||||
|
||||
# Run scheduler manually (for testing)
|
||||
docker compose exec app php console.php scheduler:run
|
||||
|
||||
# Restart scheduler
|
||||
docker compose restart scheduler
|
||||
```
|
||||
|
||||
### Cache Management
|
||||
|
||||
```bash
|
||||
# Clear application cache
|
||||
docker compose exec app php console.php cache:clear
|
||||
|
||||
# Clear specific cache tags
|
||||
docker compose exec app php console.php cache:forget user:123
|
||||
|
||||
# View cache statistics
|
||||
docker compose exec redis redis-cli -a <REDIS_PASSWORD> INFO stats
|
||||
|
||||
# Monitor cache in real-time
|
||||
docker compose exec redis redis-cli -a <REDIS_PASSWORD> MONITOR
|
||||
```
|
||||
|
||||
## Integration with Other Stacks
|
||||
|
||||
### Stack 1: Traefik (Reverse Proxy)
|
||||
|
||||
**Automatic SSL & Routing**:
|
||||
- Traefik labels in docker-compose.yml configure routing
|
||||
- SSL certificates automatically obtained via Let's Encrypt
|
||||
- Middleware chain applies security headers, rate limiting, etc.
|
||||
|
||||
**Verify Integration**:
|
||||
```bash
|
||||
# Check Traefik router
|
||||
docker exec traefik traefik healthcheck
|
||||
|
||||
# View Traefik logs
|
||||
docker logs traefik | grep michaelschiemer.de
|
||||
```
|
||||
|
||||
### Stack 3: Docker Registry
|
||||
|
||||
**Image Pulling**:
|
||||
- Stack pulls application image from private registry
|
||||
- Credentials configured via docker login
|
||||
|
||||
**Update Application**:
|
||||
```bash
|
||||
# Pull latest image
|
||||
docker compose pull app queue-worker scheduler
|
||||
|
||||
# Recreate containers with new image
|
||||
docker compose up -d --force-recreate app queue-worker scheduler
|
||||
```
|
||||
|
||||
### Stack 5: Database (PostgreSQL or MySQL)
|
||||
|
||||
**Connection**:
|
||||
- Database service must be on same Docker network or externally accessible
|
||||
- Connection configured via DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASS
|
||||
|
||||
**Run Migrations**:
|
||||
```bash
|
||||
# Apply database migrations
|
||||
docker compose exec app php console.php db:migrate
|
||||
|
||||
# Check migration status
|
||||
docker compose exec app php console.php db:status
|
||||
|
||||
# Rollback migration
|
||||
docker compose exec app php console.php db:rollback
|
||||
```
|
||||
|
||||
### Stack 2: Gitea (Optional - Shared MySQL)
|
||||
|
||||
**If using Gitea's MySQL**:
|
||||
```env
|
||||
DB_HOST=mysql
|
||||
DB_PORT=3306
|
||||
DB_NAME=michaelschiemer # Create separate database
|
||||
DB_USER=appuser # Create dedicated user
|
||||
```
|
||||
|
||||
**Create Database**:
|
||||
```bash
|
||||
docker exec mysql mysql -u root -p
|
||||
CREATE DATABASE michaelschiemer CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
CREATE USER 'appuser'@'%' IDENTIFIED BY '<password>';
|
||||
GRANT ALL PRIVILEGES ON michaelschiemer.* TO 'appuser'@'%';
|
||||
FLUSH PRIVILEGES;
|
||||
```
|
||||
|
||||
## Backup & Recovery
|
||||
|
||||
### Manual Backup
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# backup-application.sh
|
||||
|
||||
BACKUP_DIR="/backups/application"
|
||||
DATE=$(date +%Y%m%d_%H%M%S)
|
||||
|
||||
mkdir -p $BACKUP_DIR
|
||||
|
||||
# Backup application storage volume
|
||||
docker run --rm \
|
||||
-v app-storage:/data \
|
||||
-v $BACKUP_DIR:/backup \
|
||||
alpine tar czf /backup/app-storage-$DATE.tar.gz -C /data .
|
||||
|
||||
# Backup application logs
|
||||
docker run --rm \
|
||||
-v app-logs:/data \
|
||||
-v $BACKUP_DIR:/backup \
|
||||
alpine tar czf /backup/app-logs-$DATE.tar.gz -C /data .
|
||||
|
||||
# Backup Redis data (if persistence enabled)
|
||||
docker run --rm \
|
||||
-v redis-data:/data \
|
||||
-v $BACKUP_DIR:/backup \
|
||||
alpine tar czf /backup/redis-data-$DATE.tar.gz -C /data .
|
||||
|
||||
# Backup .env file
|
||||
cp .env $BACKUP_DIR/env-$DATE
|
||||
|
||||
echo "Backup completed: $BACKUP_DIR/*-$DATE.*"
|
||||
```
|
||||
|
||||
### Restore from Backup
|
||||
|
||||
```bash
|
||||
# Stop stack
|
||||
docker compose down
|
||||
|
||||
# Restore storage volume
|
||||
docker run --rm \
|
||||
-v app-storage:/data \
|
||||
-v /backups/application:/backup \
|
||||
alpine tar xzf /backup/app-storage-YYYYMMDD_HHMMSS.tar.gz -C /data
|
||||
|
||||
# Restore logs
|
||||
docker run --rm \
|
||||
-v app-logs:/data \
|
||||
-v /backups/application:/backup \
|
||||
alpine tar xzf /backup/app-logs-YYYYMMDD_HHMMSS.tar.gz -C /data
|
||||
|
||||
# Restore Redis data
|
||||
docker run --rm \
|
||||
-v redis-data:/data \
|
||||
-v /backups/application:/backup \
|
||||
alpine tar xzf /backup/redis-data-YYYYMMDD_HHMMSS.tar.gz -C /data
|
||||
|
||||
# Restore .env
|
||||
cp /backups/application/env-YYYYMMDD_HHMMSS .env
|
||||
|
||||
# Start stack
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Automated Backups
|
||||
|
||||
Add to crontab:
|
||||
```bash
|
||||
# Daily backup at 2 AM
|
||||
0 2 * * * /path/to/backup-application.sh
|
||||
|
||||
# Keep only last 14 days
|
||||
0 3 * * * find /backups/application -type f -mtime +14 -delete
|
||||
```
|
||||
|
||||
### Redis Persistence
|
||||
|
||||
**Automatic Persistence** (configured in docker-compose.yml):
|
||||
- RDB snapshots: save 900 1, save 300 10, save 60 10000
|
||||
- AOF (Append-Only File): appendonly yes, appendfsync everysec
|
||||
|
||||
**Manual Redis Backup**:
|
||||
```bash
|
||||
# Trigger manual save
|
||||
docker compose exec redis redis-cli -a <REDIS_PASSWORD> SAVE
|
||||
|
||||
# Export RDB file
|
||||
docker compose exec redis redis-cli -a <REDIS_PASSWORD> BGSAVE
|
||||
docker cp redis:/data/dump.rdb ./redis-backup.rdb
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Health Checks
|
||||
|
||||
```bash
|
||||
# Check all service health
|
||||
docker compose ps
|
||||
|
||||
# Application health
|
||||
curl https://michaelschiemer.de/health
|
||||
|
||||
# Redis health
|
||||
docker compose exec redis redis-cli -a <REDIS_PASSWORD> PING
|
||||
# Expected: PONG
|
||||
|
||||
# PHP-FPM health
|
||||
docker compose exec app php-fpm-healthcheck
|
||||
```
|
||||
|
||||
### Log Management
|
||||
|
||||
```bash
|
||||
# View all logs
|
||||
docker compose logs
|
||||
|
||||
# Follow logs for specific service
|
||||
docker compose logs -f app
|
||||
docker compose logs -f nginx
|
||||
docker compose logs -f queue-worker
|
||||
docker compose logs -f scheduler
|
||||
|
||||
# View last 100 lines
|
||||
docker compose logs --tail=100 app
|
||||
|
||||
# View logs since specific time
|
||||
docker compose logs --since 2024-01-01T00:00:00 app
|
||||
|
||||
# Search logs
|
||||
docker compose logs app | grep ERROR
|
||||
docker compose logs nginx | grep 404
|
||||
```
|
||||
|
||||
### Performance Metrics
|
||||
|
||||
```bash
|
||||
# Container resource usage
|
||||
docker stats app nginx redis queue-worker scheduler
|
||||
|
||||
# Redis statistics
|
||||
docker compose exec redis redis-cli -a <REDIS_PASSWORD> INFO stats
|
||||
docker compose exec redis redis-cli -a <REDIS_PASSWORD> INFO memory
|
||||
|
||||
# Redis slow log
|
||||
docker compose exec redis redis-cli -a <REDIS_PASSWORD> SLOWLOG GET 10
|
||||
|
||||
# Nginx status (if enabled)
|
||||
docker compose exec nginx curl http://localhost/nginx_status
|
||||
```
|
||||
|
||||
### Application Monitoring
|
||||
|
||||
```bash
|
||||
# Application metrics endpoint (if implemented)
|
||||
curl https://michaelschiemer.de/metrics
|
||||
|
||||
# Queue statistics
|
||||
docker compose exec app php console.php queue:stats
|
||||
|
||||
# Cache hit rate
|
||||
docker compose exec app php console.php cache:stats
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Application Not Accessible
|
||||
|
||||
```bash
|
||||
# Check service status
|
||||
docker compose ps
|
||||
|
||||
# Check Traefik routing
|
||||
docker exec traefik traefik healthcheck
|
||||
|
||||
# View Nginx logs
|
||||
docker compose logs nginx
|
||||
|
||||
# Test internal connectivity
|
||||
docker compose exec nginx curl http://app:9000/health
|
||||
```
|
||||
|
||||
### PHP-FPM Errors
|
||||
|
||||
```bash
|
||||
# View PHP-FPM logs
|
||||
docker compose logs app
|
||||
|
||||
# Check PHP-FPM pool status
|
||||
docker compose exec app php-fpm-healthcheck
|
||||
|
||||
# Restart PHP-FPM
|
||||
docker compose restart app
|
||||
|
||||
# Check PHP configuration
|
||||
docker compose exec app php -i | grep error
|
||||
```
|
||||
|
||||
### Redis Connection Issues
|
||||
|
||||
```bash
|
||||
# Test Redis connection
|
||||
docker compose exec redis redis-cli -a <REDIS_PASSWORD> PING
|
||||
|
||||
# Check Redis logs
|
||||
docker compose logs redis
|
||||
|
||||
# Verify Redis password
|
||||
grep REDIS_PASSWORD .env
|
||||
|
||||
# Test connection from app
|
||||
docker compose exec app php console.php redis:test
|
||||
```
|
||||
|
||||
### Queue Worker Not Processing Jobs
|
||||
|
||||
```bash
|
||||
# Check queue worker status
|
||||
docker compose ps queue-worker
|
||||
|
||||
# View queue worker logs
|
||||
docker compose logs queue-worker
|
||||
|
||||
# Check if queue worker process is running
|
||||
docker compose exec queue-worker pgrep -f queue:work
|
||||
|
||||
# Restart queue worker
|
||||
docker compose restart queue-worker
|
||||
|
||||
# Check queue size
|
||||
docker compose exec redis redis-cli -a <REDIS_PASSWORD> LLEN queues:default
|
||||
```
|
||||
|
||||
### Scheduler Not Running
|
||||
|
||||
```bash
|
||||
# Check scheduler status
|
||||
docker compose ps scheduler
|
||||
|
||||
# View scheduler logs
|
||||
docker compose logs scheduler
|
||||
|
||||
# Verify scheduler process
|
||||
docker compose exec scheduler pgrep -f scheduler:run
|
||||
|
||||
# Restart scheduler
|
||||
docker compose restart scheduler
|
||||
|
||||
# Test scheduler manually
|
||||
docker compose exec scheduler php console.php scheduler:run --once
|
||||
```
|
||||
|
||||
### High Memory Usage
|
||||
|
||||
```bash
|
||||
# Check container memory usage
|
||||
docker stats --no-stream
|
||||
|
||||
# Redis memory usage
|
||||
docker compose exec redis redis-cli -a <REDIS_PASSWORD> INFO memory
|
||||
|
||||
# Adjust Redis max memory (in .env or docker-compose.yml)
|
||||
# --maxmemory 512mb (default)
|
||||
|
||||
# Clear Redis cache if needed
|
||||
docker compose exec redis redis-cli -a <REDIS_PASSWORD> FLUSHDB
|
||||
```
|
||||
|
||||
### Slow Response Times
|
||||
|
||||
```bash
|
||||
# Check Nginx access logs
|
||||
docker compose logs nginx | grep "request_time"
|
||||
|
||||
# Enable PHP slow log (in php.ini or PHP-FPM pool config)
|
||||
# slowlog = /var/log/php-fpm-slow.log
|
||||
# request_slowlog_timeout = 5s
|
||||
|
||||
# Check database query performance
|
||||
docker compose exec app php console.php db:explain
|
||||
|
||||
# Check cache hit ratio
|
||||
docker compose exec redis redis-cli -a <REDIS_PASSWORD> INFO stats
|
||||
```
|
||||
|
||||
### SSL Certificate Issues
|
||||
|
||||
```bash
|
||||
# Check Traefik certificate
|
||||
docker exec traefik cat /acme.json | grep michaelschiemer.de
|
||||
|
||||
# Force certificate renewal (via Traefik)
|
||||
docker restart traefik
|
||||
|
||||
# Test SSL
|
||||
openssl s_client -connect michaelschiemer.de:443 -servername michaelschiemer.de < /dev/null
|
||||
```
|
||||
|
||||
### Database Connection Errors
|
||||
|
||||
```bash
|
||||
# Test database connection from app
|
||||
docker compose exec app php console.php db:test
|
||||
|
||||
# Verify database is accessible
|
||||
# If Stack 5 (PostgreSQL):
|
||||
docker exec postgres pg_isready
|
||||
|
||||
# If Stack 2 (MySQL):
|
||||
docker exec mysql mysqladmin ping
|
||||
|
||||
# Check database credentials in .env
|
||||
grep DB_ .env
|
||||
|
||||
# Test connection manually
|
||||
docker compose exec app php -r "new PDO('mysql:host=mysql;dbname=michaelschiemer', 'appuser', 'password');"
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
### Security Best Practices
|
||||
|
||||
1. **Environment Variables**: Never commit .env to version control
|
||||
2. **Strong Passwords**: Use `openssl rand -base64 32` for all passwords
|
||||
3. **Redis Password**: Always set REDIS_PASSWORD in production
|
||||
4. **Database Access**: Use dedicated database user with minimal privileges
|
||||
5. **File Permissions**: Ensure storage directories have correct ownership
|
||||
6. **Updates**: Regularly update Docker images and dependencies
|
||||
7. **Network Isolation**: app-internal network isolates services from external access
|
||||
|
||||
### Security Headers
|
||||
|
||||
**Nginx Configuration** (`nginx/conf.d/default.conf`):
|
||||
- X-Frame-Options: SAMEORIGIN
|
||||
- X-Content-Type-Options: nosniff
|
||||
- X-XSS-Protection: 1; mode=block
|
||||
- Referrer-Policy: strict-origin-when-cross-origin
|
||||
|
||||
**Traefik Middleware** (via default-chain@file):
|
||||
- HSTS
|
||||
- Additional security headers
|
||||
- Rate limiting
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
**Nginx Rate Limits** (configured in nginx/conf.d/default.conf):
|
||||
- API endpoints: 10 requests/second (burst 20)
|
||||
- General requests: 30 requests/second (burst 50)
|
||||
|
||||
**Adjust Rate Limits**:
|
||||
```nginx
|
||||
# Edit nginx/conf.d/default.conf
|
||||
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
|
||||
limit_req_zone $binary_remote_addr zone=general_limit:10m rate=30r/s;
|
||||
```
|
||||
|
||||
### File Upload Security
|
||||
|
||||
```nginx
|
||||
# Client body size limit (nginx/conf.d/default.conf)
|
||||
client_max_body_size 100M; # Adjust based on requirements
|
||||
```
|
||||
|
||||
### Sensitive File Protection
|
||||
|
||||
**Automatically Protected** (nginx/conf.d/default.conf):
|
||||
- .env, .git, .gitignore, .gitattributes
|
||||
- composer.json, composer.lock
|
||||
- package.json, package-lock.json
|
||||
- /storage (except /storage/public)
|
||||
|
||||
## Performance Tuning
|
||||
|
||||
### Redis Optimization
|
||||
|
||||
**Memory Management**:
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
--maxmemory 512mb # Adjust based on available RAM
|
||||
--maxmemory-policy allkeys-lru # Eviction policy
|
||||
```
|
||||
|
||||
**Persistence**:
|
||||
```yaml
|
||||
# Adjust RDB snapshot frequency
|
||||
--save 900 1 # After 900 sec if 1 key changed
|
||||
--save 300 10 # After 300 sec if 10 keys changed
|
||||
--save 60 10000 # After 60 sec if 10000 keys changed
|
||||
|
||||
# AOF settings
|
||||
--appendonly yes
|
||||
--appendfsync everysec # or: always, no
|
||||
```
|
||||
|
||||
**Connection Pooling**:
|
||||
```php
|
||||
// In application code
|
||||
$redis = new Redis();
|
||||
$redis->pconnect('redis', 6379); // Persistent connection
|
||||
```
|
||||
|
||||
### Nginx Optimization
|
||||
|
||||
**Worker Processes** (add to nginx.conf if needed):
|
||||
```nginx
|
||||
worker_processes auto;
|
||||
worker_connections 1024;
|
||||
```
|
||||
|
||||
**Gzip Compression** (configured):
|
||||
- Level: 6 (balance between compression ratio and CPU usage)
|
||||
- Types: text/plain, text/css, text/javascript, application/json, etc.
|
||||
- Min length: 1024 bytes
|
||||
|
||||
**Static File Caching** (configured):
|
||||
- Expires: 1 year for immutable assets
|
||||
- Cache-Control: public, immutable
|
||||
- Access log: disabled for static files
|
||||
|
||||
**Buffer Tuning** (configured):
|
||||
```nginx
|
||||
fastcgi_buffer_size 128k;
|
||||
fastcgi_buffers 256 16k;
|
||||
fastcgi_busy_buffers_size 256k;
|
||||
fastcgi_temp_file_write_size 256k;
|
||||
```
|
||||
|
||||
### PHP-FPM Optimization
|
||||
|
||||
**Pool Configuration** (adjust in Dockerfile or php-fpm.conf):
|
||||
```ini
|
||||
pm = dynamic
|
||||
pm.max_children = 50
|
||||
pm.start_servers = 5
|
||||
pm.min_spare_servers = 5
|
||||
pm.max_spare_servers = 35
|
||||
pm.max_requests = 500
|
||||
```
|
||||
|
||||
**OPcache** (enable in php.ini):
|
||||
```ini
|
||||
opcache.enable=1
|
||||
opcache.memory_consumption=128
|
||||
opcache.interned_strings_buffer=8
|
||||
opcache.max_accelerated_files=10000
|
||||
opcache.revalidate_freq=2
|
||||
```
|
||||
|
||||
**Timeout Settings** (configured in nginx):
|
||||
```nginx
|
||||
fastcgi_connect_timeout 60s;
|
||||
fastcgi_send_timeout 180s;
|
||||
fastcgi_read_timeout 180s;
|
||||
```
|
||||
|
||||
### Queue Worker Optimization
|
||||
|
||||
**Worker Count**:
|
||||
```bash
|
||||
# Run multiple queue workers for parallel processing
|
||||
docker compose up -d --scale queue-worker=3
|
||||
```
|
||||
|
||||
**Queue Configuration** (.env):
|
||||
```env
|
||||
QUEUE_WORKER_SLEEP=3 # Lower = more responsive, higher = less CPU
|
||||
QUEUE_WORKER_TRIES=3 # Retry failed jobs
|
||||
QUEUE_WORKER_TIMEOUT=60 # Increase for long-running jobs
|
||||
```
|
||||
|
||||
**Separate Queues**:
|
||||
```bash
|
||||
# Start workers for specific queues
|
||||
docker compose exec app php console.php queue:work --queue=high-priority
|
||||
docker compose exec app php console.php queue:work --queue=emails
|
||||
docker compose exec app php console.php queue:work --queue=default
|
||||
```
|
||||
|
||||
### Database Query Optimization
|
||||
|
||||
```bash
|
||||
# Analyze slow queries
|
||||
docker compose exec app php console.php db:explain
|
||||
|
||||
# Enable query logging
|
||||
docker compose exec app php console.php db:log enable
|
||||
|
||||
# Cache query results
|
||||
docker compose exec app php console.php cache:queries
|
||||
```
|
||||
|
||||
## Scheduler Configuration
|
||||
|
||||
### Cron Jobs
|
||||
|
||||
**Scheduler runs** `php console.php scheduler:run` continuously.
|
||||
|
||||
**Define Scheduled Tasks** (in application code):
|
||||
```php
|
||||
// Example: app/Console/Kernel.php
|
||||
protected function schedule(Schedule $schedule): void
|
||||
{
|
||||
// Run every minute
|
||||
$schedule->command('cache:clear')->everyMinute();
|
||||
|
||||
// Run hourly
|
||||
$schedule->command('reports:generate')->hourly();
|
||||
|
||||
// Run daily at 2 AM
|
||||
$schedule->command('cleanup:old-logs')->dailyAt('02:00');
|
||||
|
||||
// Run weekly on Sundays
|
||||
$schedule->command('backup:database')->weekly();
|
||||
}
|
||||
```
|
||||
|
||||
### Scheduler Monitoring
|
||||
|
||||
```bash
|
||||
# View scheduler logs
|
||||
docker compose logs -f scheduler
|
||||
|
||||
# List scheduled tasks
|
||||
docker compose exec app php console.php scheduler:list
|
||||
|
||||
# Run scheduler manually (for testing)
|
||||
docker compose exec app php console.php scheduler:run --once
|
||||
```
|
||||
|
||||
## Update Stack
|
||||
|
||||
### Update Application Code
|
||||
|
||||
```bash
|
||||
# Build new image
|
||||
docker build -t registry.michaelschiemer.de/michaelschiemer-app:latest .
|
||||
|
||||
# Push to registry
|
||||
docker push registry.michaelschiemer.de/michaelschiemer-app:latest
|
||||
|
||||
# Pull and recreate containers
|
||||
docker compose pull
|
||||
docker compose up -d --force-recreate app queue-worker scheduler
|
||||
|
||||
# Run migrations if needed
|
||||
docker compose exec app php console.php db:migrate
|
||||
```
|
||||
|
||||
### Update Nginx Configuration
|
||||
|
||||
```bash
|
||||
# Edit nginx configuration
|
||||
nano nginx/conf.d/default.conf
|
||||
|
||||
# Test configuration
|
||||
docker compose exec nginx nginx -t
|
||||
|
||||
# Reload Nginx
|
||||
docker compose exec nginx nginx -s reload
|
||||
|
||||
# Or restart Nginx
|
||||
docker compose restart nginx
|
||||
```
|
||||
|
||||
### Update Stack Configuration
|
||||
|
||||
```bash
|
||||
# Pull latest images
|
||||
docker compose pull
|
||||
|
||||
# Recreate containers
|
||||
docker compose up -d
|
||||
|
||||
# Verify
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- **Docker Compose Documentation**: https://docs.docker.com/compose/
|
||||
- **Nginx Documentation**: https://nginx.org/en/docs/
|
||||
- **Redis Documentation**: https://redis.io/documentation
|
||||
- **PHP-FPM Documentation**: https://www.php.net/manual/en/install.fpm.php
|
||||
- **Traefik v3 Documentation**: https://doc.traefik.io/traefik/
|
||||
|
||||
## Stack Integration Summary
|
||||
|
||||
**Depends On**:
|
||||
- Stack 1 (Traefik) - SSL and reverse proxy
|
||||
- Stack 3 (Docker Registry) - Application image storage
|
||||
- Stack 5 (Database) - Data persistence
|
||||
|
||||
**Provides**:
|
||||
- PHP application runtime
|
||||
- Web server with SSL
|
||||
- Background job processing
|
||||
- Scheduled task execution
|
||||
- Caching infrastructure
|
||||
215
deployment/stacks/application/docker-compose.yml
Normal file
215
deployment/stacks/application/docker-compose.yml
Normal file
@@ -0,0 +1,215 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# PHP-FPM Application Runtime
|
||||
app:
|
||||
image: git.michaelschiemer.de:5000/framework:latest
|
||||
container_name: app
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- app-internal
|
||||
environment:
|
||||
- TZ=Europe/Berlin
|
||||
- APP_ENV=${APP_ENV:-production}
|
||||
- APP_DEBUG=${APP_DEBUG:-false}
|
||||
- APP_URL=${APP_URL:-https://michaelschiemer.de}
|
||||
# Database
|
||||
- DB_HOST=${DB_HOST:-mysql}
|
||||
- DB_PORT=${DB_PORT:-3306}
|
||||
- DB_NAME=${DB_NAME}
|
||||
- DB_USER=${DB_USER}
|
||||
- DB_PASS=${DB_PASS}
|
||||
# Redis
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
- REDIS_PASSWORD=${REDIS_PASSWORD}
|
||||
# Cache
|
||||
- CACHE_DRIVER=redis
|
||||
- CACHE_PREFIX=${CACHE_PREFIX:-app}
|
||||
# Session
|
||||
- SESSION_DRIVER=redis
|
||||
- SESSION_LIFETIME=${SESSION_LIFETIME:-120}
|
||||
# Queue
|
||||
- QUEUE_DRIVER=redis
|
||||
- QUEUE_CONNECTION=default
|
||||
volumes:
|
||||
- app-storage:/var/www/html/storage
|
||||
- app-logs:/var/www/html/storage/logs
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "php-fpm-healthcheck"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
|
||||
# Nginx Web Server
|
||||
nginx:
|
||||
image: nginx:1.25-alpine
|
||||
container_name: nginx
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- traefik-public
|
||||
- app-internal
|
||||
environment:
|
||||
- TZ=Europe/Berlin
|
||||
volumes:
|
||||
- ./nginx/conf.d:/etc/nginx/conf.d:ro
|
||||
- app-storage:/var/www/html/storage:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
# HTTP Router
|
||||
- "traefik.http.routers.app.rule=Host(`${APP_DOMAIN:-michaelschiemer.de}`)"
|
||||
- "traefik.http.routers.app.entrypoints=websecure"
|
||||
- "traefik.http.routers.app.tls=true"
|
||||
- "traefik.http.routers.app.tls.certresolver=letsencrypt"
|
||||
# Service
|
||||
- "traefik.http.services.app.loadbalancer.server.port=80"
|
||||
# Middleware
|
||||
- "traefik.http.routers.app.middlewares=default-chain@file"
|
||||
# Network
|
||||
- "traefik.docker.network=traefik-public"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "-q", "http://localhost/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
depends_on:
|
||||
app:
|
||||
condition: service_healthy
|
||||
|
||||
# Redis Cache/Session/Queue Backend
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: redis
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- app-internal
|
||||
environment:
|
||||
- TZ=Europe/Berlin
|
||||
command: >
|
||||
redis-server
|
||||
--requirepass ${REDIS_PASSWORD}
|
||||
--maxmemory 512mb
|
||||
--maxmemory-policy allkeys-lru
|
||||
--save 900 1
|
||||
--save 300 10
|
||||
--save 60 10000
|
||||
--appendonly yes
|
||||
--appendfsync everysec
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
# Queue Worker (Background Jobs)
|
||||
queue-worker:
|
||||
image: git.michaelschiemer.de:5000/framework:latest
|
||||
container_name: queue-worker
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- app-internal
|
||||
environment:
|
||||
- TZ=Europe/Berlin
|
||||
- APP_ENV=${APP_ENV:-production}
|
||||
- APP_DEBUG=${APP_DEBUG:-false}
|
||||
# Database
|
||||
- DB_HOST=${DB_HOST:-mysql}
|
||||
- DB_PORT=${DB_PORT:-3306}
|
||||
- DB_NAME=${DB_NAME}
|
||||
- DB_USER=${DB_USER}
|
||||
- DB_PASS=${DB_PASS}
|
||||
# Redis
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
- REDIS_PASSWORD=${REDIS_PASSWORD}
|
||||
# Queue
|
||||
- QUEUE_DRIVER=redis
|
||||
- QUEUE_CONNECTION=default
|
||||
- QUEUE_WORKER_SLEEP=${QUEUE_WORKER_SLEEP:-3}
|
||||
- QUEUE_WORKER_TRIES=${QUEUE_WORKER_TRIES:-3}
|
||||
- QUEUE_WORKER_TIMEOUT=${QUEUE_WORKER_TIMEOUT:-60}
|
||||
volumes:
|
||||
- app-storage:/var/www/html/storage
|
||||
- app-logs:/var/www/html/storage/logs
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
command: php console.php queue:work --queue=default --timeout=${QUEUE_WORKER_TIMEOUT:-60}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pgrep -f 'queue:work' || exit 1"]
|
||||
interval: 60s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
depends_on:
|
||||
app:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
|
||||
# Scheduler (Cron Jobs)
|
||||
scheduler:
|
||||
image: git.michaelschiemer.de:5000/framework:latest
|
||||
container_name: scheduler
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- app-internal
|
||||
environment:
|
||||
- TZ=Europe/Berlin
|
||||
- APP_ENV=${APP_ENV:-production}
|
||||
- APP_DEBUG=${APP_DEBUG:-false}
|
||||
# Database
|
||||
- DB_HOST=${DB_HOST:-mysql}
|
||||
- DB_PORT=${DB_PORT:-3306}
|
||||
- DB_NAME=${DB_NAME}
|
||||
- DB_USER=${DB_USER}
|
||||
- DB_PASS=${DB_PASS}
|
||||
# Redis
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
- REDIS_PASSWORD=${REDIS_PASSWORD}
|
||||
volumes:
|
||||
- app-storage:/var/www/html/storage
|
||||
- app-logs:/var/www/html/storage/logs
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
command: php console.php scheduler:run
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pgrep -f 'scheduler:run' || exit 1"]
|
||||
interval: 60s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
depends_on:
|
||||
app:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
|
||||
volumes:
|
||||
app-storage:
|
||||
name: app-storage
|
||||
app-logs:
|
||||
name: app-logs
|
||||
redis-data:
|
||||
name: redis-data
|
||||
|
||||
networks:
|
||||
traefik-public:
|
||||
external: true
|
||||
app-internal:
|
||||
name: app-internal
|
||||
driver: bridge
|
||||
129
deployment/stacks/application/nginx/conf.d/default.conf
Normal file
129
deployment/stacks/application/nginx/conf.d/default.conf
Normal file
@@ -0,0 +1,129 @@
|
||||
# Nginx Configuration for PHP-FPM Application
|
||||
# Optimized for production with security headers and performance tuning
|
||||
|
||||
# Upstream PHP-FPM
|
||||
upstream php-fpm {
|
||||
server app:9000;
|
||||
}
|
||||
|
||||
# Rate limiting zones
|
||||
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
|
||||
limit_req_zone $binary_remote_addr zone=general_limit:10m rate=30r/s;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
root /var/www/html/public;
|
||||
index index.php index.html;
|
||||
|
||||
# Security Headers (additional to Traefik middleware)
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
# Client body size limit (adjust for file uploads)
|
||||
client_max_body_size 100M;
|
||||
|
||||
# Gzip compression
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_comp_level 6;
|
||||
gzip_types
|
||||
text/plain
|
||||
text/css
|
||||
text/javascript
|
||||
application/json
|
||||
application/javascript
|
||||
application/x-javascript
|
||||
text/xml
|
||||
application/xml
|
||||
application/xml+rss
|
||||
image/svg+xml;
|
||||
|
||||
# Logging
|
||||
access_log /var/log/nginx/access.log combined;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
|
||||
# Health check endpoint (for Docker healthcheck)
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "healthy\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
|
||||
# Deny access to sensitive files
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
|
||||
location ~ ^/(\.env|\.git|\.gitignore|\.gitattributes|composer\.(json|lock)|package(-lock)?\.json) {
|
||||
deny all;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
|
||||
# Static files - serve directly for performance
|
||||
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot|webp)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
access_log off;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# API endpoints - rate limited
|
||||
location ^~ /api/ {
|
||||
limit_req zone=api_limit burst=20 nodelay;
|
||||
limit_req_status 429;
|
||||
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
# PHP-FPM processing
|
||||
location ~ \.php$ {
|
||||
limit_req zone=general_limit burst=50 nodelay;
|
||||
|
||||
try_files $uri =404;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
|
||||
fastcgi_pass php-fpm;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
|
||||
# FastCGI parameters
|
||||
include fastcgi_params;
|
||||
|
||||
# PHP-FPM timeouts
|
||||
fastcgi_connect_timeout 60s;
|
||||
fastcgi_send_timeout 180s;
|
||||
fastcgi_read_timeout 180s;
|
||||
|
||||
# Buffer settings
|
||||
fastcgi_buffer_size 128k;
|
||||
fastcgi_buffers 256 16k;
|
||||
fastcgi_busy_buffers_size 256k;
|
||||
fastcgi_temp_file_write_size 256k;
|
||||
|
||||
# Hide PHP version
|
||||
fastcgi_hide_header X-Powered-By;
|
||||
}
|
||||
|
||||
# Fallback to index.php for non-existent files (framework routing)
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
# Deny access to storage directory (except public subdirectory)
|
||||
location ^~ /storage {
|
||||
deny all;
|
||||
}
|
||||
|
||||
location ^~ /storage/public {
|
||||
allow all;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user