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 requires separating your application logic from your data layer while maintaining strict network security between them. Many developers struggle with this architecture because it demands coordinating VPC peering, security groups, and PHP-FPM tuning across two distinct managed services rather than a single monolithic server. This guide provides the concrete configuration needed to ship a production-grade Laravel 12 application on AWS infrastructure in 2026.

Before writing any infrastructure code, verify your stack meets current 2026 baselines. I regularly audit Laravel development environments where outdated runtimes cause silent failures during deployment; for AWS production today, you need PHP 8.2 minimum (8.4 recommended), Laravel 11.x or 12.x, Composer 2.7+, and Node.js 22 LTS for asset compilation. Understanding these dependencies upfront prevents the most common integration issues when comparing cloud hosting options for business-critical applications.

How do you architect a secure VPC for deploying Laravel on AWS EC2 with RDS?

The foundation of any reliable AWS deployment is network isolation. Never place an RDS instance in a public subnet, and never allow SSH access directly to your application servers from the open internet without a bastion host or AWS Systems Manager Session Manager. In my experience working on production Laravel applications handling sensitive legal-tech data, a three-tier VPC architecture prevents accidental exposure and satisfies compliance requirements.

VPC 10.0.0.0/16Public SubnetApplication Load BalancerNAT Gateway / BastionPrivate App SubnetEC2 (Laravel + PHP-FPM)Auto Scaling GroupRedis (ElastiCache)Isolated DB SubnetRDS Multi-AZMySQL 8.4 / PgSQL 17
Three-tier VPC topology isolating RDS from public access while allowing EC2 communication through security groups

Subnet and security group configuration

Create three distinct subnets across at least two availability zones. The public subnet holds only your Application Load Balancer (ALB) and NAT Gateway. The private application subnet contains your EC2 instances running Laravel; these instances have no public IP addresses and route outbound traffic through the NAT Gateway for Composer updates and package installation. The isolated database subnet hosts RDS with "Publicly Accessible" explicitly disabled.

Security groups act as your primary firewall. Configure the RDS security group to accept inbound traffic on port 3306 (MySQL) or 5432 (PostgreSQL) only from the application security group ID, not from CIDR ranges. This ensures that even if another instance launches in the VPC, it cannot reach your database unless explicitly tagged. The application security group should accept HTTP/HTTPS only from the ALB security group and SSH only from your bastion or SSM endpoint. On real client projects, I have seen entire databases compromised because someone opened port 3306 to 0.0.0.0/0 during debugging and forgot to revert it; explicit security group references prevent this class of error entirely.

How do you provision and configure the EC2 instance for Laravel 12?

Once networking is established, provision your EC2 instance with Ubuntu 24.04 LTS. Avoid Amazon Linux for Laravel unless your team has specific expertise; Ubuntu’s PPA ecosystem makes managing multiple PHP versions significantly easier. Select an instance type based on workload: t3.medium works for low-traffic portals, but m6g.large or c7g.large provides better price-performance for compute-heavy Laravel applications processing queues or generating PDFs.

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 in 2026 to leverage performance improvements and typed class constants. Add the Ondřej Surý PPA and install the full extension set Laravel requires:

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 installation, tune PHP-FPM for your instance size. Edit /etc/php/8.4/fpm/pool.d/www.conf. For a 2-vCPU instance with 4GB RAM, start with:

  • pm = dynamic
  • pm.max_children = 20 (approximately 50MB per worker × 20 = 1GB reserved)
  • pm.start_servers = 4
  • pm.min_spare_servers = 2
  • pm.max_spare_servers = 6
  • pm.max_requests = 500 (recycle workers to prevent memory leaks)

These values assume each Laravel request consumes roughly 40–60MB. Monitor actual memory usage with htop or CloudWatch after load testing; over-provisioning children causes OOM kills, while under-provisioning leaves CPU idle during traffic spikes.

Nginx virtual host configuration

Create /etc/nginx/sites-available/laravel with proper FastCGI parameters pointing to 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 configuration 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 provisioned, configure Laravel’s environment to reach RDS through the private endpoint. Never use the public endpoint even if temporarily enabled for migration; DNS resolution can switch unexpectedly, and latency increases significantly outside the VPC.

EC2 InstanceLaravel 12 ApplicationPHP 8.4 PDO Driver.env DB_HOST ConfigRoute 53 Private DNSdb.laravel-prod.internalRDS InstanceMySQL 8.4 / PgSQL 17Security Group FilterEncrypted StorageDNS LookupTCP 3306
Laravel resolves RDS private endpoint through Route 53 internal DNS before establishing encrypted TCP connection

Environment variables and connection pooling

In your .env file, set the database host to the RDS private endpoint exactly as shown in the AWS 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}"
DB_SSLMODE=require

