Dockerizing and Orchestrating Legacy Laravel Systems on Modern AWS Infrastructure
Containerizing the Legacy Laravel Application
The first step in modernizing a legacy Laravel application for AWS is to containerize it. This involves creating a Dockerfile that accurately reflects the application’s runtime dependencies and build process. For older Laravel versions, common challenges include specific PHP version requirements, non-standard extensions, and manual dependency management.
Let’s assume a hypothetical legacy Laravel 5.x application with PHP 7.2, Redis, and a MySQL database. The Dockerfile will need to install these components and configure the web server (e.g., Nginx).
Dockerfile for Laravel 5.x
# Use an official PHP runtime as a parent image
FROM php:7.2-fpm
# Set the working directory in the container
WORKDIR /var/www/html
# Install system dependencies
RUN apt-get update && apt-get install -y \
git \
unzip \
libzip-dev \
libpng-dev \
libjpeg-dev \
libfreetype6-dev \
libssl-dev \
libonig-dev \
libxml2-dev \
zip \
&& rm -rf /var/lib/apt/lists/*
# Install PHP extensions
RUN docker-php-ext-configure gd --with-freetype-dir=/usr --with-jpeg-dir=/usr \
&& docker-php-ext-install -j$(nproc) gd \
&& docker-php-ext-install pdo pdo_mysql zip exif pcntl opcache sockets \
&& pecl install redis \
&& docker-php-ext-enable redis
# Install Composer
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
# Copy the application code
COPY . /var/www/html
# Install Composer dependencies
RUN composer install --no-dev --prefer-dist --optimize-autoloader
# Configure PHP-FPM
COPY docker/php-fpm/zz-laravel.ini /usr/local/etc/php-fpm.d/zz-laravel.ini
COPY docker/php-fpm/www.conf /usr/local/etc/php-fpm.d/www.conf
# Expose port 9000 and start php-fpm server
EXPOSE 9000
CMD ["php-fpm"]
The accompanying zz-laravel.ini might look like this, optimizing for production:
memory_limit = 256M upload_max_filesize = 64M post_max_size = 64M max_execution_time = 300 date.timezone = UTC error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT display_errors = Off log_errors = On error_log = /var/log/php/error.log session.save_path = /var/lib/php/sessions opcache.enable=1 opcache.memory_consumption=128 opcache.interned_strings_buffer=8 opcache.max_accelerated_files=10000 opcache.revalidate_freq=2 opcache.validate_timestamps=0 opcache.fast_shutdown=1
And the www.conf for PHP-FPM pool configuration:
[www] user = www-data group = www-data listen = /var/run/php/php7.2-fpm.sock listen.owner = www-data listen.group = www-data listen.mode = 0660 pm = dynamic pm.max_children = 50 pm.min_spare_servers = 5 pm.max_spare_servers = 10 pm.start_servers = 2 pm.slowlog_timeout = 30s pm.request_terminate_timeout = 120s pm.process_idle_timeout = 10s request_terminate_timeout = 120s request_slowlog_timeout = 30s catch_workers_output = yes clear_env = no env[PATH] = /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin env[PHPRC] = /usr/local/etc/php/conf.d/zz-laravel.ini
Next, we need a web server configuration. Nginx is a common choice for serving Laravel applications.
Nginx Configuration for Laravel
server {
listen 80;
server_name your-legacy-app.com;
root /var/www/html/public;
index index.php index.html index.htm;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass php:9000; # Assuming a service named 'php' in docker-compose
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.ht {
deny all;
}
# Optional: Caching for static assets
location ~* \.(css|js|jpg|jpeg|gif|png|ico|svg|webp)$ {
expires 1y;
add_header Cache-Control "public";
}
}
To tie these together for local development and testing, a docker-compose.yml file is essential.
Docker Compose for Local Orchestration
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: legacy_laravel_app
ports:
- "8000:80" # Map host port 8000 to container port 80
volumes:
- .:/var/www/html # Mount current directory for development
- ./docker/php/custom.ini:/usr/local/etc/php/conf.d/custom.ini # Optional custom php.ini
depends_on:
- db
- redis
environment:
APP_ENV: local
APP_DEBUG: true
DB_HOST: db
DB_PORT: 3306
DB_DATABASE: legacy_db
DB_USERNAME: user
DB_PASSWORD: password
REDIS_HOST: redis
REDIS_PORT: 6379
nginx:
image: nginx:alpine
container_name: legacy_laravel_nginx
ports:
- "80:80"
volumes:
- .:/var/www/html # Mount application code
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf # Mount Nginx config
depends_on:
- app
db:
image: mysql:5.7
container_name: legacy_laravel_db
restart: always
environment:
MYSQL_ROOT_PASSWORD: root_password
MYSQL_DATABASE: legacy_db
MYSQL_USER: user
MYSQL_PASSWORD: password
ports:
- "3306:3306"
volumes:
- db_data:/var/lib/mysql
redis:
image: redis:alpine
container_name: legacy_laravel_redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
volumes:
db_data:
redis_data:
With these files in place, you can build and run the application locally using docker-compose up --build. This provides a self-contained environment for development and initial testing.
Migrating to AWS Infrastructure
Once the application is containerized, we can leverage AWS services for orchestration and scaling. For legacy applications, a common and robust approach is to use Amazon Elastic Container Service (ECS) with the EC2 launch type. This offers more control over the underlying instances compared to Fargate, which can be beneficial for applications with specific resource requirements or complex networking needs.
Setting up ECS Cluster and Task Definitions
First, we need to push our Docker image to Amazon Elastic Container Registry (ECR). Assuming you have the AWS CLI configured:
# Create an ECR repository aws ecr create-repository --repository-name legacy-laravel-app # Authenticate Docker to your ECR registry aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin [YOUR_AWS_ACCOUNT_ID].dkr.ecr.us-east-1.amazonaws.com # Build your Docker image (ensure you are in the project root) docker build -t legacy-laravel-app . # Tag the image for ECR docker tag legacy-laravel-app:latest [YOUR_AWS_ACCOUNT_ID].dkr.ecr.us-east-1.amazonaws.com/legacy-laravel-app:latest # Push the image to ECR docker push [YOUR_AWS_ACCOUNT_ID].dkr.ecr.us-east-1.amazonaws.com/legacy-laravel-app:latest
Next, create an ECS Cluster. This can be done via the AWS Console or AWS CLI. We’ll choose the EC2 launch type for more control.
Then, define an ECS Task Definition. This JSON document describes your application’s containers, including the Docker image to use, CPU and memory requirements, environment variables, and port mappings. For our legacy Laravel app, we’ll need at least two containers: one for the Laravel application (PHP-FPM) and one for Nginx.
{
"family": "legacy-laravel-task",
"networkMode": "awsvpc",
"requiresCompatibilities": [
"EC2"
],
"cpu": "1024",
"memory": "2048",
"executionRoleArn": "arn:aws:iam::[YOUR_AWS_ACCOUNT_ID]:role/ecsTaskExecutionRole",
"containerDefinitions": [
{
"name": "laravel-app",
"image": "[YOUR_AWS_ACCOUNT_ID].dkr.ecr.us-east-1.amazonaws.com/legacy-laravel-app:latest",
"cpu": 512,
"memory": 1024,
"essential": true,
"portMappings": [
{
"containerPort": 9000,
"protocol": "tcp"
}
],
"environment": [
{
"name": "APP_ENV",
"value": "production"
},
{
"name": "APP_DEBUG",
"value": "false"
},
{
"name": "DB_HOST",
"value": "your-rds-endpoint.rds.amazonaws.com"
},
{
"name": "DB_PORT",
"value": "3306"
},
{
"name": "DB_DATABASE",
"value": "legacy_db"
},
{
"name": "DB_USERNAME",
"value": "admin"
},
{
"name": "DB_PASSWORD",
"value": "your_db_password"
},
{
"name": "REDIS_HOST",
"value": "your-elasticache-redis-endpoint.cache.amazonaws.com"
},
{
"name": "REDIS_PORT",
"value": "6379"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/legacy-laravel-task",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "laravel-app"
}
}
},
{
"name": "nginx",
"image": "nginx:alpine",
"cpu": 512,
"memory": 512,
"essential": true,
"portMappings": [
{
"containerPort": 80,
"protocol": "tcp"
}
],
"mountPoints": [
{
"sourceVolume": "app-code",
"containerPath": "/var/www/html"
},
{
"sourceVolume": "nginx-config",
"containerPath": "/etc/nginx/conf.d/default.conf"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/legacy-laravel-task",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "nginx"
}
}
}
],
"volumes": [
{
"name": "app-code",
"host": {
"sourcePath": "/path/to/your/app/code"
}
},
{
"name": "nginx-config",
"host": {
"sourcePath": "/path/to/your/nginx/config/default.conf"
}
}
]
}
Note: For EC2 launch type, the volumes section in the task definition refers to paths on the EC2 host. A more robust approach for production would be to use Amazon Elastic File System (EFS) for shared code or to bake the code directly into the Docker image and avoid host volume mounts for production deployments.
The ecsTaskExecutionRole needs permissions to pull images from ECR and send logs to CloudWatch Logs. The Nginx container will need access to the application code. In a production EC2 setup, this often involves provisioning EC2 instances with an IAM role that allows them to access EFS or using a shared volume strategy. For simplicity in this example, we’re referencing host paths, which is more typical for development or simpler setups.
Database and Cache Services
For the database, Amazon Relational Database Service (RDS) is the standard choice. Provision a MySQL instance (e.g., MySQL 5.7 or 8.0) and configure its security group to allow inbound traffic from your ECS cluster’s security group on port 3306. For caching, Amazon ElastiCache for Redis is ideal. Configure its security group similarly.
Ensure your ECS task definition’s environment variables correctly point to the RDS and ElastiCache endpoints.
Setting up Load Balancing and Service Discovery
To distribute traffic to your ECS tasks and provide a stable endpoint, an Application Load Balancer (ALB) is recommended. Create an ALB with a listener on port 80 (and optionally 443 for HTTPS). Configure a Target Group that points to your ECS cluster and the port your Nginx container is exposing (port 80 in our case).
Create an ECS Service. This service will manage the desired number of tasks running your task definition. Configure the service to use the ALB’s Target Group. The service will automatically register and deregister tasks with the ALB as they are launched or stopped.
For service discovery within the cluster (e.g., if other microservices needed to talk to your Laravel app), ECS provides built-in service discovery using AWS Cloud Map, or you can rely on DNS resolution if using the awsvpc network mode and assigning ENIs to tasks.
Deployment Strategies and CI/CD
For production deployments, manual pushes to ECR and updates to ECS task definitions are not scalable. Implement a CI/CD pipeline using AWS CodePipeline, AWS CodeBuild, and AWS CodeDeploy (or a third-party tool like GitLab CI, GitHub Actions, Jenkins).
- CodeCommit/GitHub/Bitbucket: Source code repository.
- CodeBuild: Builds the Docker image, pushes it to ECR, and can update the ECS task definition.
- CodePipeline: Orchestrates the build and deployment process.
- CodeDeploy: Manages the deployment of new task definitions to the ECS service, allowing for rolling updates or blue/green deployments.
A typical pipeline would trigger on a code commit, build the Docker image, tag it with a unique identifier (e.g., Git commit hash or build number), push to ECR, and then trigger a deployment to ECS. CodeDeploy can be configured to perform rolling updates, replacing old tasks with new ones gradually to minimize downtime.
Monitoring and Logging
Leverage AWS CloudWatch for monitoring and logging. Ensure your task definitions are configured with logConfiguration to send container logs to CloudWatch Logs. Create CloudWatch Alarms based on metrics like CPU utilization, memory utilization, request counts, and error rates from your ALB and ECS tasks. This is crucial for identifying performance bottlenecks and application errors in a production environment.
For legacy applications, specific error patterns or performance issues might only surface under load. Comprehensive logging and monitoring are key to diagnosing these issues in the containerized environment.