Laravel AWS Production Deployment Guide

Step-by-step production architecture for deploying Laravel on AWS using a custom VPC, EC2 Ubuntu 22.04 LTS, Nginx, PHP 8.3-FPM, and private Amazon RDS MySQL.

1 min read

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

aws infrastructure architecture
┌────────────────────────────────────────────────────────────────────────┐
│ 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

  1. Open the AWS Management Console and navigate to the VPC Dashboard.
  2. Click Create VPC → select VPC only.
  3. Configure settings:
    • Name tag: laravel-vpc
    • IPv4 CIDR block: 192.168.0.0/16
  4. Click Create VPC.

2. Create and Attach the Internet Gateway (IGW)

  1. Under VPC navigation, go to Internet Gateways → Create internet gateway.
  2. Name it laravel-igw and click Create internet gateway.
  3. Select the created gateway → click Actions → Attach to VPC.
  4. Select laravel-vpc and 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 NameTypeAvailability ZoneIPv4 CIDR BlockPurpose
laravel-public-subnet-1Publicus-east-1a192.168.0.0/24EC2 Web Server
laravel-private-subnet-1Privateus-east-1a192.168.1.0/24RDS MySQL Primary
laravel-private-subnet-2Privateus-east-1b192.168.2.0/24RDS MySQL Standby/Failover

4. Configure Route Tables

  1. Public Route Table (laravel-public-rt):
    • Go to Route Tables → Create route table. Name it laravel-public-rt under laravel-vpc.
    • Go to Routes → Edit routes → Add route:
      • Destination: 0.0.0.0/0
      • Target: Internet Gateway (laravel-igw)
    • Go to Subnet associations → Edit subnet associations → select laravel-public-subnet-1.
  2. Private Route Table (laravel-private-rt):
    • Create route table laravel-private-rt under laravel-vpc.
    • Do not attach an Internet Gateway.
    • Associate both laravel-private-subnet-1 and laravel-private-subnet-2.

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 • Port 80 • Source: 0.0.0.0/0
    • HTTPS • TCP • Port 443 • Source: 0.0.0.0/0
    • SSH • TCP • Port 22 • Source: My IP (Restrict to your personal public IP)

2. Database Security Group (laravel-sg-db)

  • VPC: laravel-vpc
  • Inbound Rules:
    • MySQL/Aurora • TCP • Port 3306 • Source: laravel-sg-web (Reference the web security group ID)

Phase 3: Launching EC2 & RDS

1. Launch the EC2 Web Server

  1. Open EC2 → click Launch Instance.
  2. Name: laravel-ec2-instance.
  3. AMI: Ubuntu Server 22.04 LTS (HVM), SSD Volume Type.
  4. Instance Type: t2.micro (or t3.small for production workloads).
  5. Key Pair: Create or select key pair laravel-key.pem and save it locally.
  6. Network Settings → click Edit:
    • VPC: laravel-vpc
    • Subnet: laravel-public-subnet-1
    • Auto-assign Public IP: Enable
    • Security Groups: Select laravel-sg-web
  7. Click Launch Instance.

2. Allocate and Associate an Elastic IP (EIP)

  1. Under EC2 → Network & Security → Elastic IPs.
  2. Click Allocate Elastic IP address → Allocate.
  3. Select the IP → Actions → Associate Elastic IP address.
  4. Select laravel-ec2-instance and confirm.

3. Provision Amazon RDS MySQL

  1. Navigate to RDS → Subnet groups → Create DB Subnet Group:
    • Name: laravel-rds-subnet-group
    • VPC: laravel-vpc
    • Add Availability Zones: us-east-1a and us-east-1b
    • Subnets: Select laravel-private-subnet-1 and laravel-private-subnet-2
  2. 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
  3. 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:

local terminal
chmod 0600 laravel-key.pem
ssh -i laravel-key.pem ubuntu@<ELASTIC_IP>

1. Update Packages & Install Base Tools

server terminal
sudo apt update && sudo apt upgrade -y
sudo apt install -y curl git unzip software-properties-common ufw mysql-client

2. Install and Start Nginx

server terminal
sudo apt install -y nginx
sudo systemctl enable nginx
sudo systemctl start nginx

3. 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:

server terminal
sudo add-apt-repository ppa:ondrej/php -y
sudo 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 status
php -v
sudo systemctl status php8.3-fpm --no-pager

4. Install Composer

server terminal
cd ~
curl -sS https://getcomposer.org/installer -o composer-setup.php
sudo php composer-setup.php --install-dir=/usr/local/bin --filename=composer
rm composer-setup.php
composer --version

5. Install Node.js & NPM (NodeSource v22.x)

server terminal
curl -fsSL https://dev.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
node -v && npm -v

Phase 5: Dedicated Deployer & Codebase Setup

Running web applications under an unprivileged user increases security isolation:

1. Create Dedicated Deployment User

server terminal
sudo adduser --disabled-password --gecos "" deployer
sudo usermod -a -G deployer www-data

2. Configure GitHub Deploy Keys

Switch to the deployer user and generate a dedicated SSH key pair:

server terminal
sudo su - deployer
mkdir -p ~/.ssh && chmod 700 ~/.ssh
ssh-keygen -t ed25519 -f ~/.ssh/github_deploy -C "deployer@aws-laravel"
# Configure SSH config
cat << 'EOF' > ~/.ssh/config
Host github.com
IdentityFile ~/.ssh/github_deploy
IdentitiesOnly yes
EOF
chmod 600 ~/.ssh/config
chmod 600 ~/.ssh/github_deploy
# Output public key to add to GitHub
cat ~/.ssh/github_deploy.pub

3. Clone and Configure Laravel

Still as the deployer user:

server terminal
cd /home/deployer
git clone git@github.com:<ORG_OR_USERNAME>/<REPOSITORY>.git code
cd code
# Install dependencies
composer install --no-dev --optimize-autoloader
# Environment configuration
cp .env.example .env
php artisan key:generate

Open .env and configure your database and production parameters:

.env
APP_NAME="Laravel Production"
APP_ENV=production
APP_DEBUG=false
APP_URL=https://yourdomain.com
DB_CONNECTION=mysql
DB_HOST=laravel-mysql-db.cxxxxxxx.us-east-1.rds.amazonaws.com
DB_PORT=3306
DB_DATABASE=laravel_db
DB_USERNAME=admin
DB_PASSWORD=YourStrongPasswordHere

Run database migrations and cache configurations:

server terminal
php artisan migrate --force
php artisan storage:link
php artisan config:cache
php artisan route:cache
php artisan view:cache

Compile frontend assets:

server terminal
npm install
npm run build

Exit back to the ubuntu administrative user:

server terminal
exit

Phase 6: Web Server Configuration

1. Configure Nginx Virtual Host

Remove the default configuration and create a new server block:

server terminal
sudo rm -f /etc/nginx/sites-enabled/default
sudo nano /etc/nginx/sites-available/laravel.conf

Add the following optimized virtual host configuration:

/etc/nginx/sites-available/laravel.conf
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:

server terminal
sudo ln -s /etc/nginx/sites-available/laravel.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

2. Set Up Directory Permissions

Configure ownership and permissions so Nginx (www-data) and deployer can read and write required storage directories:

server terminal
sudo chown -R deployer:www-data /home/deployer/code
sudo chmod -R 775 /home/deployer/code/storage
sudo chmod -R 775 /home/deployer/code/bootstrap/cache

Grant execute traversal permissions on the home directory:

server terminal
chmod 755 /home/deployer

Phase 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:

server terminal
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Certbot 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:

server terminal
sudo certbot renew --dry-run

Post-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 optimize executed 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.