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 migrate on 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.

dockerfile
# 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:

nginx
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:

bash
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.

bash
# 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:

php
// 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:

env
CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
REDIS_HOST=your-elasticache-endpoint.cache.amazonaws.com
php
// 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):

json
{
  "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

bash
# 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

php
// 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:

ServiceCost/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.

✨

Topics

#cloud architect Nepal#AWS consultant Nepal#DevOps engineer Nepal#Laravel#Docker
πŸ’‘

Need Help With This?

Let's talk about how I can architect a scalable solution for your business.

Get In Touch β†’