Docker Deployment¶
This document provides comprehensive guidance for deploying the Stratpoint Timesheet Application using Docker containers, including development, staging, and production environments.
Docker Architecture¶
Container Architecture Overview¶
graph TB
A[Load Balancer] --> B[Web Containers]
B --> C[Application Containers]
C --> D[Database Container]
C --> E[Redis Container]
C --> F[Queue Workers]
G[File Storage] --> C
H[Monitoring] --> I[All Containers]
subgraph "Application Stack"
B
C
F
end
subgraph "Data Layer"
D
E
end
subgraph "External Services"
G
H
end
Container Components¶
- Web Server: Nginx reverse proxy and static file serving
- Application: PHP-FPM with Laravel application
- Database: MySQL 8.0 with optimized configuration
- Cache/Queue: Redis for caching and queue management
- Workers: Background job processing containers
- Monitoring: Application and infrastructure monitoring
Dockerfile Configuration¶
Multi-Stage Application Dockerfile¶
# Dockerfile
FROM php:8.1-fpm-alpine AS base
# Install system dependencies
RUN apk add --no-cache \
git \
curl \
libpng-dev \
libxml2-dev \
zip \
unzip \
mysql-client \
nodejs \
npm
# Install PHP extensions
RUN docker-php-ext-install \
pdo_mysql \
mbstring \
exif \
pcntl \
bcmath \
gd \
xml \
soap
# Install Redis extension
RUN pecl install redis && docker-php-ext-enable redis
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Set working directory
WORKDIR /var/www/html
# Copy composer files
COPY composer.json composer.lock ./
# Development stage
FROM base AS development
# Install development dependencies
RUN composer install --no-scripts --no-autoloader
# Copy application code
COPY . .
# Generate autoloader
RUN composer dump-autoload --optimize
# Install Node.js dependencies and build assets
RUN npm ci && npm run development
# Set permissions
RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache
# Expose port
EXPOSE 9000
CMD ["php-fpm"]
# Production stage
FROM base AS production
# Install production dependencies only
RUN composer install --no-dev --no-scripts --no-autoloader --optimize-autoloader
# Copy application code
COPY . .
# Generate optimized autoloader
RUN composer dump-autoload --optimize --classmap-authoritative
# Install Node.js dependencies and build production assets
RUN npm ci --only=production && npm run production
# Remove Node.js and npm to reduce image size
RUN apk del nodejs npm
# Set permissions
RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache
# Create non-root user
RUN addgroup -g 1001 -S appgroup && \
adduser -u 1001 -S appuser -G appgroup
# Switch to non-root user
USER appuser
# Expose port
EXPOSE 9000
CMD ["php-fpm"]
Nginx Dockerfile¶
# docker/nginx/Dockerfile
FROM nginx:alpine
# Copy nginx configuration
COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf
COPY docker/nginx/sites/default.conf /etc/nginx/conf.d/default.conf
# Copy SSL certificates (for production)
COPY docker/nginx/ssl/ /etc/nginx/ssl/
# Create nginx user
RUN addgroup -g 1001 -S nginx && \
adduser -u 1001 -S nginx -G nginx
# Set permissions
RUN chown -R nginx:nginx /var/cache/nginx /var/log/nginx /etc/nginx/conf.d
# Switch to non-root user
USER nginx
# Expose ports
EXPOSE 80 443
CMD ["nginx", "-g", "daemon off;"]
Docker Compose Configurations¶
Development Environment¶
# docker-compose.dev.yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
target: development
container_name: timesheet-app-dev
restart: unless-stopped
working_dir: /var/www/html
volumes:
- .:/var/www/html
- ./storage:/var/www/html/storage
environment:
- APP_ENV=development
- DB_HOST=mysql
- REDIS_HOST=redis
depends_on:
- mysql
- redis
networks:
- timesheet-network
nginx:
build:
context: .
dockerfile: docker/nginx/Dockerfile
container_name: timesheet-nginx-dev
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./public:/var/www/html/public:ro
- ./docker/nginx/sites/development.conf:/etc/nginx/conf.d/default.conf
depends_on:
- app
networks:
- timesheet-network
mysql:
image: mysql:8.0
container_name: timesheet-mysql-dev
restart: unless-stopped
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: root_password
MYSQL_DATABASE: stratpoint_timesheet_dev
MYSQL_USER: dev_user
MYSQL_PASSWORD: dev_password
volumes:
- mysql_data:/var/lib/mysql
- ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf
- ./database/init:/docker-entrypoint-initdb.d
command: --default-authentication-plugin=mysql_native_password
networks:
- timesheet-network
redis:
image: redis:7-alpine
container_name: timesheet-redis-dev
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redis_data:/data
- ./docker/redis/redis.conf:/usr/local/etc/redis/redis.conf
command: redis-server /usr/local/etc/redis/redis.conf
networks:
- timesheet-network
queue-worker:
build:
context: .
dockerfile: Dockerfile
target: development
container_name: timesheet-queue-dev
restart: unless-stopped
working_dir: /var/www/html
volumes:
- .:/var/www/html
environment:
- APP_ENV=development
- DB_HOST=mysql
- REDIS_HOST=redis
depends_on:
- mysql
- redis
command: php artisan queue:work --sleep=3 --tries=3 --max-time=3600
networks:
- timesheet-network
scheduler:
build:
context: .
dockerfile: Dockerfile
target: development
container_name: timesheet-scheduler-dev
restart: unless-stopped
working_dir: /var/www/html
volumes:
- .:/var/www/html
environment:
- APP_ENV=development
- DB_HOST=mysql
- REDIS_HOST=redis
depends_on:
- mysql
- redis
command: sh -c "while true; do php artisan schedule:run; sleep 60; done"
networks:
- timesheet-network
volumes:
mysql_data:
driver: local
redis_data:
driver: local
networks:
timesheet-network:
driver: bridge
Production Environment¶
# docker-compose.prod.yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
target: production
restart: unless-stopped
environment:
- APP_ENV=production
env_file:
- .env.production
volumes:
- storage_data:/var/www/html/storage
- bootstrap_cache:/var/www/html/bootstrap/cache
deploy:
replicas: 3
resources:
limits:
cpus: '1.0'
memory: 1G
reservations:
cpus: '0.5'
memory: 512M
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
networks:
- app-network
- db-network
healthcheck:
test: ["CMD", "php", "artisan", "health:check"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
nginx:
build:
context: .
dockerfile: docker/nginx/Dockerfile
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./public:/var/www/html/public:ro
- ./docker/nginx/sites/production.conf:/etc/nginx/conf.d/default.conf
- ssl_certificates:/etc/nginx/ssl:ro
depends_on:
- app
deploy:
replicas: 2
resources:
limits:
cpus: '0.5'
memory: 512M
networks:
- app-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
queue-worker:
build:
context: .
dockerfile: Dockerfile
target: production
restart: unless-stopped
environment:
- APP_ENV=production
env_file:
- .env.production
volumes:
- storage_data:/var/www/html/storage
command: php artisan queue:work --sleep=3 --tries=3 --max-time=3600 --memory=512
deploy:
replicas: 2
resources:
limits:
cpus: '0.5'
memory: 512M
networks:
- app-network
- db-network
healthcheck:
test: ["CMD", "php", "artisan", "queue:monitor"]
interval: 60s
timeout: 10s
retries: 3
scheduler:
build:
context: .
dockerfile: Dockerfile
target: production
restart: unless-stopped
environment:
- APP_ENV=production
env_file:
- .env.production
volumes:
- storage_data:/var/www/html/storage
command: sh -c "while true; do php artisan schedule:run; sleep 60; done"
deploy:
replicas: 1
resources:
limits:
cpus: '0.25'
memory: 256M
networks:
- app-network
- db-network
volumes:
storage_data:
external: true
bootstrap_cache:
external: true
ssl_certificates:
external: true
networks:
app-network:
external: true
db-network:
external: true
Container Configuration Files¶
Nginx Configuration¶
# docker/nginx/sites/production.conf
server {
listen 80;
server_name timesheet.stratpoint.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name timesheet.stratpoint.com;
root /var/www/html/public;
index index.php index.html;
# SSL Configuration
ssl_certificate /etc/nginx/ssl/timesheet.stratpoint.com.crt;
ssl_certificate_key /etc/nginx/ssl/timesheet.stratpoint.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# Security Headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Gzip Compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private must-revalidate auth;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/javascript;
# Rate Limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass app:9000;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_hide_header X-Powered-By;
# Increase timeouts for long-running requests
fastcgi_read_timeout 300;
fastcgi_send_timeout 300;
}
location /api/ {
limit_req zone=api burst=20 nodelay;
try_files $uri $uri/ /index.php?$query_string;
}
location /login {
limit_req zone=login burst=5 nodelay;
try_files $uri $uri/ /index.php?$query_string;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
location ~ /\.(?!well-known).* {
deny all;
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
MySQL Configuration¶
# docker/mysql/my.cnf
[mysqld]
# Basic Settings
default-storage-engine = innodb
sql_mode = STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
max_connections = 200
max_user_connections = 180
# Character Set
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
# InnoDB Settings
innodb_buffer_pool_size = 1G
innodb_log_file_size = 256M
innodb_log_buffer_size = 16M
innodb_flush_log_at_trx_commit = 2
innodb_file_per_table = 1
# Query Cache
query_cache_type = 1
query_cache_size = 128M
query_cache_limit = 2M
# Slow Query Log
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2
# Binary Logging
log-bin = mysql-bin
binlog_format = ROW
expire_logs_days = 7
# Performance Schema
performance_schema = ON
[mysql]
default-character-set = utf8mb4
[client]
default-character-set = utf8mb4
Redis Configuration¶
# docker/redis/redis.conf
# Network
bind 0.0.0.0
port 6379
timeout 300
tcp-keepalive 60
# General
daemonize no
supervised no
pidfile /var/run/redis_6379.pid
loglevel notice
logfile ""
# Snapshotting
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir /data
# Replication
replica-serve-stale-data yes
replica-read-only yes
# Security
requirepass ${REDIS_PASSWORD}
# Memory Management
maxmemory 512mb
maxmemory-policy allkeys-lru
# Append Only File
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
# Slow Log
slowlog-log-slower-than 10000
slowlog-max-len 128
Deployment Scripts¶
Production Deployment Script¶
#!/bin/bash
# scripts/deploy-production.sh
set -e
echo "Starting production deployment..."
# Configuration
COMPOSE_FILE="docker-compose.prod.yml"
ENV_FILE=".env.production"
BACKUP_DIR="/backups/$(date +%Y%m%d_%H%M%S)"
# Pre-deployment checks
echo "Performing pre-deployment checks..."
# Check if environment file exists
if [ ! -f "$ENV_FILE" ]; then
echo "Error: Environment file $ENV_FILE not found"
exit 1
fi
# Check if required environment variables are set
source "$ENV_FILE"
required_vars=("APP_KEY" "DB_PASSWORD" "REDIS_PASSWORD")
for var in "${required_vars[@]}"; do
if [ -z "${!var}" ]; then
echo "Error: Required environment variable $var is not set"
exit 1
fi
done
# Create backup
echo "Creating backup..."
mkdir -p "$BACKUP_DIR"
docker-compose -f "$COMPOSE_FILE" exec mysql mysqldump -u root -p"$DB_PASSWORD" stratpoint_timesheet > "$BACKUP_DIR/database.sql"
docker-compose -f "$COMPOSE_FILE" exec app tar -czf - storage/ > "$BACKUP_DIR/storage.tar.gz"
# Pull latest images
echo "Pulling latest images..."
docker-compose -f "$COMPOSE_FILE" pull
# Build application image
echo "Building application image..."
docker-compose -f "$COMPOSE_FILE" build --no-cache app
# Stop services gracefully
echo "Stopping services..."
docker-compose -f "$COMPOSE_FILE" down --timeout 30
# Start services
echo "Starting services..."
docker-compose -f "$COMPOSE_FILE" up -d
# Wait for services to be ready
echo "Waiting for services to be ready..."
sleep 30
# Run migrations
echo "Running database migrations..."
docker-compose -f "$COMPOSE_FILE" exec app php artisan migrate --force
# Clear and cache configuration
echo "Optimizing application..."
docker-compose -f "$COMPOSE_FILE" exec app php artisan config:cache
docker-compose -f "$COMPOSE_FILE" exec app php artisan route:cache
docker-compose -f "$COMPOSE_FILE" exec app php artisan view:cache
# Health check
echo "Performing health check..."
if curl -f http://localhost/health; then
echo "Deployment successful!"
else
echo "Health check failed. Rolling back..."
docker-compose -f "$COMPOSE_FILE" down
# Restore from backup
echo "Restoring from backup..."
# Add rollback logic here
exit 1
fi
echo "Production deployment completed successfully!"
Container Health Monitoring¶
#!/bin/bash
# scripts/monitor-containers.sh
# Monitor container health and restart if necessary
check_container_health() {
local container_name=$1
local health_status=$(docker inspect --format='{{.State.Health.Status}}' "$container_name" 2>/dev/null)
if [ "$health_status" = "unhealthy" ]; then
echo "Container $container_name is unhealthy. Restarting..."
docker restart "$container_name"
# Send alert
curl -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"Container $container_name restarted due to health check failure\"}" \
"$SLACK_WEBHOOK_URL"
fi
}
# Check all containers
containers=("timesheet-app" "timesheet-nginx" "timesheet-mysql" "timesheet-redis" "timesheet-queue")
for container in "${containers[@]}"; do
check_container_health "$container"
done
This comprehensive Docker deployment guide provides everything needed to deploy the Stratpoint Timesheet Application in containerized environments, from development to production, with proper security, monitoring, and maintenance procedures.