Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Deploying Laravel on AWS EC2 with RDS

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.

VPC 10.0.0.0/16Public SubnetApplication LoadBalancer (ALB)NAT Gateway+ Bastion HostPrivate App SubnetEC2 (Laravel 12+ PHP 8.4-FPM)Auto Scaling GroupElastiCache RedisSession + CacheIsolated DB SubnetRDS Multi-AZMySQL 8.4 /PostgreSQL 17
Three-tier VPC topology isolating RDS from public access while routing EC2 through ALB and NAT

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 = dynamic
  • pm.max_children = 20 (about 50 MB per worker × 20 = 1 GB reserved)
  • pm.start_servers = 4
  • pm.min_spare_servers = 2
  • pm.max_spare_servers = 6
  • pm.max_requests = 500 to 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.

EC2 InstanceLaravel 12 AppPHP 8.4 PDO.env DB_HOSTRoute 53 PrivateDNS ResolverRDS InstanceMySQL 8.4 /PostgreSQL 17SG FilterEncrypted DiskDNS LookupTLS TCP 3306
Laravel resolves the RDS private endpoint through Route 53 internal DNS before opening the encrypted TCP connection

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:

  1. Run php artisan migrate:status --pending first to preview changes.
  2. Execute migrations during low-traffic windows with maintenance mode on.
  3. Use --force only in CI scripts, never interactively.
  4. Snapshot RDS before any destructive schema change.
  5. 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.

StrategyDowntimeRollback SpeedComplexityBest For
Deployer 7 (Symlink)< 1 secondInstant (symlink swap)LowSMB, legal-tech, agencies
AWS CodeDeploy (Rolling)Partial during batchMinutes (re-deploy)MediumMid-size fleets, compliance
Blue/Green (ALB Swap)ZeroSeconds (listener rule)HighEnterprise, high-traffic SaaS
Docker/ECS FargateZero (rolling)Minutes (task revision)Very HighMicroservices, 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.

1. Upload ReleaseGit Clone + InstallComposer InstallBuild Assets2. Symlink Swapln -sfn releaseOPcache ResetPHP-FPM Reload3. Post-Deployconfig:cachemigrate --forcequeue:restart4. Verify / Rollback/health Checkdep rollbackNotify Team
Atomic deploy sequence: upload, swap, post-deploy tasks, and verify or rollback in a single pipeline

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_children from real memory numbers.
  • Force TLS to RDS by setting MYSQL_ATTR_SSL_CA to the AWS CA bundle; do not rely on implicit encryption.
  • Use Deployer 7 with symlinked releases and always run queue:restart after 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

Laravel 12 requires PHP 8.2 or higher. I recommend installing PHP 8.3 or 8.4 on Ubuntu 24.04 LTS for the latest security patches and performance improvements before configuring your EC2 instance.

A t3.small EC2 instance plus db.t3.micro RDS costs approximately USD 35–45 monthly, or NPR 4,700–6,000. This covers compute, managed database, and minimal EBS storage suitable for low-traffic production Laravel applications in Nepal.

Use RDS for production. Self-hosted MySQL saves money initially but risks data loss during EC2 failures. RDS provides automated backups, patching, and failover, which matters when you lack dedicated DBA resources for Nepal-based client projects.

Place both EC2 and RDS in the same VPC with private subnets. Configure RDS security groups to accept traffic only from your EC2 security group on port 3306. Never assign a public IP to RDS. In your Laravel .env file, use the RDS private endpoint hostname, not an IP address, as AWS can change underlying IPs during maintenance. This architecture prevents external database access while maintaining application connectivity through internal AWS networking.

I use Deployer 7 with zero-downtime symlinked releases triggered via GitLab CI. Frontend assets are built in the pipeline and committed as artifacts because production EC2 instances should not run Node.js. After swapping the release symlink, reload PHP-FPM to invalidate opcache. Store persistent files like .env and storage/ in shared directories outside release folders. This approach allows instant rollbacks via dep rollback if a deployment fails, which has saved multiple production deployments on client projects.

Store sensitive values in AWS Systems Manager Parameter Store or Secrets Manager, then inject them into the Laravel .env file during deployment. Never commit .env to version control. For simpler setups, keep .env in Deployer's shared directory with strict 600 permissions owned by www-data. Rotate credentials periodically and restrict IAM roles to least privilege. On legal-tech portals handling client documents, I always encrypt database credentials at rest and audit access logs quarterly.

