Beyond the Basics: Crafting a Resilient and Scalable Laravel Application with Docker, AWS ECS, and Advanced CI/CD Pipelines
Dockerizing the Laravel Application
To achieve a consistent development and deployment environment, we’ll containerize our Laravel application using Docker. This involves creating a Dockerfile that defines the image for our application. We’ll leverage multi-stage builds to keep our production image lean.
Dockerfile for Laravel
This Dockerfile assumes a typical Laravel setup, including PHP, Composer, and Nginx as the web server. We’ll use an official PHP image as our base and install necessary extensions.
# Stage 1: Build dependencies
FROM php:8.2-fpm as builder
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
git \
unzip \
libzip-dev \
libpng-dev \
libjpeg-dev \
libfreetype6-dev \
libonig-dev \
libxml2-dev \
zip \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) gd \
&& docker-php-ext-install pdo pdo_mysql zip exif pcntl opcache
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Copy application files and install dependencies
COPY . .
RUN composer install --no-dev --optimize-autoloader
# Stage 2: Production image
FROM php:8.2-fpm-alpine
WORKDIR /app
# Install system dependencies for production
RUN apk update && apk add --no-cache \
libzip \
libpng \
libjpeg-turbo \
freetype \
oniguruma \
libxml2 \
nginx \
supervisor
# Copy PHP extensions from builder stage
COPY --from=builder /usr/local/lib/php/extensions/no-debug-non-zts-20220829/ /usr/local/lib/php/extensions/no-debug-non-zts-20220829/
# Copy Composer dependencies from builder stage
COPY --from=builder /app/vendor /app/vendor
# Copy application code from builder stage
COPY --from=builder /app /app
# Configure Nginx
COPY docker/nginx/default.conf /etc/nginx/conf.d/default.conf
# Configure Supervisor
COPY docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# Expose port and set entrypoint
EXPOSE 80
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
Nginx Configuration
We need an Nginx configuration that serves our Laravel application. This configuration will proxy requests to the PHP-FPM process.
server {
listen 80;
index index.php index.html;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /app/public;
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass php-fpm:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
location / {
try_files $uri $uri/ /index.php?$query_string;
}
}
Supervisor Configuration
Supervisor will manage our PHP-FPM and potentially other background processes (like queue workers). This ensures they restart if they crash.
[supervisord] nodaemon=true user=root [program:php-fpm] command=/usr/local/sbin/php-fpm --nodaemonize autostart=true autorestart=true user=www-data stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 # Uncomment and configure if you have queue workers #[program:laravel-queue] #command=php artisan queue:work --tries=3 --timeout=60 #autostart=true #autorestart=true #user=www-data #stdout_logfile=/dev/stdout #stdout_logfile_maxbytes=0 #stderr_logfile=/dev/stderr #stderr_logfile_maxbytes=0
Docker Compose for Local Development
For local development, docker-compose.yml simplifies the orchestration of multiple services (app, database, etc.).
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "8000:80"
volumes:
- .:/app
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf # Overriding for local dev convenience
depends_on:
- db
networks:
- app-network
db:
image: mysql:8.0
ports:
- "3306:3306"
volumes:
- db_data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: laravel_db
MYSQL_USER: user
MYSQL_PASSWORD: password
networks:
- app-network
networks:
app-network:
driver: bridge
volumes:
db_data:
AWS ECS Deployment Strategy
Amazon Elastic Container Service (ECS) is our chosen platform for orchestrating Docker containers. We’ll define task definitions and services to manage our application’s lifecycle.
ECS Task Definition
The task definition describes how to run your application, including the Docker image to use, CPU and memory requirements, and port mappings. We’ll use a JSON format for this.
{
"family": "laravel-app",
"networkMode": "awsvpc",
"requiresCompatibilities": [
"FARGATE"
],
"cpu": "1024",
"memory": "2048",
"executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
"taskRoleArn": "arn:aws:iam::123456789012:role/laravelAppTaskRole",
"containerDefinitions": [
{
"name": "laravel-app-container",
"image": "YOUR_ECR_REPOSITORY_URI:latest",
"portMappings": [
{
"containerPort": 80,
"hostPort": 80,
"protocol": "tcp"
}
],
"environment": [
{
"name": "APP_ENV",
"value": "production"
},
{
"name": "APP_URL",
"value": "https://your-domain.com"
},
{
"name": "DB_HOST",
"value": "your-rds-endpoint.rds.amazonaws.com"
},
{
"name": "DB_PORT",
"value": "3306"
},
{
"name": "DB_DATABASE",
"value": "laravel_prod_db"
},
{
"name": "DB_USERNAME",
"value": "admin"
},
{
"name": "DB_PASSWORD",
"value": "your-db-password"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/laravel-app",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "ecs"
}
},
"essential": true
}
]
}
Note: Replace placeholders like YOUR_ECR_REPOSITORY_URI, IAM role ARNs, and database credentials with your actual values. The executionRoleArn is for ECS agent permissions, and taskRoleArn is for the application’s AWS service access.
ECS Service and Load Balancer Integration
An ECS Service maintains a specified number of instances of a task definition simultaneously. We’ll integrate it with an Application Load Balancer (ALB) for external traffic management and SSL termination.
- VPC and Subnets: Ensure your ECS cluster is configured within a VPC with public and private subnets.
- Security Groups: Create security groups for the ALB (allowing HTTP/HTTPS from anywhere) and the ECS tasks (allowing traffic from the ALB’s security group on port 80).
- Application Load Balancer (ALB): Create an ALB with a listener for port 80 (and 443 for HTTPS).
- Target Group: Configure a target group pointing to your ECS service on port 80.
- ECS Service: Create an ECS service using the task definition. Configure it to use the ALB and the target group. Set desired count to 2 or more for high availability. Use a Fargate launch type for managed infrastructure.
Advanced CI/CD Pipeline with GitHub Actions
A robust CI/CD pipeline is crucial for automating builds, tests, and deployments. We’ll use GitHub Actions for this.
GitHub Actions Workflow (.github/workflows/deploy.yml)
This workflow will build the Docker image, push it to Amazon Elastic Container Registry (ECR), and then update the ECS service.
name: Laravel CI/CD
on:
push:
branches:
- main
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Build, tag, and push Docker image to ECR
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: laravel-app # Your ECR repository name
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
- name: Update ECS Service
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: task-definition.json # Path to your task definition file
service: laravel-app-service # Your ECS service name
cluster: laravel-app-cluster # Your ECS cluster name
wait-for-service-stability: true
environment-variables: |
APP_ENV=production
APP_URL=https://your-domain.com
DB_HOST=your-rds-endpoint.rds.amazonaws.com
DB_PORT=3306
DB_DATABASE=laravel_prod_db
DB_USERNAME=admin
DB_PASSWORD=${{ secrets.DB_PASSWORD }}
# You might need to pass image URI if not using task-definition.json directly
# image: ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }}
Important Considerations for CI/CD:
- AWS Credentials: Store your AWS Access Key ID and Secret Access Key as GitHub secrets.
- ECR Repository: Ensure you have an ECR repository created beforehand. The workflow assumes it exists.
- Task Definition File: The
task-definition.jsonshould be committed to your repository. You might need to dynamically update the image URI within this file before deploying, or use theimageparameter in theamazon-ecs-deploy-task-definitionaction if your task definition doesn’t have the image hardcoded. - Secrets Management: Sensitive information like database passwords should be stored as GitHub secrets and injected into the task definition environment variables during deployment.
- Database Migrations: You’ll need a strategy for running database migrations. This could be a separate job in your CI/CD pipeline that runs against a staging environment, or a manual step before a production deployment. Alternatively, you could have a sidecar container in your ECS task that runs migrations on startup (with careful handling to avoid race conditions).
Resilience and Scalability Enhancements
Beyond the core setup, several architectural patterns enhance resilience and scalability.
Database Scaling
For production, use Amazon RDS (Relational Database Service) with Multi-AZ deployments for high availability. Consider read replicas for read-heavy workloads. For extreme scalability, explore Aurora Serverless or managed NoSQL solutions like DynamoDB if your data model permits.
Caching Strategies
Implement caching at various levels:
- Application Cache: Use Redis (e.g., Amazon ElastiCache for Redis) for caching query results, computed data, and full page responses. Configure Laravel’s cache driver to use Redis.
- CDN: Utilize a Content Delivery Network (CDN) like Amazon CloudFront for serving static assets (CSS, JS, images) to reduce load on your application servers.
Queueing for Background Jobs
Offload time-consuming tasks (email sending, image processing, API calls) to background queues. Use Redis or SQS as your queue driver. Ensure your ECS service is configured to run queue worker tasks, or deploy them as separate ECS services.
// config/queue.php
'default' => env('QUEUE_DRIVER', 'redis'),
'connections' => [
// ... other connections
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => 90,
],
'sqs' => [
'driver' => 'sqs',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'queue' => env('AWS_SQS_QUEUE_URL'),
'after_commit' => env('QUEUE_AFTER_COMMIT', false),
],
],
Health Checks and Monitoring
Configure health check endpoints in your Laravel application and integrate them with your load balancer and ECS service. Implement comprehensive monitoring using AWS CloudWatch, Prometheus, or other tools to track application performance, error rates, and resource utilization.
Stateless Application Design
Ensure your Laravel application is stateless. Store session data, user uploads, and any other stateful information in external services like Redis, S3, or a database. This is critical for horizontal scaling, as any instance can handle any request.