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
However, shared hosting usually exposes only one public directory:
├── public_html/
2. No SSH, No commands
If there is SSH access, obviously, there will be:
- 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
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/
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
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
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());
This is a critical step — incorrect paths will cause a 500 error.
Step 4 — Set Up The Database Using cPanel
From cPanel:
- Open MySQL Database Wizard
- Create a database
- Create a database user
- 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)
⚠️ 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
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;
});
This ensures only someone with the correct token can trigger deployment.
Step 6 — Upload Files to the Server
Using cPanel File Manager:
- Zip
laravel_app_prod/ - Zip the contents of
public/ - Upload:
- Put
laravel_app_prodoutsidepublic_html - Public files inside
public_html
- Put
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.
- 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.
🛑 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
If everything is correct, you'll see:
Deployment completed successfully
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:
- Run migrations on your local machine.
- Export your local database as an
.sqlfile using your local tool. - Go to cPanel > phpMyAdmin on the server.
- Import the
.sqlfile 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.
- Follow me on LinkedIn
- Follow me here on Medium and join my mailing list for more in-depth content and tutorials!
Found this article useful?
🙏 Show your support by clapping 👏, subscribing 🔔, sharing to social networks
Top comments (2)
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.
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