Deploying a production Laravel application requires a secure, high-performance infrastructure topology. Rather than placing databases and servers in a single public space, enterprise standards call for isolating your data tier within private subnets while exposing only web traffic through a secured public subnet.
This guide walks through configuring a dedicated Amazon VPC, launching an EC2 instance with Nginx and PHP 8.3-FPM, provisioning a private Amazon RDS MySQL instance, establishing secure deployment pipelines, and securing traffic with Let’s Encrypt SSL.
Production Architecture Diagram
┌────────────────────────────────────────────────────────────────────────┐│ Amazon VPC (laravel-vpc): 192.168.0.0/16 ││ ││ ┌────────────────────────────────────────────────────────────────┐ ││ │ Public Subnet: laravel-public-subnet-1 (192.168.0.0/24) │ ││ │ Internet Gateway (IGW) ──> Elastic IP (EIP) │ ││ │ │ ││ │ [ EC2 Web Server: Ubuntu 22.04 LTS ] │ ││ │ - Nginx 1.24 + PHP 8.3-FPM + Node.js 22 │ ││ │ - Deployer User & GitHub Deploy Keys │ ││ │ - Security Group (laravel-sg-web): │ ││ │ • Port 80 (HTTP) ──> 0.0.0.0/0 │ ││ │ • Port 443 (HTTPS) ──> 0.0.0.0/0 │ ││ │ • Port 22 (SSH) ──> Your Static IP Only │ ││ └────────────────────────────────┬───────────────────────────────┘ ││ │ Port 3306 (Internal Traffic Only) ││ ┌────────────────────────────────▼───────────────────────────────┐ ││ │ Private Subnets (Isolated Data Tier - No Internet Access) │ ││ │ • laravel-private-subnet-1 (192.168.1.0/24 - us-east-1a) │ ││ │ • laravel-private-subnet-2 (192.168.2.0/24 - us-east-1b) │ ││ │ │ ││ │ [ Amazon RDS MySQL 8.0 Multi-AZ Subnet Group ] │ ││ │ - Security Group (laravel-sg-db): │ ││ │ • Port 3306 (MySQL) ──> Source: laravel-sg-web Only │ ││ │ - Public Access: Disabled (No External Exposure) │ ││ └────────────────────────────────────────────────────────────────┘ │└────────────────────────────────────────────────────────────────────────┘Phase 1: Networking & VPC Setup
1. Create the Custom VPC
- Open the AWS Management Console and navigate to the VPC Dashboard.
- Click Create VPC → select VPC only.
- Configure settings:
- Name tag:
laravel-vpc - IPv4 CIDR block:
192.168.0.0/16
- Name tag:
- Click Create VPC.
2. Create and Attach the Internet Gateway (IGW)
- Under VPC navigation, go to Internet Gateways → Create internet gateway.
- Name it
laravel-igwand click Create internet gateway. - Select the created gateway → click Actions → Attach to VPC.
- Select
laravel-vpcand confirm attachment.
3. Create Public and Private Subnets
Navigate to Subnets → Create subnet, select laravel-vpc, and create three subnets across distinct Availability Zones:
| Subnet Name | Type | Availability Zone | IPv4 CIDR Block | Purpose |
|---|---|---|---|---|
laravel-public-subnet-1 | Public | us-east-1a | 192.168.0.0/24 | EC2 Web Server |
laravel-private-subnet-1 | Private | us-east-1a | 192.168.1.0/24 | RDS MySQL Primary |
laravel-private-subnet-2 | Private | us-east-1b | 192.168.2.0/24 | RDS MySQL Standby/Failover |
4. Configure Route Tables
- Public Route Table (
laravel-public-rt):- Go to Route Tables → Create route table. Name it
laravel-public-rtunderlaravel-vpc. - Go to Routes → Edit routes → Add route:
- Destination:
0.0.0.0/0 - Target:
Internet Gateway(laravel-igw)
- Destination:
- Go to Subnet associations → Edit subnet associations → select
laravel-public-subnet-1.
- Go to Route Tables → Create route table. Name it
- Private Route Table (
laravel-private-rt):- Create route table
laravel-private-rtunderlaravel-vpc. - Do not attach an Internet Gateway.
- Associate both
laravel-private-subnet-1andlaravel-private-subnet-2.
- Create route table
Phase 2: Security Groups
Security groups act as stateful firewalls. We create two discrete groups:
1. Web Security Group (laravel-sg-web)
- VPC:
laravel-vpc - Inbound Rules:
HTTP• TCP • Port80• Source:0.0.0.0/0HTTPS• TCP • Port443• Source:0.0.0.0/0SSH• TCP • Port22• Source:My IP(Restrict to your personal public IP)
2. Database Security Group (laravel-sg-db)
- VPC:
laravel-vpc - Inbound Rules:
MySQL/Aurora• TCP • Port3306• Source:laravel-sg-web(Reference the web security group ID)
Phase 3: Launching EC2 & RDS
1. Launch the EC2 Web Server
- Open EC2 → click Launch Instance.
- Name:
laravel-ec2-instance. - AMI:
Ubuntu Server 22.04 LTS (HVM), SSD Volume Type. - Instance Type:
t2.micro(ort3.smallfor production workloads). - Key Pair: Create or select key pair
laravel-key.pemand save it locally. - Network Settings → click Edit:
- VPC:
laravel-vpc - Subnet:
laravel-public-subnet-1 - Auto-assign Public IP:
Enable - Security Groups: Select
laravel-sg-web
- VPC:
- Click Launch Instance.
2. Allocate and Associate an Elastic IP (EIP)
- Under EC2 → Network & Security → Elastic IPs.
- Click Allocate Elastic IP address → Allocate.
- Select the IP → Actions → Associate Elastic IP address.
- Select
laravel-ec2-instanceand confirm.
3. Provision Amazon RDS MySQL
- Navigate to RDS → Subnet groups → Create DB Subnet Group:
- Name:
laravel-rds-subnet-group - VPC:
laravel-vpc - Add Availability Zones:
us-east-1aandus-east-1b - Subnets: Select
laravel-private-subnet-1andlaravel-private-subnet-2
- Name:
- Navigate to Databases → Create database:
- Engine: MySQL (Version 8.0)
- Template: Free tier (or Production)
- DB instance identifier:
laravel-mysql-db - Master username:
admin - Master password:
YourStrongPasswordHere - Instance class:
db.t2.micro - Storage: 20 GB gp3
- VPC:
laravel-vpc - DB subnet group:
laravel-rds-subnet-group - Public access: No
- VPC security group: Select
laravel-sg-db - Initial database name:
laravel_db
- Click Create database. Once created, copy the RDS Endpoint address.
Phase 4: Server Provisioning
Set permissions for your downloaded .pem key on your local machine and connect via SSH:
chmod 0600 laravel-key.pemssh -i laravel-key.pem ubuntu@<ELASTIC_IP>1. Update Packages & Install Base Tools
sudo apt update && sudo apt upgrade -ysudo apt install -y curl git unzip software-properties-common ufw mysql-client2. Install and Start Nginx
sudo apt install -y nginxsudo systemctl enable nginxsudo systemctl start nginx3. Install PHP 8.3 & Required Extensions
Add Ondřej Surý’s official PHP repository and install PHP 8.3-FPM alongside all extensions required by Laravel:
sudo add-apt-repository ppa:ondrej/php -ysudo apt update
sudo apt install -y php8.3-fpm php8.3-cli php8.3-common php8.3-mysql \php8.3-xml php8.3-curl php8.3-gd php8.3-mbstring php8.3-zip \php8.3-bcmath php8.3-intl php8.3-readline php8.3-tokenizer php8.3-redis
# Verify statusphp -vsudo systemctl status php8.3-fpm --no-pager4. Install Composer
cd ~curl -sS https://getcomposer.org/installer -o composer-setup.phpsudo php composer-setup.php --install-dir=/usr/local/bin --filename=composerrm composer-setup.phpcomposer --version5. Install Node.js & NPM (NodeSource v22.x)
curl -fsSL https://dev.nodesource.com/setup_22.x | sudo -E bash -sudo apt install -y nodejsnode -v && npm -vPhase 5: Dedicated Deployer & Codebase Setup
Running web applications under an unprivileged user increases security isolation:
1. Create Dedicated Deployment User
sudo adduser --disabled-password --gecos "" deployersudo usermod -a -G deployer www-data2. Configure GitHub Deploy Keys
Switch to the deployer user and generate a dedicated SSH key pair:
sudo su - deployer
mkdir -p ~/.ssh && chmod 700 ~/.sshssh-keygen -t ed25519 -f ~/.ssh/github_deploy -C "deployer@aws-laravel"
# Configure SSH configcat << 'EOF' > ~/.ssh/configHost github.com IdentityFile ~/.ssh/github_deploy IdentitiesOnly yesEOF
chmod 600 ~/.ssh/configchmod 600 ~/.ssh/github_deploy
# Output public key to add to GitHubcat ~/.ssh/github_deploy.pub3. Clone and Configure Laravel
Still as the deployer user:
cd /home/deployergit clone git@github.com:<ORG_OR_USERNAME>/<REPOSITORY>.git codecd code
# Install dependenciescomposer install --no-dev --optimize-autoloader
# Environment configurationcp .env.example .envphp artisan key:generateOpen .env and configure your database and production parameters:
APP_NAME="Laravel Production"APP_ENV=productionAPP_DEBUG=falseAPP_URL=https://yourdomain.com
DB_CONNECTION=mysqlDB_HOST=laravel-mysql-db.cxxxxxxx.us-east-1.rds.amazonaws.comDB_PORT=3306DB_DATABASE=laravel_dbDB_USERNAME=adminDB_PASSWORD=YourStrongPasswordHereRun database migrations and cache configurations:
php artisan migrate --forcephp artisan storage:linkphp artisan config:cachephp artisan route:cachephp artisan view:cacheCompile frontend assets:
npm installnpm run buildExit back to the ubuntu administrative user:
exitPhase 6: Web Server Configuration
1. Configure Nginx Virtual Host
Remove the default configuration and create a new server block:
sudo rm -f /etc/nginx/sites-enabled/defaultsudo nano /etc/nginx/sites-available/laravel.confAdd the following optimized virtual host configuration:
server { listen 80 default_server; listen [::]:80 default_server; server_name yourdomain.com www.yourdomain.com;
root /home/deployer/code/public; index index.php index.html;
charset utf-8;
# Logs access_log /var/log/nginx/laravel_access.log; error_log /var/log/nginx/laravel_error.log;
location / { try_files $uri $uri/ /index.php?$query_string; }
location = /favicon.ico { access_log off; log_not_found off; } location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.php;
location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/var/run/php/php8.3-fpm.sock; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; include fastcgi_params;
fastcgi_buffers 8 16k; fastcgi_buffer_size 32k; fastcgi_connect_timeout 60; fastcgi_send_timeout 180; fastcgi_read_timeout 180; }
location ~ /\.(?!well-known).* { deny all; }}Enable the site and test configuration:
sudo ln -s /etc/nginx/sites-available/laravel.conf /etc/nginx/sites-enabled/sudo nginx -tsudo systemctl reload nginx2. Set Up Directory Permissions
Configure ownership and permissions so Nginx (www-data) and deployer can read and write required storage directories:
sudo chown -R deployer:www-data /home/deployer/codesudo chmod -R 775 /home/deployer/code/storagesudo chmod -R 775 /home/deployer/code/bootstrap/cacheGrant execute traversal permissions on the home directory:
chmod 755 /home/deployerPhase 7: SSL with Let’s Encrypt (Certbot)
Once your domain’s DNS A records point to your AWS Elastic IP, issue a free, auto-renewing SSL certificate:
sudo apt install -y certbot python3-certbot-nginxsudo certbot --nginx -d yourdomain.com -d www.yourdomain.comCertbot automatically modifies your Nginx configuration to handle HTTPS termination, redirect HTTP to HTTPS, and schedules a systemd timer for automatic renewals.
Test the renewal flow:
sudo certbot renew --dry-runPost-Deployment Checklist
- VPC Isolation: Database is inside private subnets with no public IP.
- Firewall: Port 22 SSH is restricted to authorized IPs only.
- Application Cache:
php artisan optimizeexecuted for config, routes, and views. - Asset Bundles:
build/assets compiled with Vite. - HTTPS: Valid TLS certificate applied with automatic renewal active.
Questions or Feedback?
Have questions about AWS architecture, multi-server scaling, or Redis queue workers on AWS? Reach out via email at kashifwahaj@gmail.com.