Note the DB_SSLMODE=require directive. RDS supports TLS encryption in transit by default, but Laravel does not enforce it unless explicitly configured. Without this flag, credentials travel unencrypted between EC2 and RDS within the VPC—a risk if traffic traverses peered networks or if future misconfigurations expose routes. Download the RDS CA bundle and reference it if your organization requires certificate verification beyond AWS’s default trust chain.

For high-traffic applications, consider enabling persistent connections or using an external pooler like PgBouncer (for PostgreSQL) or ProxySQL (for MySQL). Laravel’s native PDO connections are stateless per request; under heavy queue workloads or Octane deployments, connection churn becomes a bottleneck. On a legal-tech portal I built handling concurrent document generation, adding ProxySQL reduced database connection overhead by 40% and eliminated intermittent timeout errors during peak filing periods.

Running migrations safely against RDS

Never run php artisan migrate manually on production EC2 instances. Integrate migrations into your deployment 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 enabled.
  3. Use --force flag only in automated scripts, never interactively.
  4. Back up RDS snapshots before destructive schema changes.
  5. Test migrations against a staging RDS clone restored from production snapshot weekly.

If your application uses read replicas, ensure migrations target the writer endpoint exclusively. Laravel’s default configuration handles this correctly, but custom multi-connection setups sometimes accidentally route DDL statements to readers, causing replication lag or silent failures.

What deployment strategy minimizes downtime for Laravel on AWS?

Zero-downtime deployment is non-negotiable for production Laravel systems. I use Deployer 7 with symlinked releases on every AWS project because it integrates cleanly with GitLab CI, supports atomic rollbacks, and requires no additional infrastructure like CodeDeploy agents. For teams already invested in AWS-native tooling, CodeDeploy with Blue/Green or Rolling configurations achieves similar results, but adds operational complexity that rarely justifies itself for small-to-medium Laravel applications.

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 define shared files and writable directories explicitly. Missing entries here cause post-deploy cache misses or log permission errors that manifest hours after release:

<?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');

Critically, restart queue workers after every deploy. Laravel caches job classes in memory; without queue:restart, old code continues executing queued jobs even though web requests serve new code. This mismatch causes subtle bugs that are extremely difficult to diagnose. I have debugged payment processing failures on eCommerce platforms where the webhook handler ran updated logic but the queue worker still executed the previous version’s refund routine—always restart queues atomically with the symlink swap.

1. Upload ReleaseGit Clone + InstallComposer InstallBuild Assets (Vite)2. Symlink Swapln -sfn release/N currentOPcache ResetPHP-FPM Reload3. Post-Deployconfig:cachemigrate --forcequeue:restart4. Verify / RollbackHealth Check Endpointdep rollback (if fail)Notify Team (Slack)
Atomic deployment sequence ensuring zero downtime through symlink swaps and coordinated service restarts

How do you monitor and maintain Laravel performance on AWS long-term?

Deployment is day one; operations determine whether your architecture survives month six. Configure CloudWatch Logs agent on EC2 to stream Laravel logs, PHP-FPM slow logs, and Nginx access/error logs to centralized storage. Set alarms on RDS CPU utilization exceeding 70%, free storage dropping below 20%, and replica lag surpassing 30 seconds. These thresholds catch problems before users report them.

Implement application-level health checks beyond simple HTTP 200 responses. Create a /health route that verifies database connectivity, Redis availability, and queue worker responsiveness. Configure your ALB target group to use this endpoint for health checks; when dependencies fail, the ALB stops routing traffic to unhealthy instances automatically. On production systems I maintain, this pattern has prevented cascading failures during RDS maintenance events and ElastiCache node replacements.

Schedule regular maintenance tasks: rotate OPcache every deploy, prune old log files weekly, analyze slow query logs monthly, and test disaster recovery quarterly. Backup verification matters more than backup creation; restore your RDS snapshot to a test instance monthly and run your application’s smoke tests against it. If restoration takes longer than your RTO allows, adjust your backup strategy or instance sizing now—not during an actual outage.

Cost optimization deserves ongoing attention. Right-size EC2 instances based on CloudWatch metrics after 30 days of production traffic. Reserve instances for predictable baseline load and use spot instances for queue workers that tolerate interruption. Enable RDS Performance Insights to identify expensive queries; often, adding a composite index reduces monthly RDS spend more effectively than vertical scaling. For Nepal-based clients billing in NPR, even modest AWS optimizations translate to meaningful savings when converted annually.

Next Steps for Your Laravel AWS Deployment

Deploying Laravel on AWS EC2 with RDS demands disciplined networking, precise runtime configuration, and automated deployment workflows—but the payoff is a resilient, scalable platform that grows with your business. Start with the VPC architecture outlined above, validate each layer independently before integrating, and invest early in monitoring and rollback capabilities. If you need hands-on assistance architecting or migrating your Laravel application to AWS, reach out to discuss your project requirements. Whether you are building a legal-tech portal, an eCommerce platform, or a SaaS product, getting the infrastructure right from day one prevents costly rework later.

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

Quick Contact Options
Choose how you want to connect me: