
August 15, 2026
13 min read
By Kokil Thapa | Last reviewed: August 2026
Deploying Laravel on AWS EC2 with RDS means splitting the app and the database into two managed services and keeping them on a private network. The catch is coordination: VPC routing, security groups, PHP-FPM tuning, and a zero-downtime release pipeline all have to line up. This guide walks through the exact configuration I ship to production for Laravel 12 apps running on AWS in 2026, with concrete commands and the small mistakes that cause most of the outages I get paged for.
Before any infrastructure code, the stack has to match the 2026 baseline. I regularly audit Laravel development environments where outdated runtimes break silently during deploys. For AWS production today, that means PHP 8.2 minimum (8.4 recommended), Laravel 11 or 12, Composer 2.10, and Node 24 LTS for asset compilation. If you are still weighing cloud versus shared hosting, the trade-offs in choosing between AWS and shared hosting in Nepal are worth understanding before locking in a billing model.
How do you architect a secure VPC for deploying Laravel on AWS EC2 with RDS?
The network is the foundation. If it is wrong, every later step is fragile. Never put RDS in a public subnet, and never expose SSH on the app servers to the open internet without a bastion or AWS Systems Manager Session Manager. On production Laravel applications I run, a three-tier VPC prevents the accidental exposures that turn into compliance incidents.
Subnet and security group configuration
Create three subnets across at least two availability zones. The public subnet holds the Application Load Balancer (ALB) and the NAT Gateway. The private application subnet runs EC2 with no public IPs; outbound traffic flows through NAT for Composer updates. The isolated database subnet holds RDS with Publicly Accessible explicitly disabled.
Security groups are your real firewall. Configure the RDS security group to accept 3306 (MySQL) or 5432 (PostgreSQL) only from the application security group ID, not from CIDR ranges. The app security group accepts 80/443 only from the ALB security group, and SSH only from your bastion or SSM endpoint. On client projects, I have seen entire databases breached because someone opened 3306 to 0.0.0.0/0 during a debug session and forgot to revert it. Security group references close that hole by design.
Database subnet group and parameter tuning
When you create the RDS instance, attach it to a DB subnet group that spans your isolated subnets in two AZs. Choose the Multi-AZ deployment option for production; the standby replica is a synchronous hot spare and absorbs failover in roughly 60–120 seconds. Set the parameter group to utf8mb4 for MySQL (so emoji and Devanagari text survive) and set log_min_duration_statement = 1000 to capture slow queries for the Performance Insights dashboard. For a practical breakdown of index design that complements these tunings, the indexing deep dive covers the patterns that matter most.
How do you provision and configure the EC2 instance for Laravel 12?
With networking in place, provision EC2 on Ubuntu 24.04 LTS. Avoid Amazon Linux for Laravel unless your team already has muscle memory for it; the Ondřej PPA makes multi-PHP work far simpler. Pick an instance type by workload: t3.medium handles low-traffic portals, but m6g.large or c7g.large gives better price-performance for queue-heavy apps or PDF generation. For long-term cost planning beyond EC2, the Linux system administration service covers the kind of ongoing tuning these boxes need.
Installing PHP 8.4 and required extensions
Laravel 12 supports PHP 8.2 through 8.4. I standardize on PHP 8.4 for new deployments to pick up performance work and typed class constants. Add the Surý PPA and install the full extension set:
sudo add-apt-repository ppa:ondrej/php -y
sudo apt update
sudo apt install -y php8.4-fpm php8.4-cli php8.4-common \
php8.4-mysql php8.4-pgsql php8.4-sqlite3 \
php8.4-xml php8.4-curl php8.4-mbstring \
php8.4-zip php8.4-bcmath php8.4-intl \
php8.4-readline php8.4-redis php8.4-gd \
nginx composer git unzip After install, tune PHP-FPM for the instance. Edit /etc/php/8.4/fpm/pool.d/www.conf. For a 2 vCPU, 4 GB instance, start with:
pm = dynamicpm.max_children = 20(about 50 MB per worker × 20 = 1 GB reserved)pm.start_servers = 4pm.min_spare_servers = 2pm.max_spare_servers = 6pm.max_requests = 500to recycle workers and bound memory leaks
These values assume each Laravel request uses 40–60 MB. Watch real usage with htop and CloudWatch after load tests. Over-provision children and you risk OOM kills; under-provision and CPU sits idle during spikes. For a deeper treatment of pool sizing, the PHP-FPM tuning guide walks through the math for larger fleets.
Nginx virtual host configuration
Create /etc/nginx/sites-available/laravel with FastCGI parameters pointing at the PHP 8.4 socket:
server {
listen 80;
server_name _;
root /var/www/html/current/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_buffer_size 16k;
fastcgi_buffers 4 16k;
}
location ~ /\.(?!well-known).* {
deny all;
}
} Enable the site and validate before reloading:
sudo ln -s /etc/nginx/sites-available/laravel /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx php8.4-fpm How do you connect Laravel to RDS securely and optimize database performance?
With EC2 ready, point Laravel at the RDS private endpoint. Never use the public endpoint even for a one-off migration. DNS resolution can switch without warning, and cross-public latency adds milliseconds that turn into seconds under load. If you also need object storage, the S3 file storage setup covers the matching piece of this architecture.
Environment variables and connection pooling
In .env, set the host to the RDS private endpoint exactly as shown in the console:
DB_CONNECTION=mysql
DB_HOST=laravel-prod-db.cluster-abc123xyz.us-east-1.rds.amazonaws.com
DB_PORT=3306
DB_DATABASE=laravel_prod
DB_USERNAME=app_user
DB_PASSWORD="${SSM:/prod/laravel/db-password}"
MYSQL_ATTR_SSL_CA=/var/www/html/current/storage/rds-ca-bundle.pem Note the explicit SSL CA bundle path. RDS supports TLS in transit by default, but Laravel does not enforce it unless you point PDO at the AWS CA bundle. Without that, credentials travel in cleartext within the VPC—a risk if traffic ever traverses peered networks or future misconfigurations expose internal routes. Reference the bundle downloaded from the AWS trust store; the exact file is documented in the AWS RDS SSL/TLS guide.
For high-traffic apps, persistent connections or an external pooler are worth the complexity. Laravel's PDO connections are stateless per request; under heavy queues or Octane, connection churn becomes the bottleneck. On a legal-tech portal I built for concurrent document generation, dropping ProxySQL in front of MySQL cut connection overhead by about 40% and killed the intermittent timeouts that plagued peak filing days. The same effect on Postgres comes from PgBouncer; the trade-offs are covered in the pooling explainer.
Running migrations safely against RDS
Never run php artisan migrate manually on production EC2. Wire it into the deploy pipeline with explicit safeguards:
- Run
php artisan migrate:status --pendingfirst to preview changes. - Execute migrations during low-traffic windows with maintenance mode on.
- Use
--forceonly in CI scripts, never interactively. - Snapshot RDS before any destructive schema change.
- Refresh staging RDS from a production snapshot weekly and run migrations there first.
If you use read replicas, make sure migrations target the writer endpoint only. Laravel's default config handles this, but hand-rolled multi-connection setups sometimes route DDL to readers and produce silent replication lag. For a wider view of safe schema evolution, the zero-downtime migration guide goes into the patterns that keep deploys safe as the schema grows.
What deployment strategy minimizes downtime for Laravel on AWS?
Zero-downtime is non-negotiable for production Laravel. I use Deployer 7 with symlinked releases on every AWS project because it integrates cleanly with GitLab CI, supports atomic rollbacks, and needs no extra agent on the box. AWS-native alternatives like CodeDeploy with Blue/Green or Rolling work, but add operational overhead that rarely pays off for small-to-medium Laravel apps. The full comparison and zero-downtime patterns are in the Deployer 7 walkthrough.
| Strategy | Downtime | Rollback Speed | Complexity | Best For |
|---|---|---|---|---|
| Deployer 7 (Symlink) | < 1 second | Instant (symlink swap) | Low | SMB, legal-tech, agencies |
| AWS CodeDeploy (Rolling) | Partial during batch | Minutes (re-deploy) | Medium | Mid-size fleets, compliance |
| Blue/Green (ALB Swap) | Zero | Seconds (listener rule) | High | Enterprise, high-traffic SaaS |
| Docker/ECS Fargate | Zero (rolling) | Minutes (task revision) | Very High | Microservices, polyglot stacks |
Deployer 7 configuration essentials
Your deploy.php should declare shared files and writable directories explicitly. Missing entries cause post-deploy cache misses or log permission errors that show up hours later:
<?php
namespace Deployer;
require 'recipe/laravel.php';
host('production')
->set('hostname', '10.0.2.15')
->set('remote_user', 'deploy')
->set('deploy_path', '/var/www/html');
set('shared_files', ['.env']);
set('shared_dirs', ['storage/app', 'storage/logs']);
set('writable_dirs', ['bootstrap/cache', 'storage/framework']);
after('deploy:succeeded', 'artisan:optimize:clear');
after('deploy:succeeded', 'artisan:migrate');
after('deploy:succeeded', 'artisan:queue:restart'); Restart queue workers on every deploy. Laravel caches job classes in memory; without queue:restart, old code keeps running queued jobs while web requests serve new code. I have debugged payment failures on eCommerce platforms where the webhook handler ran updated logic but the queue worker still executed the previous refund routine. Always restart queues atomically with the symlink swap.
How do you monitor and maintain Laravel performance on AWS long-term?
Deployment is day one. Operations decide whether the architecture survives month six. Install the CloudWatch Logs agent on EC2 to stream Laravel logs, PHP-FPM slow logs, and Nginx access/error logs to a centralized group. Set alarms on RDS CPU over 70%, free storage under 20%, and replica lag over 30 seconds. These thresholds surface problems before users do.
Add application-level health checks beyond a 200 OK. Create a /health route that verifies database connectivity, Redis availability, and queue worker responsiveness. Point the ALB target group at it. On systems I maintain, this pattern has prevented cascading failures during RDS maintenance windows and ElastiCache node replacements. Pair this with structured logging and the structured logging playbook so on-call engineers can actually search the firehose during an incident.
Schedule recurring maintenance. Rotate OPcache every deploy. Prune old log files weekly. Review slow query logs monthly. Test disaster recovery quarterly. Backup verification matters more than backup creation. Restore your RDS snapshot to a test instance monthly and run smoke tests against it. If the restore takes longer than your RTO allows, fix the backup strategy now—not during a real outage. A practical backup plan is laid out in the cloud backup and DR guide.
Cost optimization is ongoing. Right-size EC2 from CloudWatch metrics after 30 days of production traffic. Reserve instances for the predictable baseline, and use spot for queue workers that tolerate interruption. Enable RDS Performance Insights to surface expensive queries; a single composite index often reduces monthly RDS spend more than vertical scaling ever would. For a fuller cost playbook, the AWS cost optimization and FinOps basics articles go into the tactics that actually move the bill.
For ongoing engineering and DevOps support, the support and maintenance service covers the operational layer that production AWS workloads need. If performance is the bottleneck, the speed optimization service pairs well with a deployment overhaul. Broader architecture and infra help is available through enterprise application development and the full services catalogue.
Key Takeaways
- Always run RDS in an isolated subnet with the security group locked to the app security group ID, never a CIDR.
- Use PHP 8.4 with Laravel 12 and Composer 2.10 for current 2026 stacks; tune PHP-FPM
pm.max_childrenfrom real memory numbers. - Force TLS to RDS by setting
MYSQL_ATTR_SSL_CAto the AWS CA bundle; do not rely on implicit encryption. - Use Deployer 7 with symlinked releases and always run
queue:restartafter the symlink swap to keep workers in lockstep. - Wire migrations into the deploy pipeline, snapshot RDS before destructive changes, and verify backups by restoring them on a schedule.
People Also Ask
What instance size do I need to run Laravel on EC2?
For a low-traffic Laravel app with under 50 concurrent users, a t3.medium (2 vCPU, 4 GB) is usually enough. For queue-heavy workloads or apps that generate PDFs and process images, step up to m6g.large or c7g.large. Watch actual memory usage with CloudWatch after 30 days and resize from data, not guesses. Over-provisioning is wasted spend; under-provisioning shows up as OOM kills at peak.
Should I use RDS Multi-AZ for a small Laravel app?
Yes for any production system. Multi-AZ gives you a synchronous standby in another availability zone and absorbs failover in 60–120 seconds. The cost premium is roughly double the single-AZ instance price, but you also get automated backups, point-in-time recovery, and protection against AZ-level outages. For a staging environment, single-AZ is fine.
Can I run Laravel migrations automatically on every deploy?
Yes, but only in CI scripts and only after a successful build. Run php artisan migrate --force as a post-deploy step, but gate it on health checks and have an automated RDS snapshot taken beforehand. Avoid running migrations on manual SSH sessions; it breaks the audit trail and makes rollbacks harder.
How much does it cost to run Laravel on EC2 with RDS?
A small production stack (one t3.medium EC2, a db.t3.medium Multi-AZ RDS MySQL, and a NAT Gateway) typically lands in the USD 80–150 per month range depending on region and data transfer. Reserved Instances or Savings Plans can cut that by 30–60% for one- or three-year terms. Right-sizing after 30 days of real traffic usually beats guessing at launch.
Conclusion
Deploying Laravel on AWS EC2 with RDS is a stack of small, disciplined decisions. The VPC isolates RDS. The security groups reference each other by ID, not CIDR. PHP-FPM is tuned from real memory numbers. RDS is reached over the private endpoint with TLS forced. Releases run through Deployer 7 with atomic symlink swaps and queue restarts. Each piece is straightforward; together they keep the app up while you sleep. If you want hands-on help shipping this stack for a production Laravel workload, get in touch with your project details and we can map the path from your current setup to a reliable AWS deployment.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

