For teams deploying Laravel apps to shared hosting using Git push, post-receive hooks, and manual cPanel setups, this SOP enables:
- Clean deployment via Git
- MySQL database configuration
- Environment setup via
.env
- Seamless handling of subdomains vs. root domains
- Production-ready deployment without root access
📦 Assumptions
- Shared hosting (e.g., A2, Hostinger, Bluehost) with cPanel + SSH
- Git, Composer, and PHP (>= 7.4) are available
- Laravel version 8+
- DNS already configured to hosting server
🔐 SSH Key Setup (One-Time)
- Generate SSH key on your local machine:
ssh-keygen -t rsa -b 4096 -C "[email protected]"
- Copy the public key:
cat ~/.ssh/id_rsa.pub
- SSH into the server and append it to
~/.ssh/authorized_keys
:
nano ~/.ssh/authorized_keys
# Paste the key and save
🔧 Laravel Deployment Structure
❗ Subdomain vs. Primary Domain: Key Differences
Context | Laravel Folder Placement | Document Root Target | Notes |
---|---|---|---|
Subdomain | Full Laravel app (including /public ) |
/subdomain.domain.com/public |
Clean separation; recommended for staging/UAT |
Primary Domain | Laravel core outside /public_html
|
/public_html/ |
Must expose only public/ , rest stays outside |
🌐 Subdomain Deployment (Recommended)
🔹 Step-by-Step
-
Create Subdomain in cPanel
- Navigate to
Domains > Subdomains
- Example:
uat.domain.com
- Set Document Root:
/home/
/subdomains/uat.domain.com/public
- Navigate to
Create Bare Git Repo on Server
mkdir -p /home//repos/laravel-uat.git
cd /home//repos/laravel-uat.git
git init --bare
-
Configure
post-receive
Hook
nano hooks/post-receive
#!/bin/sh
GIT_WORK_TREE=/home//subdomains/uat.domain.com git checkout -f
cd /home//subdomains/uat.domain.com
composer install --no-dev
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
chmod +x hooks/post-receive
- Deploy from Local or GitHub
git remote add live ssh://@/home//repos/laravel-uat.git
git push live uat
git push live master
🏠 Primary Domain Deployment (domain.com
)
❗ Caveat
Shared hosting ties domain.com
to /public_html
. You cannot place the full Laravel folder in /public_html
. Use the approach below:
🔹 Directory Structure
/home//laravel_main
/home//public_html ← contains only files from `/public`
🔹 Steps
Push Full Laravel App to
~/laravel_main
Copy
/public
Contents to/public_html
cp -R laravel_main/public/* public_html/
- Edit
public_html/index.php
Update paths:
require __DIR__.'/../laravel_main/vendor/autoload.php';
$app = require_once __DIR__.'/../laravel_main/bootstrap/app.php';
- Install Dependencies & Migrate
cd ~/laravel_main
composer install --no-dev
php artisan migrate --force
php artisan key:generate
php artisan config:cache
- Set Permissions
chmod -R 775 storage
chmod -R 775 bootstrap/cache
🛠️ MySQL Database Setup
- Go to cPanel > MySQL Databases
- Create database:
db_laravel
- Create user:
user_laravel
- Assign user to database with All Privileges
- Update
.env
:
DB_CONNECTION=mysql
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=db_laravel
DB_USERNAME=user_laravel
DB_PASSWORD=StrongPassword123
⚠️ Common Issues & Fixes
Problem | Cause | Fix |
---|---|---|
500 Internal Server Error | Wrong index paths / file permissions | Fix index.php , run chmod -R 775 storage bootstrap/cache
|
Composer not installed | Shared host restriction | Install locally and upload vendor/
|
.env missing |
Laravel config errors | Upload .env , run php artisan config:cache
|
Directory listing visible | Wrong document root | Set root to public/ , not project root |
Artisan fails | PHP CLI outdated | Check php -v , request upgrade or use compatible Laravel version |
📌 Summary Table
Context | Code Location | Public Files Location | Method |
---|---|---|---|
Subdomain | ~/subdomains/app/ |
~/subdomains/app/public |
Full Laravel folder used |
Primary Domain | ~/laravel_main |
~/public_html |
public folder only exposed |
✅ Post-Deployment Checklist
- [ ]
.env
configured correctly - [ ]
php artisan key:generate
run - [ ]
storage
andbootstrap/cache
are writable - [ ] Composer dependencies installed
- [ ] Artisan commands run:
config:cache
,route:cache
,view:cache
- [ ] Database connection tested
- [ ] Subdomain or root domain resolving properly
- [ ] Debug mode off in production (
APP_DEBUG=false
)
P.S. - This post was refined with the help of ChatGPT and includes an image sourced from the web. If anyone has concerns or would like the content removed, I’m open to doing so.