Docker Deployment Guide
This guide covers how to deploy ShulNET using Docker in various environments, from local development to production deployments.
Table of Contents
- Quick Start (Development)
- Production Deployment
- Docker Image Architecture
- Environment Configuration
- Integration with External Services
- Kubernetes Deployment
- Docker Swarm/Compose Production Setup
- Troubleshooting
Quick Start (Development)
Prerequisites
- Docker Desktop or Docker Engine 20.10+
- Docker Compose v2.0+
- Git
Local Development Setup
- Clone the repository
git clone cd shulnet-php
- Configure environment
cp .env.example .env # Edit .env to set your environment variables
- Start the stack
docker-compose up -d
- Run initial setup
# Generate application key docker-compose exec app php artisan key:generate # Run migrations docker-compose exec app php artisan migrate # Seed the database (optional) docker-compose exec app php artisan db:seed # Create storage link docker-compose exec app php artisan storage:link
- Access the application
- Application: http://localhost:8000
- Database: localhost:3307 (MySQL)
- Redis: localhost:6380
Production Deployment
Building the Production Image
The Dockerfile uses multi-stage builds for optimization:
# Build production image docker build --target app -t shulnet-php:latest . # Or with version tag docker build --target app -t shulnet-php:v1.0.0 . # Push to registry docker tag shulnet-php:latest your-registry.com/shulnet-php:latest docker push your-registry.com/shulnet-php:latest
Production Environment Variables
Create a .env.production file (DO NOT commit to version control):
# Application APP_NAME=ShulNET APP_ENV=production APP_KEY=base64:YOUR_GENERATED_KEY_HERE APP_DEBUG=false APP_URL=https://your-domain.com # Database DB_CONNECTION=mysql DB_HOST=your-db-host DB_PORT=3306 DB_DATABASE=shulnet_production DB_USERNAME=shulnet_user DB_PASSWORD=STRONG_PASSWORD_HERE # Cache & Session CACHE_STORE=redis SESSION_DRIVER=redis QUEUE_CONNECTION=redis # Redis REDIS_HOST=your-redis-host REDIS_PASSWORD=REDIS_PASSWORD_HERE REDIS_PORT=6379 # Mail MAIL_MAILER=smtp MAIL_HOST=smtp.your-provider.com MAIL_PORT=587 [email protected] MAIL_PASSWORD=EMAIL_PASSWORD_HERE MAIL_ENCRYPTION=tls [email protected] MAIL_FROM_NAME="${APP_NAME}" # AWS S3 (if using for file storage) AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= AWS_DEFAULT_REGION=us-east-1 AWS_BUCKET= FILESYSTEM_DISK=s3
Docker Image Architecture
Multi-Stage Build
The Dockerfile consists of three stages:
- Base Stage - Common dependencies
- PHP 8.4 FPM
- System packages (nginx, supervisor, image libraries)
- PHP extensions (pdo_mysql, gd, bcmath, zip, etc.)
- Vendor Stage - Dependency installation
- Composer dependencies (production-only)
- Optimized for caching
- App Stage - Final production image
- Application code
- Optimized autoloader
- Nginx configuration
- Supervisor configuration
- Proper permissions for www-data
Container Components
The production container runs three services via Supervisor:
- PHP-FPM (port 9000) - PHP application server
- Nginx (port 80) - Web server
- Laravel Queue Worker - Background job processing
Image Size Optimization
- Multi-stage builds reduce final image size
- No dev dependencies in production
- Optimized autoloader with --classmap-authoritative
- Clean apt cache after installations
Environment Configuration
Docker Compose Services
App Service
app: image: parkerfly38/shulnet-php:latest # Database and Redis must be available # Exposes port 8000 for development (80 internally)
Database Service
db: image: mysql:8.0 # Persistent volume: dbdata # Port 3307 externally, 3306 internally
Redis Service
redis: image: redis:alpine # Port 6380 externally, 6379 internally
Node Service (Development)
node: image: node:20-alpine # Runs Vite dev server on port 5173 # For asset compilation
Volume Mounts
Development:
volumes: - ./:/var/www/html # Live code reload - ./docker/php/local.ini:/usr/local/etc/php/conf.d/local.ini
Production:
volumes: - storage:/var/www/html/storage # Persistent storage - logs:/var/www/html/storage/logs # Log files
Integration with External Services
Reverse Proxy (Nginx/Traefik/HAProxy)
With Traefik
version: '3.8'
services:
app:
image: parkerfly38/shulnet-php:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.shulnet.rule=Host(`shulnet.example.com`)"
- "traefik.http.routers.shulnet.entrypoints=websecure"
- "traefik.http.routers.shulnet.tls.certresolver=letsencrypt"
- "traefik.http.services.shulnet.loadbalancer.server.port=80"
networks:
- traefik-public
- backend
db:
image: mysql:8.0
networks:
- backend
redis:
image: redis:alpine
networks:
- backend
networks:
traefik-public:
external: true
backend:
internal: true
With Nginx Reverse Proxy
upstream shulnet_backend {
server app:80;
}
server {
listen 443 ssl http2;
server_name shulnet.example.com;
ssl_certificate /etc/ssl/certs/shulnet.crt;
ssl_certificate_key /etc/ssl/private/shulnet.key;
location / {
proxy_pass http://shulnet_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
External Database
To use an external MySQL/PostgreSQL database:
services:
app:
image: parkerfly38/shulnet-php:latest
environment:
- DB_CONNECTION=mysql
- DB_HOST=external-db.example.com
- DB_PORT=3306
- DB_DATABASE=shulnet
- DB_USERNAME=${DB_USERNAME}
- DB_PASSWORD=${DB_PASSWORD}
# Remove db service dependency
External Redis/Cache
services:
app:
image: parkerfly38/shulnet-php:latest
environment:
- REDIS_HOST=redis.example.com
- REDIS_PORT=6379
- REDIS_PASSWORD=${REDIS_PASSWORD}
- CACHE_STORE=redis
- SESSION_DRIVER=redis
- QUEUE_CONNECTION=redis
S3-Compatible Object Storage
For file uploads (CloudFlare R2, AWS S3, MinIO):
services:
app:
image: parkerfly38/shulnet-php:latest
environment:
- FILESYSTEM_DISK=s3
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
- AWS_DEFAULT_REGION=us-east-1
- AWS_BUCKET=shulnet-storage
- AWS_ENDPOINT=https://storage.example.com # For R2/MinIO
- AWS_USE_PATH_STYLE_ENDPOINT=false
Kubernetes Deployment
Namespace and ConfigMap
apiVersion: v1 kind: Namespace metadata: name: shulnet --- apiVersion: v1 kind: ConfigMap metadata: name: shulnet-config namespace: shulnet data: APP_NAME: "ShulNET" APP_ENV: "production" DB_CONNECTION: "mysql" CACHE_STORE: "redis" SESSION_DRIVER: "redis" QUEUE_CONNECTION: "redis"
Secrets
apiVersion: v1 kind: Secret metadata: name: shulnet-secrets namespace: shulnet type: Opaque stringData: APP_KEY: "base64:YOUR_APP_KEY_HERE" DB_PASSWORD: "YOUR_DB_PASSWORD" DB_USERNAME: "shulnet_user" REDIS_PASSWORD: "YOUR_REDIS_PASSWORD"
Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: shulnet-app
namespace: shulnet
spec:
replicas: 3
selector:
matchLabels:
app: shulnet
template:
metadata:
labels:
app: shulnet
spec:
containers:
- name: shulnet
image: parkerfly38/shulnet-php:latest
ports:
- containerPort: 80
envFrom:
- configMapRef:
name: shulnet-config
- secretRef:
name: shulnet-secrets
env:
- name: DB_HOST
value: "mysql-service"
- name: REDIS_HOST
value: "redis-service"
volumeMounts:
- name: storage
mountPath: /var/www/html/storage
livenessProbe:
httpGet:
path: /api/health
port: 80
initialDelaySeconds: 40
periodSeconds: 30
readinessProbe:
httpGet:
path: /api/health
port: 80
initialDelaySeconds: 10
periodSeconds: 10
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
volumes:
- name: storage
persistentVolumeClaim:
claimName: shulnet-storage-pvc
Service
apiVersion: v1
kind: Service
metadata:
name: shulnet-service
namespace: shulnet
spec:
selector:
app: shulnet
ports:
- protocol: TCP
port: 80
targetPort: 80
type: ClusterIP
Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shulnet-ingress
namespace: shulnet
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/proxy-body-size: "40m"
spec:
ingressClassName: nginx
tls:
- hosts:
- shulnet.example.com
secretName: shulnet-tls
rules:
- host: shulnet.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: shulnet-service
port:
number: 80
Persistent Volume Claim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: shulnet-storage-pvc
namespace: shulnet
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Gi
storageClassName: nfs-client # Or your storage class
Database (MySQL StatefulSet)
apiVersion: v1
kind: Service
metadata:
name: mysql-service
namespace: shulnet
spec:
ports:
- port: 3306
selector:
app: mysql
clusterIP: None
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
namespace: shulnet
spec:
serviceName: mysql-service
replicas: 1
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: shulnet-secrets
key: DB_PASSWORD
- name: MYSQL_DATABASE
value: shulnet
- name: MYSQL_USER
valueFrom:
secretKeyRef:
name: shulnet-secrets
key: DB_USERNAME
- name: MYSQL_PASSWORD
valueFrom:
secretKeyRef:
name: shulnet-secrets
key: DB_PASSWORD
volumeMounts:
- name: mysql-data
mountPath: /var/lib/mysql
volumeClaimTemplates:
- metadata:
name: mysql-data
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 20Gi
Redis Deployment
apiVersion: v1
kind: Service
metadata:
name: redis-service
namespace: shulnet
spec:
ports:
- port: 6379
selector:
app: redis
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
namespace: shulnet
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:alpine
ports:
- containerPort: 6379
command: ["redis-server", "--requirepass", "$(REDIS_PASSWORD)"]
env:
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: shulnet-secrets
key: REDIS_PASSWORD
Init Job (Migrations)
apiVersion: batch/v1
kind: Job
metadata:
name: shulnet-migrate
namespace: shulnet
spec:
template:
spec:
containers:
- name: migrate
image: parkerfly38/shulnet-php:latest
command: ["php", "artisan", "migrate", "--force"]
envFrom:
- configMapRef:
name: shulnet-config
- secretRef:
name: shulnet-secrets
env:
- name: DB_HOST
value: "mysql-service"
- name: REDIS_HOST
value: "redis-service"
restartPolicy: OnFailure
Docker Swarm/Compose Production Setup
Stack Deployment
version: '3.8'
services:
app:
image: parkerfly38/shulnet-php:latest
deploy:
replicas: 3
update_config:
parallelism: 1
delay: 10s
restart_policy:
condition: on-failure
placement:
constraints:
- node.role == worker
environment:
- DB_HOST=db
- REDIS_HOST=redis
secrets:
- app_key
- db_password
networks:
- frontend
- backend
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/api/health"]
interval: 30s
timeout: 3s
retries: 3
start_period: 40s
db:
image: mysql:8.0
deploy:
replicas: 1
placement:
constraints:
- node.labels.database == true
environment:
- MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db_password
- MYSQL_DATABASE=shulnet
secrets:
- db_password
volumes:
- db_data:/var/lib/mysql
networks:
- backend
redis:
image: redis:alpine
deploy:
replicas: 1
networks:
- backend
nginx:
image: nginx:alpine
deploy:
replicas: 2
ports:
- "80:80"
- "443:443"
configs:
- source: nginx_config
target: /etc/nginx/nginx.conf
networks:
- frontend
networks:
frontend:
driver: overlay
backend:
driver: overlay
internal: true
volumes:
db_data:
driver: local
secrets:
app_key:
external: true
db_password:
external: true
configs:
nginx_config:
external: true
Deploy Commands
# Create secrets echo "base64:YOUR_APP_KEY" | docker secret create app_key - echo "YOUR_DB_PASSWORD" | docker secret create db_password - # Deploy stack docker stack deploy -c docker-compose.prod.yml shulnet # Scale services docker service scale shulnet_app=5 # Update service docker service update --image parkerfly38/shulnet-php:v1.0.1 shulnet_app # View logs docker service logs -f shulnet_app
Troubleshooting
Common Issues
Container fails health check
# Check container logs docker logs shulnet-app # Check health endpoint manually docker exec shulnet-app curl -f http://localhost/api/health # Verify PHP-FPM is running docker exec shulnet-app ps aux | grep php-fpm
Permission errors on storage
# Fix permissions docker exec shulnet-app chown -R www-data:www-data /var/www/html/storage docker exec shulnet-app chmod -R 775 /var/www/html/storage
Database connection issues
# Test database connectivity
docker exec shulnet-app php artisan tinker
>>> DB::connection()->getPdo();
# Check environment variables
docker exec shulnet-app env | grep DB_
Queue workers not processing jobs
# Check supervisor status docker exec shulnet-app supervisorctl status # Restart queue worker docker exec shulnet-app supervisorctl restart laravel-worker:* # View worker logs docker exec shulnet-app tail -f /var/www/html/storage/logs/worker.log
High memory usage
# Check container stats docker stats shulnet-app # Adjust PHP memory limit in docker/php/local.ini # Then rebuild image or mount updated config
Debugging
# Shell into container docker exec -it shulnet-app bash # View all supervisor logs docker exec shulnet-app tail -f /var/log/supervisor/supervisord.log # View nginx logs docker exec shulnet-app tail -f /var/log/nginx.err.log # View PHP-FPM logs docker exec shulnet-app tail -f /var/log/php-fpm.err.log # Laravel logs docker exec shulnet-app tail -f /var/www/html/storage/logs/laravel.log
Performance Tuning
PHP-FPM Pool Configuration
Create docker/php/www.conf (mount as volume):
[www] pm = dynamic pm.max_children = 50 pm.start_servers = 10 pm.min_spare_servers = 5 pm.max_spare_servers = 20 pm.max_requests = 500
Nginx Worker Configuration
Update docker/nginx/default.conf:
worker_processes auto;
worker_connections 1024;
http {
keepalive_timeout 65;
client_max_body_size 40M;
# Enable gzip
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript;
}
MySQL Optimization
For production MySQL, add configuration:
db:
image: mysql:8.0
command: --default-authentication-plugin=mysql_native_password
--max-connections=200
--innodb-buffer-pool-size=1G
--innodb-log-file-size=256M
Monitoring
Health Checks
The application includes a health check endpoint at /api/health that verifies:
- Application is running
- Database connectivity
- Cache connectivity
Logging
Logs are available in multiple locations:
- Application logs: /var/www/html/storage/logs/laravel.log
- Worker logs: /var/www/html/storage/logs/worker.log
- Nginx logs: /var/log/nginx.err.log and /var/log/nginx.access.log
- PHP-FPM logs: /var/log/php-fpm.err.log
- Supervisor logs: /var/log/supervisor/supervisord.log
External Monitoring Integration
Consider integrating with:
- Application Performance Monitoring: New Relic, DataDog, Scout APM
- Log Aggregation: ELK Stack, Splunk, Papertrail
- Uptime Monitoring: Pingdom, UptimeRobot, StatusCake
Security Considerations
Best Practices
- Never commit secrets - Use .env files, Docker secrets, or environment variables
- Use TLS/SSL - Always encrypt traffic with HTTPS in production
- Regular updates - Keep base images and dependencies updated
- Limit container privileges - Run as non-root user (www-data)
- Network isolation - Use internal networks for database/cache
- Backup regularly - Automated backups of database and storage volumes
- Rate limiting - Implement at reverse proxy level
- Firewall rules - Only expose necessary ports
Image Scanning
# Scan for vulnerabilities docker scan parkerfly38/shulnet-php:latest # Or use Trivy trivy image parkerfly38/shulnet-php:latest
Backup and Recovery
Database Backup
# Backup
docker exec shulnet-db mysqldump -u root -p${DB_PASSWORD} shulnet > backup.sql
# Restore
docker exec -i shulnet-db mysql -u root -p${DB_PASSWORD} shulnet < backup.sql
Storage Backup
# Backup storage volume docker run --rm \ -v shulnet_storage:/data \ -v $(pwd):/backup \ alpine tar czf /backup/storage-backup.tar.gz /data # Restore docker run --rm \ -v shulnet_storage:/data \ -v $(pwd):/backup \ alpine tar xzf /backup/storage-backup.tar.gz -C /
Additional Resources
- Laravel Deployment Documentation
- Docker Best Practices
- Kubernetes Documentation
- Docker Compose Documentation
For additional help or questions, please open an issue in the repository.