The laravel 403 forbidden error right after uploading to shared hosting isn’t a permissions problem — it’s that Apache is serving your project root instead of the public/ folder. Laravel’s actual document root is public/, and on shared hosts the web server looks at public_html/ or www/, finds no index.html, and refuses to list the directory. The fix is either a root-level .htaccess rewrite or, if your host allows it, changing the document root to point at public/ directly.
Last verified: 2026-04-23 on Laravel 11 with PHP 8.3 and Apache 2.4 on cPanel. Originally published 2022-07-06, rewritten and updated 2026-04-23.
TL;DR
Create .htaccess in the Laravel project root with the rewrite below. It forwards every request into public/, where Laravel’s own .htaccess takes over.
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_URI} !^public
RewriteRule ^(.*)$ public/$1 [L]
</IfModule>
Why the error happens
Laravel ships with two relevant entry points:
artisan serve— for local dev, with PHP’s built-in server pointed atpublic/.public/index.php— the real entry point Apache/Nginx should serve in production.
When you SFTP the whole project into public_html/, Apache treats the project root as the document root. It looks for an index.html or index.php there, finds only the artisan script and a bunch of directories, and — because shared hosts disable Options +Indexes — returns:
Forbidden
You don't have permission to access / on this server.
The permissions message is misleading — the files are readable, the server just doesn’t know which file to send.
Fix 1 — Root .htaccess rewrite (quick, works on any shared host)
Drop this file at the project root (the same folder that contains artisan, composer.json, and the public/ directory):
# .htaccess (Laravel project root)
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_URI} !^public
RewriteRule ^(.*)$ public/$1 [L]
</IfModule>
What each line does:
<IfModule mod_rewrite.c>— only run the rules if Apache’smod_rewriteis loaded. Almost always is on shared hosts, but the guard prevents a 500 if it isn’t.RewriteEngine On— turn the rewriting engine on for this directory.RewriteCond %{REQUEST_URI} !^public— only rewrite when the URL doesn’t already start withpublic. Prevents an infinite loop.RewriteRule ^(.*)$ public/$1 [L]— capture whatever came in (^(.*)$) and rewrite it aspublic/<capture>.[L]stops further rewrites in this.htaccess.
Laravel’s stock public/.htaccess then takes the rewritten path and dispatches it to index.php. The router handles the URL from there.

Fix 2 — Change the document root (cleaner, if your host allows)
A better long-term layout is to keep the Laravel app outside public_html/ entirely and point the hosting document root at the public/ subfolder inside it:
/home/user/
├── app/ (Laravel project — not web-accessible)
│ ├── app/
│ ├── bootstrap/
│ ├── config/
│ ├── public/ ← document root points here
│ └── ...
└── public_html/ (empty or symlinked to ../app/public)
Most hosting panels expose this setting:
- cPanel → Domains → Document Root →
/home/user/app/public. - Plesk → Hosting Settings → Document Root.
- aaPanel → Site → Site Directory (set to the
publicsubfolder).
This keeps .env, storage/, and vendor/ outside the web-reachable tree, which is the layout Laravel is designed for.
After the fix — clear caches and set permissions
If the 403 is gone but you now see 500 errors, two usual causes:
# 1. storage/ and bootstrap/cache/ must be writable by the web user
chmod -R 775 storage bootstrap/cache
# 2. Rebuild the config/route caches after moving files
php artisan config:clear
php artisan route:clear
php artisan view:clear
Run those from SSH on the host. If SSH isn’t available, chmod via the file manager’s permissions dialog and delete bootstrap/cache/config.php manually.
Frequently asked questions
Because Laravel’s actual document root is the public/ folder, but most shared-hosting accounts serve from /home/user/public_html/ or www/. Apache looks in your project root for an index.html or index.php, doesn’t find one (you didn’t upload those, you uploaded a Laravel app), and returns 403 because directory listings are disabled. The fix is an .htaccess file that rewrites all requests into public/ — or, better, pointing the host’s document root directly at the public/ folder.
.htaccess rewrite or document-root change? Changing the document root to /home/user/project/public in the hosting control panel (cPanel → Domains → Document Root, Plesk → Hosting Settings, etc.). That’s what Laravel expects and what the framework’s docs recommend. The .htaccess rewrite is a workaround for hosts that won’t let you change the document root — it works, but every request pays a rewrite cost and you have two .htaccess files (root and public/) to keep in sync.
public_html? Not ideally. When the app lives inside public_html, Apache can serve any file in the project tree — .env, storage/logs/, vendor/ — as long as someone guesses the URL. The .htaccess rewrite does block direct hits to the project root, but it’s safer to keep the Laravel app outside the web root and point the document root at only the public/ subfolder. On hosts where that’s impossible, at minimum make sure .env is protected (most default Apache configs already block hidden files).
RewriteCond %{REQUEST_URI} !^public need that exact form? The condition says “only apply this rewrite when the request URI does NOT start with public.” Without it, the rewrite would re-fire on the already-rewritten request (/foo → public/foo → public/public/foo → …) and either loop or 500 out. The leading ! is the negation; ^public is a regex for “starts with public”.
public/.htaccess that’s already there? No — the two files work in sequence. Your new root-level .htaccess rewrites the request URL into public/. Laravel’s stock public/.htaccess then handles the further rewrite into index.php?... so the router can match it. Keep both files; they do different jobs.
Related guides
- How to Exclude .well-known from Redirection for Let’s Encrypt with .htaccess in Laravel — the companion rule that lets ACME challenges reach
/.well-known/. - How to Install Laravel on Ubuntu — a fresh Laravel 11 install with the correct folder layout.
- How to Run a Laravel Project from GitHub — the clone + deploy flow that lands you on shared hosting.
- How to Fix cURL Error 60 SSL Certificate Problem in Laravel — another post-deploy-to-shared-hosting problem worth knowing.
References
Apache mod_rewrite docs: httpd.apache.org/docs/current/mod/mod_rewrite. Laravel deployment: laravel.com/docs/deployment.