Tools
Tools: Deploying Laravel on Shared Hosting (No SSH Required)
2026-02-07
0 views
admin
Why Laravel Deployment on Shared Hosting Is Different ## 1. Project File Structure ## 2. No SSH, No commands ## How to solve the situation step-by-step ## Step 1: Prepare Your Application Locally ## Step 2 — Re-organize Project File Structure ## Step 3 — Fix Laravel Paths in index.php ## Step 4 — Set Up The Database Using cPanel ## Step 5 — Temporary Deploy Route (Token Protected) ## Step 6 — Upload Files to the Server ## Step 7 — Set Critical Permissions ## Step 8 — Run Migrations and Remove the Deploy Route ## Common Issues Faced ## 500 Internal Server Error ## 403 Unauthorized ## Final Thoughts ## 🔗 Stay Connected Have you ever wondered if you can deploy a Laravel application on shared hosting without SSH? The answer is YES. I faced this exact situation during my early freelance journey: a client with a tight budget needed to see the first version of his Laravel website online before moving forward. The hosting environment had no SSH, no Composer, no Artisan, no NPM. So I had to figure out a safe and reliable alternative. In this article, I'll share the exact workflow I use to deploy Laravel applications on shared hosting without SSH access. A typical Laravel project looks like this: However, shared hosting usually exposes only one public directory: If there is SSH access, obviously, there will be: First, I clone my Laravel project into a production-ready copy so I don't affect my development setup: Inside laravel_app_prod, I prepare the application for production. To match shared hosting file structure, I move only public/ folder content inside the new public_html/ folder, like this: Now, we should do one important step to make the app boot correctly, so we update the paths in public/index.php: This is a critical step — incorrect paths will cause a 500 error. Then update the .env file inside laravel_app_prod: ⚠️ Never enable APP_DEBUG=true in production. Coming to the important part, no SSH, no problem, we can temporarily execute Artisan commands via a protected web route. ⚠️ This route must be removed immediately after deployment. Add a secret token to .env: Add the route in routes/web.php: This ensures only someone with the correct token can trigger deployment. Using cPanel File Manager: Then extract both archives. Why did we put laravel_app_prod outside public_html?
This is a crucial security measure. By placing your core application logic, and most importantly your .env file, one level above the public directory, you ensure that they are physically impossible to access via a web browser. Even if your server misconfigures and starts serving PHP files as text (which happens!), your credentials remain hidden because they live outside the public root. Laravel requires write access to specific folders. If these aren't set, your logs won't write and sessions won't save. 🛑 Note: Never set folders to 777. It is a major security risk on shared hosting. If everything is correct, you'll see: Immediately after that: ❗ Delete the deploy route
❗ Remove DEPLOY_TOKEN from .env This is critical for security. Shared hosting is not ideal for Laravel, but it's still very common for: With proper preparation and a secure workflow, Laravel applications can run reliably even without SSH access. 💡 Pro Tip: The Safer Database Alternative
If you are uncomfortable running migrations via a web route (which carries security risks), you can use the Export/Import method: This is safer because it doesn't require executable logic in your production routes. Follow me for more Laravel tutorials, dev tips, deployment workflows and solving real-world production headaches. Found this article useful?
🙏 Show your support by clapping 👏, subscribing 🔔, sharing to social networks Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK:
project/ ├── app/ ├── bootstrap/ ├── config/ ├── public/ ├── storage/ ├── vendor/ └── artisan Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
project/ ├── app/ ├── bootstrap/ ├── config/ ├── public/ ├── storage/ ├── vendor/ └── artisan CODE_BLOCK:
project/ ├── app/ ├── bootstrap/ ├── config/ ├── public/ ├── storage/ ├── vendor/ └── artisan CODE_BLOCK:
├── public_html/ Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
├── public_html/ CODE_BLOCK:
├── public_html/ CODE_BLOCK:
├── laravel_app/ ├── laravel_app_prod/ Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
├── laravel_app/ ├── laravel_app_prod/ CODE_BLOCK:
├── laravel_app/ ├── laravel_app_prod/ CODE_BLOCK:
// clearing the cache php artisan config:clear php artisan cache:clear php artisan route:clear php artisan view:clear // installing production dependencies and optimize composer install --no-dev --optimize-autoloader // building frontend assets npm run build Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
// clearing the cache php artisan config:clear php artisan cache:clear php artisan route:clear php artisan view:clear // installing production dependencies and optimize composer install --no-dev --optimize-autoloader // building frontend assets npm run build CODE_BLOCK:
// clearing the cache php artisan config:clear php artisan cache:clear php artisan route:clear php artisan view:clear // installing production dependencies and optimize composer install --no-dev --optimize-autoloader // building frontend assets npm run build CODE_BLOCK:
public_html/ <-------------------| laravel_app_prod/ | ├── app/ | ├── bootstrap/ | ├── config/ | ├── public/ ---------------------| ├── storage/ ├── vendor/ └── artisan Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
public_html/ <-------------------| laravel_app_prod/ | ├── app/ | ├── bootstrap/ | ├── config/ | ├── public/ ---------------------| ├── storage/ ├── vendor/ └── artisan CODE_BLOCK:
public_html/ <-------------------| laravel_app_prod/ | ├── app/ | ├── bootstrap/ | ├── config/ | ├── public/ ---------------------| ├── storage/ ├── vendor/ └── artisan CODE_BLOCK:
if (file_exists( $maintenance = __DIR__ . '/../laravel_app_prod/storage/framework/maintenance.php') ) { require $maintenance; } require __DIR__ . '/../laravel_app_prod/vendor/autoload.php'; (require_once __DIR__ . '/../laravel_app_prod/bootstrap/app.php') ->handleRequest(Request::capture()); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
if (file_exists( $maintenance = __DIR__ . '/../laravel_app_prod/storage/framework/maintenance.php') ) { require $maintenance; } require __DIR__ . '/../laravel_app_prod/vendor/autoload.php'; (require_once __DIR__ . '/../laravel_app_prod/bootstrap/app.php') ->handleRequest(Request::capture()); CODE_BLOCK:
if (file_exists( $maintenance = __DIR__ . '/../laravel_app_prod/storage/framework/maintenance.php') ) { require $maintenance; } require __DIR__ . '/../laravel_app_prod/vendor/autoload.php'; (require_once __DIR__ . '/../laravel_app_prod/bootstrap/app.php') ->handleRequest(Request::capture()); CODE_BLOCK:
APP_NAME=MyApp APP_ENV=production APP_DEBUG=false // very important (security) APP_URL=https://your-app-domain DB_DATABASE=database_name DB_USERNAME=database_user DB_PASSWORD=database_password // (you already saved it) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
APP_NAME=MyApp APP_ENV=production APP_DEBUG=false // very important (security) APP_URL=https://your-app-domain DB_DATABASE=database_name DB_USERNAME=database_user DB_PASSWORD=database_password // (you already saved it) CODE_BLOCK:
APP_NAME=MyApp APP_ENV=production APP_DEBUG=false // very important (security) APP_URL=https://your-app-domain DB_DATABASE=database_name DB_USERNAME=database_user DB_PASSWORD=database_password // (you already saved it) CODE_BLOCK:
DEPLOY_TOKEN=verySecretRandomToken123 Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
DEPLOY_TOKEN=verySecretRandomToken123 CODE_BLOCK:
DEPLOY_TOKEN=verySecretRandomToken123 COMMAND_BLOCK:
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Route; Route::get('/deploy/{token}', function ($token) { abort_unless($token === env('DEPLOY_TOKEN'), 403); // 1. Run Migrations & Clear Cache Artisan::call('migrate', ['--force' => true]); Artisan::call('optimize:clear'); // 2. Fix Storage Link (The Custom Fix) // We point to the 'public_html' folder using $_SERVER['DOCUMENT_ROOT'] $targetFolder = storage_path('app/public'); $linkFolder = $_SERVER['DOCUMENT_ROOT'] . '/storage'; if (!file_exists($linkFolder)) { symlink($targetFolder, $linkFolder); $storageStatus = 'Storage link created successfully.'; } else { $storageStatus = 'Storage link already exists.'; } return "Deployment completed.<br>" . "Migrations run.<br>" . "Cache cleared.<br>" . $storageStatus; }); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Route; Route::get('/deploy/{token}', function ($token) { abort_unless($token === env('DEPLOY_TOKEN'), 403); // 1. Run Migrations & Clear Cache Artisan::call('migrate', ['--force' => true]); Artisan::call('optimize:clear'); // 2. Fix Storage Link (The Custom Fix) // We point to the 'public_html' folder using $_SERVER['DOCUMENT_ROOT'] $targetFolder = storage_path('app/public'); $linkFolder = $_SERVER['DOCUMENT_ROOT'] . '/storage'; if (!file_exists($linkFolder)) { symlink($targetFolder, $linkFolder); $storageStatus = 'Storage link created successfully.'; } else { $storageStatus = 'Storage link already exists.'; } return "Deployment completed.<br>" . "Migrations run.<br>" . "Cache cleared.<br>" . $storageStatus; }); COMMAND_BLOCK:
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Route; Route::get('/deploy/{token}', function ($token) { abort_unless($token === env('DEPLOY_TOKEN'), 403); // 1. Run Migrations & Clear Cache Artisan::call('migrate', ['--force' => true]); Artisan::call('optimize:clear'); // 2. Fix Storage Link (The Custom Fix) // We point to the 'public_html' folder using $_SERVER['DOCUMENT_ROOT'] $targetFolder = storage_path('app/public'); $linkFolder = $_SERVER['DOCUMENT_ROOT'] . '/storage'; if (!file_exists($linkFolder)) { symlink($targetFolder, $linkFolder); $storageStatus = 'Storage link created successfully.'; } else { $storageStatus = 'Storage link already exists.'; } return "Deployment completed.<br>" . "Migrations run.<br>" . "Cache cleared.<br>" . $storageStatus; }); CODE_BLOCK:
https://your-domain.com/deploy/verySecretRandomToken123 Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
https://your-domain.com/deploy/verySecretRandomToken123 CODE_BLOCK:
https://your-domain.com/deploy/verySecretRandomToken123 CODE_BLOCK:
Deployment completed successfully Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
Deployment completed successfully CODE_BLOCK:
Deployment completed successfully - No composer dependencies can be installed
- No asset building since we no longer have "npm run"
- No migrations can be executed
- No cache clearing or optimizing - Open MySQL Database Wizard
- Create a database
- Create a database user
- Assign the user ALL PRIVILEGES - Zip laravel_app_prod/
- Zip the contents of public/
- Upload: Put laravel_app_prod outside public_html Public files inside public_html
- Put laravel_app_prod outside public_html
- Public files inside public_html - Put laravel_app_prod outside public_html
- Public files inside public_html - In cPanel File Manager, navigate to laravel_app_prod/storage.
- Right-click → Change Permissions.
- Set to 775 (User: Read/Write/Execute, Group: Read/Write/Execute, World: Read/Execute).
- Do the same for laravel_app_prod/bootstrap/cache. - Incorrect file paths in index.php
- Wrong PHP version (use cPanel MultiPHP Manager)
- Incorrect folder permissions (storage 775, bootstrap/cache 775) - Unmatched route token with DEPLOY_TOKEN - Client projects
- Budget-constrained deployments - Run migrations on your local machine.
- Export your local database as an .sql file using your local tool.
- Go to cPanel > phpMyAdmin on the server.
- Import the .sql file directly. - Follow me on LinkedIn
- Follow me here on Medium and join my mailing list for more in-depth content and tutorials!
how-totutorialguidedev.toaimlservernetworkmysqldatabase