Common causes include incorrect security group rules blocking port 3306, mismatched PHP versions between local and server, missing PHP extensions like pdo_mysql or bcmath, and wrong file ownership on storage/ and bootstrap/cache/. Check Laravel logs in storage/logs/laravel.log and PHP-FPM error logs. Verify the RDS endpoint in .env matches exactly, including the port. I have debugged this exact issue repeatedly when developers copy-paste endpoints with trailing whitespace or use public instead of private DNS names.

Install Certbot and obtain free Let's Encrypt certificates for your domain. Configure Apache or Nginx to serve HTTPS on port 443 and redirect HTTP traffic. Set APP_URL=https://yourdomain.com in .env so Laravel generates correct asset and route URLs. Enable HSTS headers and configure certificate auto-renewal via cron. For RDS connections, enable SSL in the database configuration if compliance requires encrypted transit. On Nepal-facing sites, ensure certificate chains include intermediates to avoid mobile browser warnings.

Enable CloudWatch alarms for EC2 CPU, memory, and disk utilization alongside RDS connection count and slow query metrics. Install Laravel Telescope or Debugbar in non-production environments for request inspection. Configure log forwarding to CloudWatch Logs or a service like Papertrail for centralized error tracking. Set billing alerts to catch unexpected cost spikes early. In my experience, disk space exhaustion on /var/log is the most frequent silent failure on small EC2 instances running Laravel, so monitor that specifically.

Enable OPcache with validate_timestamps=0 in production and restart PHP-FPM after deploys. Use Redis on ElastiCache or a t3.micro instance for session, cache, and queue drivers instead of file-based storage. Precompile config and routes with php artisan config:cache and route:cache. Optimize autoloader with composer dump-autoload --optimize. Serve static assets via CloudFront CDN to reduce EC2 load. These optimizations routinely cut response times by 40–60% on budget instances serving Nepal-based eCommerce sites.

Yes, run queue workers as systemd services using Redis or database drivers. Configure supervisor or systemd to manage worker processes with automatic restarts. Set --tries and --timeout flags appropriately for your job types. Monitor failed jobs via Laravel Horizon or the failed_jobs table. While SQS integrates natively with AWS, Redis queues are simpler to debug and sufficient for most Nepal-based applications under moderate load. Reserve SQS for high-throughput systems requiring guaranteed delivery across distributed services.

Enable RDS automated backups with a retention period matching your recovery point objective. Create manual snapshots before major deployments. For Laravel storage files, use AWS Backup or a nightly cron job syncing uploads to S3 via aws s3 sync. Test restores quarterly; untested backups are worthless. On legal-tech platforms storing client documents, I retain daily backups for thirty days and weekly backups for one year. Document the restore procedure so any team member can execute it during emergencies without tribal knowledge.

Disable root SSH login and use key-based authentication only. Configure UFW to allow only ports 22, 80, and 443. Install fail2ban to block brute-force attempts. Keep Ubuntu, PHP, and Laravel updated monthly. Restrict S3 bucket policies to specific IAM roles. Sanitize all user input with Laravel Form Requests and validate file uploads strictly. For applications handling Nepali legal documents or payments via eSewa and Khalti, enable WAF rules against SQL injection and XSS. Audit dependencies regularly with composer audit.

Export your current database using mysqldump with --single-transaction for consistency. Create the target schema on RDS, then import the dump via mysql client from your EC2 instance within the VPC. Update .env with the new RDS endpoint and credentials. Run php artisan migrate:fresh only if starting clean; otherwise verify migrations match the imported state. Test thoroughly in staging before switching production traffic. Downtime depends on database size; a 5GB database typically imports in under ten minutes on db.t3.micro instances.

Choose AWS Lightsail for predictable pricing under USD 20 monthly when you need simplicity over configurability. Consider Laravel Forge or Vapor if you want managed infrastructure without DevOps overhead. For Nepal-focused projects with tight budgets, a well-configured shared hosting plan may suffice until traffic justifies dedicated resources. EC2 plus RDS makes sense when you need VPC isolation, custom networking, or plan to scale beyond single-instance limits. Avoid over-engineering early; I have seen startups burn NPR 50,000 monthly on unused AWS capacity that a Rs 3,000 shared host could handle initially.

Share this article

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.

Quick Contact Options
Choose how you want to connect me: