- Remove middleware reference from Gitea Traefik labels (caused routing issues) - Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s) - Add explicit service reference in Traefik labels - Fix intermittent 504 timeouts by improving PostgreSQL connection handling Fixes Gitea unreachability via git.michaelschiemer.de
21 KiB
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
cd ../traefik
docker compose up -d
2. DNS Configuration
Point michaelschiemer.de to your server IP (94.16.110.151)
3. Docker Registry Access
# Login to private registry
docker login registry.michaelschiemer.de
# Verify access
docker pull registry.michaelschiemer.de/michaelschiemer-app:latest
4. Application Image Built
# Build and push application image
docker build -t registry.michaelschiemer.de/michaelschiemer-app:latest .
docker push registry.michaelschiemer.de/michaelschiemer-app:latest
5. PostgreSQL Production Stack Available
The separate postgresql-production stack must be running. See deployment/stacks/postgresql-production/README.md for setup.
Configuration
1. Create Environment File
cp .env.example .env
2. Generate Passwords
# Database password
openssl rand -base64 32
# Redis password
openssl rand -base64 32
Update .env:
DB_PASS=<generated-database-password>
REDIS_PASSWORD=<generated-redis-password>
3. Configure Database Connection
If using Stack 5 (PostgreSQL):
DB_HOST=postgres
DB_PORT=5432
DB_NAME=michaelschiemer
DB_USER=appuser
If using Stack 2 Gitea MySQL:
DB_HOST=mysql
DB_PORT=3306
DB_NAME=michaelschiemer
DB_USER=appuser
If using external database:
DB_HOST=<external-host>
DB_PORT=<port>
DB_NAME=<database>
DB_USER=<username>
4. Adjust Queue Worker Settings (Optional)
# 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
APP_ENV=production # production, staging, or development
APP_DEBUG=false # Enable debug mode (false for production)
APP_URL=https://michaelschiemer.de
Deployment
Initial Setup
# 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
# 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
# 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
# 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
# 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
# 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:
# 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:
# 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
Database Stack: PostgreSQL Production (Separate Stack)
Connection:
- Production Application Stack connects to separate
postgresql-productionstack - Database container:
postgres-production - Network:
postgres-production-internal - Database service must be on same Docker network (
postgres-production-internal) - Connection configured via DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASS
Run Migrations:
# 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:
DB_HOST=mysql
DB_PORT=3306
DB_NAME=michaelschiemer # Create separate database
DB_USER=appuser # Create dedicated user
Create Database:
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
#!/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
# 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:
# 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:
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
- Environment Variables: Never commit .env to version control
- Strong Passwords: Use
openssl rand -base64 32for all passwords - Redis Password: Always set REDIS_PASSWORD in production
- Database Access: Use dedicated database user with minimal privileges
- File Permissions: Ensure storage directories have correct ownership
- Updates: Regularly update Docker images and dependencies
- 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:
# 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
# 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:
# docker-compose.yml
--maxmemory 512mb # Adjust based on available RAM
--maxmemory-policy allkeys-lru # Eviction policy
Persistence:
# 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:
// In application code
$redis = new Redis();
$redis->pconnect('redis', 6379); // Persistent connection
Nginx Optimization
Worker Processes (add to nginx.conf if needed):
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):
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):
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):
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):
fastcgi_connect_timeout 60s;
fastcgi_send_timeout 180s;
fastcgi_read_timeout 180s;
Queue Worker Optimization
Worker Count:
# Run multiple queue workers for parallel processing
docker compose up -d --scale queue-worker=3
Queue Configuration (.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:
# 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
# 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):
// 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
# 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
# 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
# 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
# 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