Skip to content

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.