How to Deploy Laravel 13 on Shared Hosting (Without Moving Files)
There’s a particular kind of frustration in finishing a Laravel app, feeling proud of it, then realizing you still have to deploy it. Most tutorials make this harder than it needs to be: cut your public/ folder apart, scatter its contents into public_html/, rewrite the paths inside index.php. It works — until your next git pull overwrites something and the whole site 500s.
This guide takes a cleaner path. You upload your project as-is, add one .htaccess file, and let the server route everything quietly into place. Nothing moves, so git pull never breaks your paths — and you’ll understand why each step works, so you can debug confidently instead of copy-pasting and praying.
Table of Contents
Is shared hosting actually okay for Laravel?
Short answer: for a lot of projects, yes. Shared hosting earned its bad reputation in the era of PHP 5.6 and no SSH, but that reputation has outlived the reality. A modern plan with PHP 8.3, SSH, and Composer runs a real Laravel app just fine.
It’s a good fit for portfolio sites, client brochure sites, small SaaS MVPs, admin panels — anything in the hundreds-to-low-thousands of daily visitors. The economics are hard to argue with: roughly $3–8/month versus $12–50 for a VPS you’d also have to maintain.
What your host actually needs to support
Laravel 13 raised the floor a little, so check your host against this list before you start rather than discovering a missing piece halfway through.
- PHP 8.3 or newer. The big one. 8.3 is the absolute minimum; 8.4 is the safe default for a new project in 2026 — recent but settled, so every package already works on it. 8.5 works too, but if you hit an odd dependency error, drop to 8.4. If your host tops out at 8.2, Laravel 13 simply won’t install — no workaround.
- SSH access. Not mandatory, but the difference between a 10-minute deploy and an hour of FTP fiddling. It lets you run Composer and Artisan directly on the server.
- Composer. Modern Laravel has dozens of dependencies; Composer installs them. Most developer-friendly hosts pre-install it.
- Git. Optional but transformative — clone once and pull updates later instead of re-uploading every time.
- MySQL 8.0+ or PostgreSQL 13+, plus the PHP extensions Laravel expects (OpenSSL, PDO, Mbstring, Tokenizer, XML, Ctype, BCMath). Standard almost everywhere worth using.
If your host already ticks these boxes, skip ahead to the deployment steps or if you just want something that’s confirmed to work with everything in this guide, Hostinger’s Premium plan is the setup the examples below are based on.
Choosing a host: what to actually look for
Rather than rank providers, it’s more useful to know the few things that actually matter for Laravel — once you do, you can judge any host yourself.
PHP version is the dealbreaker. This is where most cheap plans quietly disqualify themselves: a lot of budget hosts still cap shared plans at 8.1 or 8.2, a hard stop for Laravel 13. Confirm 8.3+ is selectable in the control panel before you pay.
SSH should be included, not an upsell. Some hosts lock it behind a higher tier, which defeats the point of going budget. You want it on the entry plan.
Composer and Git pre-installed save real time. You can usually install Composer manually when it’s missing, but it’s twenty minutes of yak-shaving you didn’t need.
A free first-year domain is a genuine saving. A .com runs $10–15/year, so bundled with annual hosting it meaningfully offsets the cost — just note the date, since it renews at full price in year two.
If you’d rather weigh alternatives, SiteGround and A2 Hosting are both solid and developer-friendly; they cost a bit more, and as always, confirm PHP 8.3+ on the specific plan.
Why this guide uses Hostinger as the example
The steps below use Hostinger‘s Premium shared plan, because it happens to tick every box above at the low end of the price range:
- SSH on the entry plan — many hosts at this price make you upgrade for shell access, which is what makes the fast SSH deploy here possible without paying more.
- PHP up to 8.5, selectable in hPanel — covers Laravel 13’s 8.3 minimum with room to spare, in a couple of clicks rather than a support ticket.
- Composer and Git both available — clone and install in minutes instead of setting up tooling first.
- LiteSpeed, free SSL, weekly backups, and a free first-year domain — decent performance and HTTPS with no extra setup or fees.
Full disclosure: the Hostinger links are affiliate links, so signing up through one earns this site a commission at no extra cost to you. There’s a 30-day money-back guarantee, so trying carries little risk — and if you already have a host that meets the checklist above, this guide works identically on any cPanel or hPanel setup.
Two ways to deploy
There are two routes below. Method 1 (SSH + Git) is the one to use if you possibly can — it’s faster, cleaner, and makes future updates trivial. Method 2 (FileZilla) exists for when SSH genuinely isn’t available; it reaches the same destination with more manual steps.
Both rely on the same core trick, so it’s worth understanding it before you start.
The one idea that makes this work
Laravel serves from its public/ directory — that’s where index.php lives, and it’s deliberately the only folder meant to be web-accessible. Everything else (your .env, app/ code, vendor/ libraries) belongs outside the web root. Shared hosting fights this: it points your domain at a fixed folder, usually public_html/, and rarely lets you change that to public_html/public.
The messy fix everyone reaches for is to move public/‘s contents up into public_html/ and edit index.php to find the rest. The cleaner fix — this guide’s — leaves everything where Laravel put it and adds one small .htaccess rule that forwards every request into public/. The visitor’s URL never shows it; Laravel runs exactly as it does on your machine. Here’s the flow:
Visitor requests yourdomain.com/about
│
▼
public_html/.htaccess → rewrites the request into public/
│
▼
public/.htaccess (Laravel's own) → hands off to public/index.php
│
▼
Laravel routes /about and returns the page
Because nothing moved, git pull and composer update can never break your paths. That’s the whole appeal.
Method 1 — Deploy with SSH and Git
This takes about 10–15 minutes the first time and a couple of minutes for every update after.
Step 1 · Connect over SSH
Find your SSH details in your hosting control panel — on Hostinger it’s under Advanced → SSH Access, and you’re looking for the username, host, and port. Then connect:
ssh u123456789@123.45.67.89 -p 65002
On macOS or Linux the built-in Terminal handles this. On Windows, use Windows Terminal or PuTTY. The first time, you’ll be asked to confirm the host fingerprint — type yes.
If you can’t find SSH anywhere in your panel, your plan may not include it. Either enable/upgrade it, or jump to Method 2.
Step 2 · Go to your web root
cd public_html
Some hosts nest it under the domain name instead:
cd domains/yourdomain.com/public_html
Run pwd to confirm where you are before doing anything destructive.
Step 3 · Clone your project
Assuming your code is on GitHub, clone it into a temporary folder and move everything (including hidden dotfiles) into the current directory:
git clone https://github.com/yourusername/your-app.git tmp
mv tmp/* tmp/.* . 2>/dev/null
rm -rf tmp
For a private repo, set up a deploy key first — generate a key with ssh-keygen -t ed25519, then add the contents of the .pubfile to your repo’s Settings → Deploy keys on GitHub. Clone using the SSH URL (git@github.com:...) instead of HTTPS.
No Git? Zip the project locally, upload the zip through the control panel’s File Manager, and extract it here. Then continue from Step 4.
Step 4 · Install dependencies
composer install --optimize-autoloader --no-dev
The --no-dev flag skips development-only packages you don’t need in production, and --optimize-autoloader makes class loading faster. This takes a few minutes.
If you get composer: command not found, your host may expose it as composer.phar — try php composer.phar install --optimize-autoloader --no-dev. If it’s missing entirely, install it locally to the project:
curl -sS https://getcomposer.org/installer | php
php composer.phar install --optimize-autoloader --no-dev
Step 5 · Set up your environment file
Copy the example file and open it for editing:
cp .env.example .env
nano .env
The settings that genuinely matter for a production deploy:
APP_NAME="Your App"
APP_ENV=production
APP_DEBUG=false
APP_URL=https://yourdomain.com
DB_CONNECTION=mysql
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=
DB_USERNAME=
DB_PASSWORD=
APP_DEBUG=false is not optional — leaving it true in production exposes stack traces, environment variables, and database details to anyone who triggers an error. Leave the database fields blank for the moment; you’ll fill them in after the next step. Save and exit nano with Ctrl+X, then Y, then Enter.
Step 6 · Create the database
In your control panel, find the MySQL Databases section and do three things: create a database, create a user (let the panel generate a strong password and copy it somewhere safe), and then assign that user to the database with all privileges. That third step is the one people forget, and it produces a confusing “access denied” later.
Now reopen .env and fill in the three blank fields with the database name, username, and password you just created:
nano .env
Step 7 · Generate the app key
php artisan key:generate
This writes a unique APP_KEY into your .env. Laravel uses it to encrypt sessions and other sensitive data, so the app won’t run properly without it.
Step 8 · Run your migrations
php artisan migrate --force
The --force flag is required because Laravel won’t run migrations in a production environment without explicit confirmation — a deliberate guard against accidental data loss. If you have seeders you want to run, add php artisan db:seed --force.
Step 9 · Add the .htaccess redirect
This is the step that does the magic from earlier. Create an .htaccess file in your project root — the same level as app/, vendor/, and artisan, not inside public/:
nano .htaccess
You asked for something simple, secure, and effective, so here it is — a forward to public/ plus a hard block on the .env file, and nothing you don’t need:
<IfModule mod_rewrite.c>
RewriteEngine On
# Send every request into Laravel's public/ folder
RewriteCond %{REQUEST_URI} !^/public/
RewriteRule ^(.*)$ public/$1 [L,QSA]
</IfModule>
# Never serve the environment file, even if something else misconfigures
<Files .env>
Require all denied
</Files>
That’s the whole thing. Laravel’s own .htaccess already lives inside public/ and handles the actual routing once requests arrive there, so you don’t need to duplicate any of that here.
A note on security: because your app files sit inside the web root with this approach, the .env block above matters. The <Files .env> rule uses Apache 2.4+ syntax (Require all denied); if your host runs older Apache you’d use Order allow,deny / Deny from all instead, but 2.4+ is near-universal in 2026. After deploying, it’s worth confirming the protection works — visiting yourdomain.com/.env should return a 403, not your file.
Step 10 · Cache and optimize
php artisan config:cache
php artisan route:cache
php artisan view:cache
These compile your config, routes, and views into fast cached versions. The payoff is real — noticeably quicker responses — but there’s a catch worth remembering: if you change a config or route file later, you must re-run these, or the app will keep serving the old cached version and you’ll wonder why your changes aren’t showing up. If you deploy often, it’s worth automating these steps with Git hooks so the right commands fire on every pull and you never forget one.
Step 11 · Fix folder permissions
chmod -R 775 storage bootstrap/cache
Laravel needs to write logs, cached files, and session data into storage/ and bootstrap/cache/. Without write permission you’ll hit a 500 error the moment it tries. This is the single most common cause of a deploy that “doesn’t work” with no obvious reason.
Step 12 · Link storage (only if you serve uploaded files)
If your app lets users upload files that need to be publicly accessible:
php artisan storage:link
This creates a symlink from public/storage to storage/app/public. Skip it if you don’t have user uploads. (Heads up: on some shared hosts the symlink command fails due to how the filesystem is configured — if so, you can create the link manually or via a tiny script, and the host’s docs usually cover their specific case.)
Step 13 · Visit your site
Open https://yourdomain.com. If you see your app, you’re done — genuinely. If you see an error instead, the troubleshooting section below covers the handful of things that actually go wrong, almost always permissions or the .env.
Method 2 — Deploy with FileZilla (no SSH)
If your plan has no SSH, you can still deploy; you’ll just do locally what you’d otherwise do on the server, then move files across by FTP.
Prepare everything on your own machine first. Because you can’t run Composer or build assets on the server, do it locally:
composer install --optimize-autoloader --no-dev
npm install && npm run build
cp .env.example .env
php artisan key:generate
Set APP_ENV=production and APP_DEBUG=false in that .env before uploading.
Upload with FileZilla. Grab the free client from filezilla-project.org, then get your FTP credentials from the control panel (host, username, password, and port — 21 for FTP or 22 for SFTP). Connect, navigate the right-hand (server) panel to public_html/, and upload your entire project. The vendor/ folder makes this slow — expect 10–30 minutes — because it contains thousands of small files.
Create the .htaccess in public_html/ through the File Manager, using exactly the same content as Step 9 above.
Set up the database the same way as Method 1’s Step 6, then edit .env through the File Manager to add the credentials.
Run migrations without a terminal. This is the one genuinely awkward part of going SSH-less. The standard approach is a temporary route that runs migrations once, then is deleted immediately. Add this to the very bottom of routes/web.php:
Route::get('/deploy-setup-7x2', function () {
Artisan::call('migrate', ['--force' => true]);
return 'Done: ' . Artisan::output();
});
Visit https://yourdomain.com/deploy-setup-7x2 once, confirm it succeeded, then delete that route from routes/web.phpimmediately and re-upload the file. Leaving a route like this live is a real security hole — treat it as a match you light and blow out at once. (The odd suffix is a small deterrent against anyone guessing the URL in the seconds it exists.)
Fix permissions through the File Manager: set storage/ and bootstrap/cache/ to 775, applying recursively.
Then visit your site as in Method 1.
When something goes wrong
Most failed deploys come down to a small number of causes. Here are the ones you’ll actually meet, roughly in order of likelihood.
A 500 error with a blank page is almost always one of two things: file permissions or the .env. Re-run chmod -R 775 storage bootstrap/cache, and confirm .env exists with APP_DEBUG=false and a populated APP_KEY. If you’re still stuck, your real error is waiting in storage/logs/laravel.log or your host’s error log — read it before guessing.
A redirect loop (“the page isn’t redirecting properly”) usually means the .htaccess rewrite is fighting itself. Try the variant with a leading slash on the target — RewriteRule ^(.*)$ /public/$1 [L] — which some server configurations need.
CSS and JavaScript don’t load, or you get mixed-content warnings. Your pages are HTTPS but assets are being requested over HTTP. Set both APP_URL and ASSET_URL to your https:// address in .env, and force the scheme in app/Providers/AppServiceProvider.php:
public function boot(): void
{
if (app()->environment('production')) {
\Illuminate\Support\Facades\URL::forceScheme('https');
}
}
Then re-run php artisan config:cache.
“419 Page Expired” when submitting a form is a CSRF/session cookie mismatch. In config/session.php, set 'same_site' => 'lax', make sure APP_URL matches the domain visitors actually use, and clear the config cache.
“Vite manifest not found” means your frontend assets weren’t built. Laravel 13 uses Vite by default. Run npm install && npm run build locally and upload the resulting public/build/ folder. If you don’t use a build step at all, replace the @vite(...)directive in your Blade layout with plain <link> and <script> tags pointing at your assets. (This is a common enough headache that it has its own walkthrough — see the dedicated Vite manifest not found guide if the quick fix above doesn’t cover your case.)
“Could not resolve… requires php ^8.3” during composer install means your active PHP version is too old for Laravel 13. Check it with php -v, then raise it to 8.4 (the safe default) in your control panel’s PHP configuration and re-run the install. If you’d already set it to 8.5 and that’s where a package complains, stepping down to 8.4 usually clears it.
Database “connection refused” is usually the wrong DB_HOST. Try localhost first (correct on most shared hosts), then 127.0.0.1. If neither works, your host’s docs or support will tell you the exact hostname — it’s occasionally something non-obvious.
If the .htaccess method isn’t an option
For the large majority of shared hosts the method above is the cleanest route — skip this section. The exceptions, and what to do in each:
- Your host lets you set the web root. The ideal case — better than
.htaccessrewriting. Point the domain atpublic_html/public(look for a “document root” setting in your panel) and you’re done: no rewrite overhead, server aimed at the right place from the start. - Your host ignores
.htaccessrewrites (rare on Apache/LiteSpeed). Fall back to the traditional method this guide avoids: movepublic/‘s contents intopublic_html/, keep the rest of the app one level up outside the web root, and edit the tworequirepaths inindex.php. More fragile, needs re-checking after major updates, but well-documented and reliable. - You’re on Nginx, not Apache.
.htaccessdoesn’t apply — add alocationblock pointing atpublic/instead. Uncommon on shared hosting, so you’ll usually only hit this on a VPS.
Short version: try for a settable web root first, use the .htaccess method as your default, keep file-moving in your back pocket for hosts that force your hand.
Keeping it fast
Once the site is up, a few things keep it quick without any real effort. The Artisan caching commands from Step 10 are the biggest single win — just remember to re-run them after config or route changes. Beyond that, enabling OPcache in your PHP settings lets the server skip recompiling your PHP on every request, which is a meaningful speed-up for free. And if your app leans on the database, adding indexes to the columns you filter and sort by most often (foreign keys, timestamps, anything in a where clause) does more for real-world speed than almost anything else.
For a global audience, putting a free Cloudflare tier in front of your domain caches static assets close to your visitors. None of this is mandatory; do it when you notice you need it, not before.
Before you call it done
A short pre-launch check that catches the things most likely to bite you:
APP_DEBUG=falseandAPP_ENV=productionin.env- Visiting
yourdomain.com/.envreturns a 403, not the file - HTTPS works and your SSL certificate is active
- Any temporary deploy routes (like the migration route in Method 2) are deleted
- A strong, generated database password — not one you reused
storage/is writable but not publicly browsable
Run through it once; it takes two minutes and saves the kind of incident you’d rather not have. And once the basics are covered, hardening against XSS is the natural next layer — the Laravel Content Security Policy guide walks through adding CSP headers with nonces and violation reporting without breaking your front end.
Knowing when to move on
Shared hosting is a fine place to start, and for many sites it’s also a fine place to stay. But it’s worth recognizing the signals that you’ve outgrown it, so you upgrade by choice rather than during an outage: persistent 500s under traffic spikes, your host emailing you about CPU usage, page loads dragging past a few seconds even with caching on, or a genuine need for queue workers, WebSockets, or background jobs that run continuously. Traffic consistently north of ~5,000 daily visitors is another reasonable trigger.
When that day comes, the usual next steps are a VPS like DigitalOcean or Vultr (cheap, but you maintain the server), or a managed platform like Cloudways or Laravel Cloud (more expensive, but they handle the maintenance). There’s no rush, though — plenty of successful small apps never leave shared hosting at all, and if you started on a plan like Hostinger’syou can usually upgrade within the same provider when you outgrow it, which keeps the migration painless.
Wrapping up
Deploying Laravel doesn’t have to involve dismembering your public/ folder or dreading your next update. Upload the project as-is, add one small .htaccess file, protect your .env, and let the server do the routing. The structure you developed against is the structure you deploy — which means the next deploy, and the one after that, stay boring. Boring, when it comes to production, is exactly what you want.
If you hit a snag the troubleshooting section didn’t cover, leave a comment describing what you see and what’s in your laravel.log — that’s almost always where the answer is hiding.
Can you run Laravel on shared hosting?
Keep going from here — a few related guides on the blog that pair well with this one:
- Simplify your Laravel workflow with Git hooks — automate the migrate-and-recache dance on every deploy
- Fix “Vite manifest not found” in Laravel — the deeper fix for the asset-build error above
- Laravel Content Security Policy: complete implementation guide — lock down your live app against XSS
- Send SMS, OTP & DLT messages in Laravel with Fast2SMS — add SMS to the app you just deployed