As a cloud architect and AWS consultant based in Nepal, I have migrated over a dozen Laravel applications from single VPS servers to high-availability AWS architectures. The trigger is always the same: the app works fine at 100 users, then crashes at 1,000. A single server becomes a single point of failure, deployments cause downtime, and the database melts under load.
This is the exact blueprint I follow β not theory, but a working architecture you can implement step by step.
β The Problem with Single-Server Deployments
When you run Laravel on a single VPS, you have:
- One point of failure β if the server goes down, your entire app is offline
- Deployment downtime β running
git pull && php artisan migrateon a live server creates a window of unavailability - No horizontal scaling β a traffic spike cannot be handled by adding servers
- Database and app competing for the same CPU and RAM
β The Architecture Blueprint
User Request
β Route 53 (DNS)
β CloudFront (CDN β caches static assets globally)
β Application Load Balancer (distributes traffic across containers)
β ECS Task (App) | ECS Task (App) β multiple tasks run simultaneously
β RDS Aurora MySQL (writer + reader endpoints)
β ElastiCache Redis (sessions, cache, queues)
β S3 Bucket (user uploads, media files) Every component is a managed AWS service. You do not manage servers, install patches, or worry about disk space.
β Step 1: Dockerize Your Laravel Application
Docker makes your application run identically everywhere β your laptop, test server, and production.
# Stage 1: Build dependencies
FROM php:8.3-fpm-alpine AS builder
RUN apk add --no-cache git curl libpng-dev libzip-dev zip unzip
RUN docker-php-ext-install pdo_mysql opcache zip gd bcmath
RUN pecl install redis && docker-php-ext-enable redis
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist
COPY . .
RUN composer dump-autoload --optimize
# Stage 2: Final image (smaller β no build tools)
FROM php:8.3-fpm-alpine
RUN apk add --no-cache nginx supervisor
COPY --from=builder /usr/local/lib/php/extensions /usr/local/lib/php/extensions
COPY --from=builder /usr/local/etc/php/conf.d /usr/local/etc/php/conf.d
COPY --from=builder /app /var/www/html
COPY docker/nginx.conf /etc/nginx/nginx.conf
COPY docker/supervisord.conf /etc/supervisord.conf
RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache
# Pre-warm Laravel caches at build time
RUN php artisan config:cache && php artisan route:cache && php artisan view:cache
EXPOSE 80
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"] The docker/nginx.conf for PHP-FPM:
events { worker_processes auto; }
http {
include mime.types;
server {
listen 80;
root /var/www/html/public;
index index.php;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ .php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /.(?!well-known).* { deny all; }
}
} Test locally before pushing to AWS:
docker build -t laravel-app:local .
docker run -p 8080:80 --env-file .env laravel-app:local β Step 2: Push to AWS Elastic Container Registry (ECR)
ECR is AWS's private Docker registry. Your ECS tasks pull images from here.
# Authenticate Docker to ECR
aws ecr get-login-password --region ap-south-1 | docker login --username AWS --password-stdin 123456789.dkr.ecr.ap-south-1.amazonaws.com
# Create the repository (one-time setup)
aws ecr create-repository --repository-name laravel-app --region ap-south-1
# Build and push
docker build -t laravel-app .
docker tag laravel-app:latest 123456789.dkr.ecr.ap-south-1.amazonaws.com/laravel-app:latest
docker push 123456789.dkr.ecr.ap-south-1.amazonaws.com/laravel-app:latest Use ap-south-1 (Mumbai) as your primary region β it is the closest AWS region to Nepal with the lowest latency for Nepali users.
β Step 3: RDS Aurora β Managed Database with Automatic Failover
Never run your own MySQL on EC2. Amazon RDS Aurora is a MySQL-compatible database with automatic backups, multi-AZ failover, and read replicas.
Configure Laravel to use separate read/write connections. This distributes load automatically β writes go to the primary, reads (typically 80% of traffic) go to the replica:
// config/database.php
'mysql' => [
'driver' => 'mysql',
'write' => ['host' => env('DB_WRITE_HOST')], // Aurora writer endpoint
'read' => [['host' => env('DB_READ_HOST')]], // Aurora reader endpoint
'sticky' => true, // After a write, reads in the same request use write connection
'database' => env('DB_DATABASE'),
'username' => env('DB_USERNAME'),
'password' => env('DB_PASSWORD'),
'charset' => 'utf8mb4',
'options' => [
PDO::MYSQL_ATTR_SSL_CA => '/etc/ssl/certs/ca-certificates.crt',
],
], β Step 4: ElastiCache Redis
ElastiCache for Redis replaces self-managed Redis. Use it for sessions (so users stay logged in across multiple containers), caching, and job queues:
CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
REDIS_HOST=your-elasticache-endpoint.cache.amazonaws.com // config/database.php β separate Redis databases for each concern
'redis' => [
'client' => 'phpredis',
'default' => ['host' => env('REDIS_HOST'), 'database' => '0'],
'cache' => ['host' => env('REDIS_HOST'), 'database' => '1'],
'sessions' => ['host' => env('REDIS_HOST'), 'database' => '2'],
], Separating Redis databases means you can flush the cache without wiping sessions or queued jobs.
β Step 5: ECS Fargate β Serverless Containers
ECS Fargate runs your Docker containers without any EC2 instances to manage.
ECS Task Definition (the blueprint for your container):
{
"family": "laravel-app",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "512",
"memory": "1024",
"containerDefinitions": [{
"name": "app",
"image": "123456789.dkr.ecr.ap-south-1.amazonaws.com/laravel-app:latest",
"portMappings": [{ "containerPort": 80 }],
"secrets": [
{ "name": "APP_KEY", "valueFrom": "arn:aws:ssm:...:parameter/laravel/app-key" },
{ "name": "DB_PASSWORD", "valueFrom": "arn:aws:ssm:...:parameter/laravel/db-password" }
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/laravel-app",
"awslogs-region": "ap-south-1",
"awslogs-stream-prefix": "app"
}
},
"healthCheck": {
"command": ["CMD-SHELL", "curl -f http://localhost/health || exit 1"],
"interval": 30,
"timeout": 5,
"retries": 3
}
}]
} Secrets are fetched from AWS Systems Manager Parameter Store at container start β they never appear in plaintext.
β Step 6: Auto Scaling
# Scale based on CPU utilisation
aws application-autoscaling put-scaling-policy --service-namespace ecs --resource-id service/production/laravel-app --scalable-dimension ecs:service:DesiredCount --policy-name scale-on-cpu --policy-type TargetTrackingScaling --target-tracking-scaling-policy-configuration '{
"TargetValue": 70.0,
"PredefinedMetricSpecification": {
"PredefinedMetricType": "ECSServiceAverageCPUUtilization"
},
"ScaleInCooldown": 300,
"ScaleOutCooldown": 60
}' ECS maintains a minimum of 2 tasks (for redundancy) and scales up to 10 during traffic spikes. With 2 tasks minimum, if one task crashes, users are immediately served by the second β zero downtime.
β Step 7: S3 + CloudFront for Media
// config/filesystems.php
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'ap-south-1'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'), // CloudFront distribution URL
],
// Uploading
$path = $request->file('avatar')->store('avatars', 's3');
// Secure temporary URL (expires in 60 minutes)
$url = Storage::disk('s3')->temporaryUrl($user->avatar, now()->addHour()); Set FILESYSTEM_DISK=s3 in production. Files survive container restarts, are globally cached via CloudFront CDN, and scale infinitely.
β Monthly Cost for Nepal-Based Projects
For a typical SaaS or marketplace serving Nepal + South Asia:
| Service | Cost/month |
|---|---|
| ECS Fargate (2 tasks, 0.5 vCPU/1GB) | ~$22 |
| RDS Aurora Serverless v2 | ~$35-50 |
| ElastiCache (cache.t4g.micro) | ~$13 |
| Application Load Balancer | ~$18 |
| S3 + CloudFront (50GB + 100GB transfer) | ~$8 |
| Route 53 | ~$1 |
Total: ~$97-112/month for a fully managed, auto-scaling, high-availability production stack.
Need Help With This?
Let's talk about how I can architect a scalable solution for your business.
Get In Touch β