Complete step-by-step guide to deploying Laravel API + React frontend on cPanel shared hosting
Before starting, confirm your cPanel hosting plan supports the following:
| Requirement | Minimum | Where to check |
|---|---|---|
| PHP version | 8.2 or higher | cPanel → Software → Select PHP Version |
| MySQL | 5.7 or 8.0 | cPanel → Databases → MySQL Databases |
| SSH / Terminal access | Required for Composer | cPanel → Advanced → Terminal |
| Composer | 2.x | Available via SSH: composer --version |
| Node.js | 18+ (local machine only) | Build React on your PC, upload the dist/ folder |
| Disk space | 500 MB minimum | cPanel → Files → Disk Usage |
composer install on your local machine targeting PHP 8.2, then upload the vendor/ folder via FTP. See Step 6 for details.Go to cPanel → Select PHP Version → Extensions and enable:
pdo_mysql # Database driver mbstring # String handling openssl # Encryption / JWT tokenizer # Laravel requirement xml # XML parsing ctype # Character type functions fileinfo # File validation json # JSON support bcmath # Precise arithmetic
yourusername_agencypm → click Create Database
.env file.The recommended setup uses two subdomains — one for the API and one for the frontend:
| Subdomain | Purpose | Document root |
|---|---|---|
api.yourdomain.com |
Laravel backend API | public_html/api/public |
app.yourdomain.com |
React frontend | public_html/app |
api · Domain: yourdomain.com · Document root: public_html/api/public → click Create
app · Domain: yourdomain.com · Document root: public_html/app → click Create
public/ folder, NOT the project root. If you point to the project root, your .env file and application code will be publicly accessible.public_html/
api
backend/ folder contents (not the folder itself)
public_html/api/ → click Upload → upload your zip file
/public_html/api/
yourdomain.com, Port: 21, credentials from cPanel → FTP Accounts.public_html/api/ → find .env.example → right-click → Copy → name it .env
.env → Edit → update all values below
# Application APP_NAME="Agency PM" APP_ENV=production APP_KEY= # Will generate this in Step 7 APP_DEBUG=false APP_URL=https://api.yourdomain.com # Database — use values from Step 2 DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=yourusername_agencypm DB_USERNAME=yourusername_dbuser DB_PASSWORD=YourStrongPassword123! # Frontend URL (for CORS) FRONTEND_URL=https://app.yourdomain.com SANCTUM_STATEFUL_DOMAINS=app.yourdomain.com # Session & Cache CACHE_DRIVER=file SESSION_DRIVER=file QUEUE_CONNECTION=sync
Open cPanel → Advanced → Terminal (or connect via SSH with your cPanel credentials):
# Navigate to your project cd ~/public_html/api # Install PHP dependencies (production mode) composer install --no-dev --optimize-autoloader # Generate app key php artisan key:generate # Set storage permissions chmod -R 775 storage bootstrap/cache chown -R $(whoami):$(whoami) storage bootstrap/cache
If your host does not provide terminal access:
backend/ folder → run: composer install --no-dev --optimize-autoloader
php artisan key:generate --show → copy the output (e.g. base64:Abc123...)
.env file on the server → set APP_KEY=base64:Abc123...
vendor/ folder to public_html/api/vendor/ via FTP or File Manager
vendor/ folder is large (~50–100MB). Uploading via File Manager as a ZIP is faster than uploading thousands of individual files via FTP. Zip it locally, upload, then extract on the server.# Run all database migrations php artisan migrate --force # Open Laravel Tinker to create your admin account php artisan tinker
Inside Tinker, paste and run:
App\Models\User::create([ 'name' => 'Admin', 'email' => 'admin@youragency.com', 'password' => bcrypt('YourStrongPassword!'), 'role' => 'super_admin', ]); # Press Ctrl+D or type exit() to leave Tinker
If you have no SSH access, import migrations manually:
database/migrations/
-- Run in phpMyAdmin SQL tab INSERT INTO users (name, email, password, role, is_active, created_at, updated_at) VALUES ( 'Admin', 'admin@youragency.com', '$2y$12$paste_bcrypt_hash_here', -- generate at bcrypt-generator.com 'super_admin', 1, NOW(), NOW() );
public_html/api/public/.htaccessLaravel includes this file by default. Verify it contains:
<IfModule mod_rewrite.c> RewriteEngine On RewriteRule ^(.*)$ index.php [QSA,L] # Handle Authorization header (required for Sanctum tokens) RewriteCond %{HTTP:Authorization} . RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] </IfModule> # Security headers <IfModule mod_headers.c> Header always set X-Content-Type-Options nosniff Header always set X-Frame-Options DENY Header always set X-XSS-Protection "1; mode=block" </IfModule>
public_html/app/.htaccessCreate this file so React Router works correctly (all routes serve index.html):
Options -MultiViews <IfModule mod_rewrite.c> RewriteEngine On RewriteBase / # Serve existing files and folders directly RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d # Everything else goes to React's index.html RewriteRule ^ index.html [QSA,L] </IfModule> # Cache static assets <IfModule mod_expires.c> ExpiresActive On ExpiresByType image/png "access plus 1 year" ExpiresByType text/css "access plus 1 month" ExpiresByType application/javascript "access plus 1 month" </IfModule>
# Navigate to frontend folder cd frontend # Create production environment file echo 'VITE_API_URL=https://api.yourdomain.com/api/v1' > .env.production # Install dependencies npm install # Build for production npm run build # The dist/ folder is ready to upload ls dist/
dist/ (not the folder itself) → zip it
public_html/app/ → Upload the zip file
public_html/app/ as destination
public_html/app/ → New File → name it .htaccess → paste the contents from Step 8
public_html/app/ should contain: index.html, .htaccess, and an assets/ folder with your JS/CSS bundles.Laravel must allow requests from your React frontend domain. Open public_html/api/config/cors.php and update:
<?php return [ 'paths' => ['api/*'], 'allowed_methods' => ['*'], 'allowed_origins' => [ 'https://app.yourdomain.com', ], 'allowed_origins_patterns' => [], 'allowed_headers' => ['*'], 'exposed_headers' => [], 'max_age' => 0, 'supports_credentials' => false, ];
Also ensure Sanctum is configured in config/sanctum.php:
'stateful' => explode(',', env( 'SANCTUM_STATEFUL_DOMAINS', 'app.yourdomain.com' )),
Clear the config cache after making changes:
php artisan config:clear php artisan cache:clear
Open your browser or use a tool like Postman:
# Should return {"message":"Unauthenticated."} — means API is working GET https://api.yourdomain.com/api/v1/auth/me # Test login endpoint POST https://api.yourdomain.com/api/v1/auth/login Content-Type: application/json { "email": "admin@youragency.com", "password": "YourStrongPassword!" }
| Check | Expected result | Status |
|---|---|---|
https://api.yourdomain.com/api/v1/auth/login | Returns JSON with access_token | Test this |
https://app.yourdomain.com | Login page loads | Test this |
| Login with admin account | Redirects to dashboard | Test this |
| Create a project | Project appears in list | Test this |
| Add a team member | Member visible in Team page | Test this |
| Open kanban board | Drag & drop works | Test this |
| Problem | Likely cause | Fix |
|---|---|---|
| 500 Internal Server Error on API | Missing APP_KEY or wrong .env | Run php artisan key:generate; check storage/logs/laravel.log |
| 404 Not Found on API routes | .htaccess not working / mod_rewrite disabled | Contact host to enable mod_rewrite; verify public/.htaccess exists |
| CORS error in browser console | Frontend URL not in allowed_origins | Update config/cors.php; run php artisan config:clear |
| React app shows blank page | React Router needs .htaccess fallback | Create .htaccess in app folder (see Step 8) |
| Cannot connect to database | Wrong DB credentials or DB_HOST | Use 127.0.0.1 not localhost; double-check username format (cpanelusername_dbname) |
| Composer not found via SSH | Composer not in PATH | Try php /usr/local/bin/composer install or download composer.phar manually |
| Storage permission error | Wrong file permissions | Run chmod -R 775 storage bootstrap/cache |
| Login works but token rejected | SANCTUM_STATEFUL_DOMAINS mismatch | Set SANCTUM_STATEFUL_DOMAINS=app.yourdomain.com in .env (no https://) |
| PHP version error during composer install | Server using PHP < 8.2 | cPanel → Software → Select PHP Version → switch to PHP 8.2 |
# Via SSH tail -f ~/public_html/api/storage/logs/laravel.log # Via File Manager # Navigate to: public_html/api/storage/logs/laravel.log → right-click → View
https://app.yourdomain.com with your team. Create client accounts from the Team page — clients will see only their own portal.