How to Host a Laravel App on AWS EC2, RDS & S3 (2026 Guide)

Khimananda Oli 8 min read Database
How to Host a Laravel App on AWS EC2, RDS & S3 (2026 Guide)

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.

UsersbrowserCloudFrontCDN + HTTPSEC2Nginx + PHP-FPMRDS MySQLprivate subnetS3 bucketuploads + media
The AWS architecture for a Laravel app: CloudFront fronts an EC2 web server that reads and writes a private RDS MySQL database and stores media in 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 mysqld on 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.

VPC 10.0.0.0/16InternetgatewayPublic subnetEC2 websg-web: 80/443from anywherePrivate subnetRDS MySQLsg-db: 3306from sg-web only3306
A VPC boundary for the Laravel app: EC2 sits in a public subnet, RDS MySQL stays private, and its security group only accepts port 3306 from the web server's group.

Create two security groups and reference one from the other — this is cleaner than pinning private IP addresses:

  1. sg-web (on EC2): inbound 80 and 443 from 0.0.0.0/0, plus 22 from your office IP only.
  2. 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.

Controller$request->fileStorage::disk('s3')->put()Flysystem driverIAM roletemp credsno keys in .envS3 bucketobject + URLOne code path — swap the disk from 'local' to 's3' with no controller changes
How Laravel writes an upload to S3: the controller calls the 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.

Frequently Asked Questions

Yes. EC2 gives you a full Linux server where you install Nginx, PHP-FPM, and Composer and run Laravel exactly as you would on any VPS. The AWS advantage is pairing EC2 with managed RDS for the database and S3 for file storage so the server stays stateless and replaceable.

For a small production app, a t3.small (2 vCPU, 2 GB) is a sensible start; busier apps move to t3.medium or a compute-optimised c-family instance. Because compute is separated from data, you can resize the instance later without touching the database or storage.

No. Place RDS on a private subnet with public access disabled and allow inbound port 3306 only from the EC2 security group. Keeping the database off the public internet is the single most important security control in this architecture.

Set DB_HOST in .env to the RDS endpoint DNS name (never an IP), with DB_PORT 3306 and the master or app user credentials. Use the DNS name because Multi-AZ failover changes the underlying IP. Then run php artisan migrate --force from EC2 to confirm connectivity.

Files on the EC2 disk are lost if the instance is replaced and are invisible to any additional instances you add. S3 is durable, effectively unlimited, and shared across every server, so it is the correct home for user uploads, media, and generated documents.

Install league/flysystem-aws-s3-v3, set FILESYSTEM_DISK=s3, and provide AWS_DEFAULT_REGION and AWS_BUCKET in .env. Laravel already defines the s3 disk in config/filesystems.php, so no code changes are needed beyond calling Storage::disk('s3') or passing 's3' to store().

No, and you should not use them. Attach an IAM role to the EC2 instance and leave AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY blank. The AWS SDK automatically retrieves temporary credentials from the instance metadata, so no long-lived keys ever touch the server.

Grant only the actions the app uses — typically s3:GetObject, s3:PutObject, and s3:DeleteObject scoped to arn:aws:s3:::bucket/*, plus s3:ListBucket on the bucket ARN. Avoid s3:* and Resource "*". This limits blast radius if the instance is ever compromised.

Security groups are stateful virtual firewalls on each resource. The web group allows 80/443 from anywhere and SSH from your IP only; the database group allows 3306 solely from the web group. Referencing groups instead of IP ranges keeps rules correct as instances change.

The cleanest option is an Application Load Balancer with a free AWS Certificate Manager certificate that terminates TLS in front of EC2. For a single instance you can instead use Let's Encrypt via Certbot on the box. Redirect HTTP to HTTPS and set APP_URL to the https address.

An Application Load Balancer sits in front of one or more EC2 instances, terminates HTTPS, and health-checks targets. It becomes your scaling point — add instances behind it and traffic spreads automatically. It is optional for one server but recommended once you need redundancy or zero-downtime scaling.

Keep the bucket's public access fully blocked and store the files privately. Generate short-lived signed links with Storage::disk('s3')->temporaryUrl($path, now()->addMinutes(10)) so only authorised users can download them, and the URL expires quickly.

For production, yes. Multi-AZ keeps a synchronous standby in a second availability zone and fails over automatically during maintenance or hardware failure, usually within a minute or two. It roughly doubles the database cost but removes a major single point of failure.

Keep it out of version control. Store it in AWS Secrets Manager or SSM Parameter Store and inject it at deploy time, or place it only in the server's .env with restricted file permissions. Use a long random password and rotate it periodically.

Yes. CloudFront caches static assets and public S3 media at edge locations close to users and serves them over HTTPS on your own domain. It reduces load on EC2 and improves latency for global visitors, which matters for audiences spread across Nepal and abroad.