
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Running Laravel on a single VPS is fine until traffic grows, the disk fills with user uploads, or one bad reboot takes your database down with your web server. Learning how to host a Laravel app on AWS EC2, RDS and S3 splits those concerns onto managed services: a stateless compute layer, a managed MySQL database, and durable object storage for files. This guide walks the full build — launch the instance, wire the managed database, offload media to S3, and lock it all down with security groups and least-privilege IAM. If you would rather hand it off, the AWS and DevOps deployment services cover the same architecture end to end.
s3 filesystem driver. Security groups control traffic, and an IAM role grants EC2 least-privilege access to S3.What AWS services do you need to host a Laravel app?
You can host Laravel on AWS with three core services and a few supporting ones. Keeping compute, data, and files on separate managed layers is what makes the setup scale and survive failures:
- EC2 — a virtual server running Nginx, PHP-FPM, and your Laravel code. This is the stateless compute layer; treat it as replaceable.
- RDS for MySQL — a managed database with automated backups, patching, and point-in-time recovery, so you never run
mysqldon the web box yourself. - S3 — durable object storage for user uploads, generated PDFs, and public media, addressed through Laravel's filesystem abstraction.
- IAM — an instance role that grants EC2 scoped access to the S3 bucket without any hard-coded keys.
- VPC, security groups, and (optionally) an Application Load Balancer with an ACM TLS certificate — the network boundary and HTTPS termination.
How do you launch and configure the EC2 instance for Laravel?
Start with a t3.small (2 vCPU, 2 GB) running Ubuntu 24.04 LTS for a small production app; scale the instance type later without re-architecting. Launch it into a public subnet, attach a security group that allows only SSH from your IP and HTTP/HTTPS, and assign an Elastic IP so the address survives a stop/start. Then install the runtime — PHP 8.4, Nginx, and Composer — the same stack you would run on any Ubuntu server:
sudo apt update && sudo apt upgrade -y
sudo apt install -y nginx php8.4-fpm php8.4-cli php8.4-mysql \
php8.4-mbstring php8.4-xml php8.4-curl php8.4-zip php8.4-bcmath \
php8.4-gd unzip git
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer
cd /var/www
sudo git clone [email protected]:you/your-app.git app
cd app
composer install --no-dev --optimize-autoloader
cp .env.example .env
php artisan key:generate Point an Nginx server block at /var/www/app/public, set the web user to www-data, and give it ownership of storage and bootstrap/cache. If you have already deployed Laravel on a plain server, this part is identical — the AWS-specific work is the database and storage that follow. For a step-by-step CI/CD flow that pushes releases to this instance automatically, see the GitLab CI/CD pipeline for Laravel guide.
How do you connect Laravel to a managed RDS MySQL database?
Create an RDS for MySQL 8.0 instance and — this is the key security decision — place it on a private subnet with no public access. The database should only be reachable from the EC2 instance, never from the open internet. You control that with security groups, not IP allow-lists.
Create two security groups and reference one from the other — this is cleaner than pinning private IP addresses:
- sg-web (on EC2): inbound 80 and 443 from
0.0.0.0/0, plus 22 from your office IP only. - sg-db (on RDS): inbound 3306 with the source set to
sg-web, so only the web tier can reach MySQL.
Then point Laravel at the RDS endpoint in .env. Use the RDS DNS name — never an IP — because failover to a standby changes the underlying address:
DB_CONNECTION=mysql
DB_HOST=your-app-db.abc123xyz.ap-south-1.rds.amazonaws.com
DB_PORT=3306
DB_DATABASE=your_app
DB_USERNAME=app_user
DB_PASSWORD=use-a-long-random-secret Run php artisan migrate --force from EC2 to verify connectivity. For production resilience, enable Multi-AZ so RDS keeps a synchronous standby in a second availability zone and fails over automatically. Store the database password in AWS Secrets Manager or SSM Parameter Store rather than committing it, and keep automated backups at seven days or more.
How do you configure Laravel to store uploads on S3?
Keeping files on the EC2 disk breaks the moment you add a second instance or replace the box — the uploads live on one server only. S3 fixes that: every instance reads and writes the same durable bucket. Laravel ships an s3 filesystem driver, so you change configuration, not application code.
s3 disk, Flysystem signs the request with the EC2 IAM role, and the object lands in the bucket.First install the driver package, then configure the disk:
composer require league/flysystem-aws-s3-v3 "^3.0" Laravel already defines the s3 disk in config/filesystems.php; you only supply the environment values. Because EC2 carries an IAM role, you deliberately leave the key and secret blank — the SDK pulls temporary credentials from the instance metadata automatically:
FILESYSTEM_DISK=s3
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=ap-south-1
AWS_BUCKET=your-app-media
AWS_USE_PATH_STYLE_ENDPOINT=false Now storing an upload is one line, and the same call works whether the file lands on local disk in development or S3 in production:
$path = $request->file('avatar')->store('avatars', 's3');
$url = Storage::disk('s3')->url($path); For private files (invoices, user documents), keep the bucket blocked from public access and generate short-lived signed URLs with Storage::disk('s3')->temporaryUrl($path, now()->addMinutes(10)). For public assets, front the bucket with CloudFront so files are cached at the edge and served over HTTPS on your own domain.
How do you apply IAM least-privilege and HTTPS on AWS?
Never bake long-lived access keys into .env on a server. Instead, attach an IAM role to the EC2 instance and grant it only the S3 actions the app actually uses, scoped to the one bucket. A least-privilege policy looks like this:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
"Resource": "arn:aws:s3:::your-app-media/*"
},
{
"Effect": "Allow",
"Action": ["s3:ListBucket"],
"Resource": "arn:aws:s3:::your-app-media"
}
]
} Notice the two-part resource: object actions target bucket/*, while ListBucket targets the bucket ARN itself. Grant nothing broader — no s3:*, no Resource: "*". For HTTPS, the cleanest path is an Application Load Balancer in front of EC2 with a free TLS certificate from AWS Certificate Manager; the ALB terminates TLS and also becomes your scaling point when you add more instances behind it. For a single-instance start, you can instead terminate TLS on the box with a Let's Encrypt certificate via Certbot and move to an ALB later. Either way, redirect all HTTP traffic to HTTPS and set APP_URL to the https:// address so Laravel generates correct absolute URLs.
Conclusion
Hosting a Laravel app on AWS EC2, RDS and S3 gives you a clean separation that a single VPS cannot: replaceable compute, a managed database that backs itself up, and durable file storage that any number of instances can share. Build it in that order — EC2 first, then lock RDS behind security groups, then move uploads to S3 with an IAM role — and you have an architecture that scales by changing an instance type or adding a load balancer, not by rebuilding. If you want this deployed, secured, and cost-tuned for your workload, get in touch or browse the AWS deployment case studies for how it looks in production.