How to Deploy Laravel 13 on Shared Hosting (Without Moving Files)
There’s a particular kind of frustration that comes from finishing a Laravel app, feeling proud of it, and then realizing you still have to deploy it. Most tutorials make this harder than it needs to be: they tell you to cut your public/ folder apart, scatter its contents into public_html/, and rewrite the paths inside index.php. It works, sort of, until your next git pull overwrites something and the whole site 500s.
This guide takes a cleaner path. You’ll upload your Laravel project exactly as it sits on your machine, add a single .htaccessfile, and let the server quietly route everything to the right place. Nothing gets moved. Nothing gets renamed. Your .envstays protected. And when you update later, git pull just works.
By the end you’ll have a live Laravel 13 site and — more importantly — an understanding of why each step matters, 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 an era of PHP 5.6 and no SSH access, and that reputation has mostly outlived the reality. A modern shared plan with PHP 8.3, SSH, and Composer is perfectly capable of running a real Laravel app.
It’s a good fit for portfolio sites, client brochure sites, small SaaS MVPs, internal admin panels, and anything with traffic in the hundreds-to-low-thousands of daily visitors. The economics are hard to argue with: roughly $3–8/month versus $12–50/month for a VPS you’d also have to maintain yourself.
Where shared hosting genuinely falls short is anything needing a process that runs continuously or full server control: queue workers humming 24/7, WebSockets via Laravel Reverb, heavy image or video processing, or sustained traffic above ~5,000 daily visitors. If that’s you, a VPS or managed platform is the right call, and there’s a section near the end on knowing when you’ve crossed that line. For everyone else, read on.
What your host actually needs to support
Laravel 13 raised the floor a little, so it’s worth checking your host against this list before you start rather than discovering a missing piece halfway through.
- PHP 8.3 or newer. This is the big one. Laravel 13 requires PHP 8.3 as an absolute minimum, and 8.4 is the sweet spot for a new project in 2026 — recent enough to be well-supported, settled enough that every package you’ll reach for already works on it. PHP 8.5 is worth choosing if your host offers it and you want the newest, with one caveat: brand-new PHP releases occasionally precede some packages catching up, so if you hit an odd dependency error, dropping to 8.4 is the quick fix. Either way, if your host tops out at PHP 8.2, Laravel 13 simply won’t install — no workaround.
- SSH access. Not strictly mandatory, but it’s the difference between a 10-minute deploy and an hour of fiddling with FTP. SSH lets you run Composer and Artisan commands directly on the server.
- Composer. Modern Laravel has dozens of dependencies; you need Composer to install them. Most developer-friendly hosts pre-install it.
- Git. Optional but transformative. Cloning your repo and later pulling updates beats re-uploading files every time.
- MySQL 8.0+ or PostgreSQL 13+, plus the standard PHP extensions Laravel expects (OpenSSL, PDO, Mbstring, Tokenizer, XML, Ctype, BCMath). These come standard almost everywhere worth using.
If your current host already ticks these boxes, skip ahead to the deployment steps. If you don’t have hosting yet, the next section covers what to look for — 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 understand the few things that genuinely matter for Laravel, because once you know them you can evaluate any host yourself.
The PHP version is the dealbreaker. This is where most cheap plans quietly disqualify themselves. A surprising number of budget hosts still cap shared plans at PHP 8.1 or 8.2 — fine for older apps, but a hard stop for Laravel 13. Always confirm 8.3+ is selectable in the control panel before you pay.
SSH should be included, not an upsell. Some hosts technically offer SSH but 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. When they’re not, you can usually install Composer manually, but it’s twenty minutes of yak-shaving you didn’t need.
A free domain for the first year is a genuine saving, not a gimmick. A .com runs $10–15/year, so when it’s bundled with annual hosting it meaningfully offsets the cost — just remember it renews at full price in year two, so note that date.
Why this guide uses Hostinger as the example
Throughout the steps below, the examples use Hostinger‘s Premium shared plan, partly for consistency and partly because it happens to tick the boxes above at the low end of the price range. Concretely, here’s what’s useful about it for a Laravel deployment, and why each thing matters to you rather than why it’s a nice feature in the abstract:
- It’s among the cheapest plans that still includes SSH. Plenty of hosts at this price make you upgrade for shell access; here it’s on the entry plan, which is what makes the fast SSH-based deploy in this guide possible without paying more.
- PHP versions up to 8.5 are selectable in hPanel. That comfortably covers Laravel 13’s 8.3 minimum and leaves you room to move — you pick the version in a couple of clicks rather than opening a support ticket and waiting. (For a new Laravel 13 project, 8.4 is the safe default; more on choosing a version just below.)
- Composer and Git are both available, which is what lets you clone your repo and install dependencies in minutes instead of setting up tooling first or re-uploading files on every change.
- A free domain is included for the first year on annual plans, which offsets a real chunk of the first-year cost — just note it renews at the normal price afterward, so put that date on your calendar.
- It runs LiteSpeed and includes free SSL and weekly backups — the practical effect being decent performance and HTTPS without extra setup or extra fees.
Full disclosure: the Hostinger links here are affiliate links, so if you sign up through one this site earns a commission at no extra cost to you. That’s worth being upfront about — but it’s also why it matters that everything above is checkable: confirm the current PHP version and SSH availability on their plan page before buying, and if you already have a host that meets the checklist, this entire guide works identically on any cPanel or hPanel setup. There’s a 30-day money-back guarantee, so the risk of trying is low either way.
If you’d rather weigh alternatives, SiteGround and A2 Hosting are both solid and developer-friendly; they tend to cost a bit more, and as always, double-check that PHP 8.3+ is available on the specific plan you’re considering.
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 is built to serve 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, your app/ code, your vendor/ libraries) is supposed to sit safely outside the web root.
Shared hosting complicates this because 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 of the app. The cleaner fix — the one this guide uses — is to leave everything where Laravel put it and add a small .htaccess rule that transparently 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 approach above is the cleanest route and you can ignore this section. But a few situations call for something different, so it’s worth knowing your options exist and when each one applies.
If your host lets you set the web root, that’s actually the ideal setup — better than .htaccess rewriting. Some panels (and most VPS configurations) let you point the domain directly at public_html/public or change the document root to your project’s public/ folder. If you have that toggle, use it: there’s no rewrite overhead and nothing forwarding requests, because the server is aimed at exactly the right place from the start. Look for a “document root” or “website root directory” setting in your panel before assuming you don’t have it.
If your host blocks or ignores .htaccess rewrites (rare on Apache/LiteSpeed shared hosting, but it happens), the fallback is the traditional method this guide set out to avoid: move the contents of public/ up into public_html/, keep the rest of your Laravel app in a folder one level up and outside the web root, and edit the two require paths near the top of index.php to point at the relocated bootstrap/app.php and autoloader. It’s more fragile and needs re-checking after major updates, but it’s well-documented and works when nothing else will.
If you’re on Nginx rather than Apache, .htaccess doesn’t apply at all — Nginx doesn’t read it. You’d instead add a small location block to the server config pointing at public/. Pure Nginx is uncommon on shared hosting (most use Apache or LiteSpeed, which both honor .htaccess), so you’ll usually only meet this on a VPS — at which point the “knowing when to move on” section below is probably more relevant to you anyway.
The short version: try for a settable web root first, use the .htaccess method in this guide as your reliable default, and keep the file-moving method in your back pocket only 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.
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