DEV Community

Cover image for Deploying Laravel on Shared Hosting (No SSH Required)
HoudaifaDevBS
HoudaifaDevBS

Posted on • Originally published at Medium

Deploying Laravel on Shared Hosting (No SSH Required)

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.

Why Laravel Deployment on Shared Hosting Is Different

1. Project File Structure

A typical Laravel project looks like this:

project/  
 ├── app/  
 ├── bootstrap/  
 ├── config/  
 ├── public/  
 ├── storage/  
 ├── vendor/  
 └── artisan
Enter fullscreen mode Exit fullscreen mode

However, shared hosting usually exposes only one public directory:

├── public_html/
Enter fullscreen mode Exit fullscreen mode

2. No SSH, No commands

If there is SSH access, obviously, there will be:

  1. No composer dependencies can be installed
  2. No asset building since we no longer have "npm run"
  3. No migrations can be executed
  4. No cache clearing or optimizing

How to solve the situation step-by-step

Step 1: Prepare Your Application Locally

First, I clone my Laravel project into a production-ready copy so I don't affect my development setup:

├── laravel_app/  
├── laravel_app_prod/
Enter fullscreen mode Exit fullscreen mode

Inside laravel_app_prod, I prepare the application for production.

// 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

Step 2 — Re-organize Project File Structure

To match shared hosting file structure, I move only public/ folder content inside the new public_html/ folder, like this:

public_html/  <-------------------|  
laravel_app_prod/                 |  
 ├── app/                         |  
 ├── bootstrap/                   |  
 ├── config/                      |  
 ├── public/ ---------------------|  
 ├── storage/             
 ├── vendor/              
 └── artisan
Enter fullscreen mode Exit fullscreen mode

Step 3 — Fix Laravel Paths in index.php

Now, we should do one important step to make the app boot correctly, so we update the paths in public/index.php:

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

This is a critical step — incorrect paths will cause a 500 error.

Step 4 — Set Up The Database Using cPanel

From cPanel:

  1. Open MySQL Database Wizard
  2. Create a database
  3. Create a database user
  4. Assign the user ALL PRIVILEGES

Then update the .env file inside laravel_app_prod:

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

⚠️ Never enable APP_DEBUG=true in production.

Step 5 — Temporary Deploy Route (Token Protected)

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:

DEPLOY_TOKEN=verySecretRandomToken123
Enter fullscreen mode Exit fullscreen mode

Add the route in routes/web.php:

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

This ensures only someone with the correct token can trigger deployment.

Step 6 — Upload Files to the Server

Using cPanel File Manager:

  1. Zip laravel_app_prod/
  2. Zip the contents of public/
  3. Upload:
    • Put laravel_app_prod outside public_html
    • Public files inside public_html

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.

Step 7 — Set Critical Permissions

Laravel requires write access to specific folders. If these aren't set, your logs won't write and sessions won't save.

  1. In cPanel File Manager, navigate to laravel_app_prod/storage.
  2. Right-click → Change Permissions.
  3. Set to 775 (User: Read/Write/Execute, Group: Read/Write/Execute, World: Read/Execute).
  4. Do the same for laravel_app_prod/bootstrap/cache.

🛑 Note: Never set folders to 777. It is a major security risk on shared hosting.

Step 8 — Run Migrations and Remove the Deploy Route

Open in the browser:

https://your-domain.com/deploy/verySecretRandomToken123
Enter fullscreen mode Exit fullscreen mode

If everything is correct, you'll see:

Deployment completed successfully
Enter fullscreen mode Exit fullscreen mode

Immediately after that:

❗ Delete the deploy route

❗ Remove DEPLOY_TOKEN from .env

This is critical for security.

Common Issues Faced

500 Internal Server Error

  • Incorrect file paths in index.php
  • Wrong PHP version (use cPanel MultiPHP Manager)
  • Incorrect folder permissions (storage 775, bootstrap/cache 775)

403 Unauthorized

  • Unmatched route token with DEPLOY_TOKEN

Final Thoughts

Shared hosting is not ideal for Laravel, but it's still very common for:

  • Client projects
  • MVPs
  • Budget-constrained deployments

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:

  1. Run migrations on your local machine.
  2. Export your local database as an .sql file using your local tool.
  3. Go to cPanel > phpMyAdmin on the server.
  4. Import the .sql file directly.

This is safer because it doesn't require executable logic in your production routes.

🔗 Stay Connected

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

Top comments (2)

Collapse
 
xwero profile image
david duymelinck • Edited

With a CI/CD solution you don't have to go through all that local setup. You can run the preparation and sftp to the server.

A solution to trigger artisan commands after deployment is to add a cron job that checks for a .deployed file.
When that is present it runs a batch script with the Artisan commands, and whatever else that is needed. So no need to expose a deploy url.

It is not a shared hosting problem that there is no ssh access, that is a hosting company problem.
Even cheap hosting companies should provide ssh access, if they don't look for one that does.
This will not only help you, but also the client.

Collapse
 
houdaifadev profile image
HoudaifaDevBS

Indeed, this is a better way to handle it, even without exposing any public route that may be dangerous if forgotten

Actually, I encountered that a few times when I was deploying the first version app into the client's already had "shared hosting" when freelancing. Of course, I would never try to buy a server with no SSH if I am deploying a Laravel app