How I Deployed a Laravel + React Application to Production on AWS

Getting an application “working on my machine” is one milestone. Getting it safely into production is a different job. This summer, I took on a project where a development team had finished their Laravel application and needed it live on AWS, secured and production-ready. Here is how it went and what I learned.
The Challenge
The client’s team had built the application and needed someone to own the infrastructure side:
AWS environment setup
Production LAMP stack configuration for the Laravel backend
Deployment of the React frontend
Security testing of the application
A web application firewall (WAF) in front of everything
The project ran from July 2025 to early September 2025. It took longer than first planned because the scope grew along the way, which is common in real deployments.
Step 1: Laying the AWS Foundation
Before touching the application, I set up the cloud environment:
Networking: a VPC with public and private subnets, so only what needs to be internet-facing is exposed
Compute: EC2 instances for the application servers
Access control: IAM roles with least-privilege permissions instead of shared credentials
Security groups: tight inbound rules, allowing only the ports the app requires
Step 2: Production LAMP Stack for Laravel
Laravel runs well on Linux, Apache, MySQL, and PHP, but production needs more than composer install:
Apache virtual hosts configured with the document root pointing to
/publicPHP-FPM tuned for production, with debug mode off
A properly secured
.envfile, never exposed or committedCorrect file permissions for
storageandbootstrap/cacheQueue workers and the scheduler managed with Supervisor and cron
Database access locked down, with credentials separated from the app server
HTTPS enforced with a valid SSL certificate
The goal is a setup that survives reboots, deploys cleanly, and can be reproduced.
Step 3: Deploying the React Application
The React frontend was built into static assets and served alongside the backend. Key points:
Environment-specific API URLs set at build time
Compressed, cached static assets
Proper routing fallbacks so client-side routes don’t return 404s on refresh
CORS configured so the frontend and API talk to each other, and nobody else
Step 4: Security Testing
Before calling anything production-ready, I tested the application the way an attacker would think about it. I checked for:
Common vulnerabilities such as SQL injection, XSS, and CSRF
Exposed files and directories, such as
.env,.git, and backupsWeak or missing security headers
Outdated packages and server software
Misconfigured permissions and open ports
Each finding was documented and fixed, then retested. Laravel’s built-in protections help a lot, but they don’t cover server misconfiguration. That part is the infrastructure owner’s job.
Step 5: Adding a Web Application Firewall
The last layer was an AWS WAF in front of the application. It filters malicious traffic before it reaches the servers:
Managed rule sets covering common attacks (OWASP-style threats)
Rate limiting to slow brute-force and bot traffic
Blocking of known bad inputs and suspicious patterns
I started in monitoring mode to check for false positives, then switched to blocking once the rules were tuned. Going straight to blocking can break legitimate users.
Lessons Learned
Scope changes are normal. Deployments uncover things nobody planned for. Clear communication matters more than a perfect initial estimate.
Security is a layer cake. Network rules, server hardening, application checks, and a WAF each catch different problems.
Document everything. A deployment the client can’t understand or repeat is only half delivered.
Staging first. Test the full flow before production, every time.
Final Thoughts
The client left a 4.9/5 review and said they’d hire me again.
Need something similar?
I help teams take finished applications to secure, production-ready infrastructure on AWS and Azure: server setup, CI/CD, security hardening, and WAF. If you have a Laravel, Node.js, or React app ready to go live, you can reach me on www.linkedin.com/in/pdeep1506 or pdeep1506.work@gmail.com .
Comments
Join the discussionOne thing I'd add to the deploy checklist: Supervisor keeps your queue workers alive across reboots, but it does not restart them when you ship new code. We learned this on a dashboard app: after a deploy, the workers kept running the old release's code, so a queued report job referenced a column we had just renamed and failed quietly for the better part of a day. Now every deploy ends with php artisan queue:restart, which signals the workers to finish their current job and exit so Supervisor boots them on the new code. It's one line, but it closes the gap between "deployed" and "actually running the new code".
Great point, Kamran, thank you.

