🧩
Hosting

Deploying Laravel on Shared Hosting: Solving the public Folder Problem

05.11.2026
← All articles

Almost everyone who uploads a Laravel project to ordinary shared hosting for the first time sees the same thing: all files are copied into the site's root folder, and the browser shows a file listing, a 403 error or a blank page instead of the app. The reason is that Laravel expects only one part of the project, the public folder, to be reachable from the web. Everything else, including configuration, libraries and secret keys, should live where the web server cannot serve it.

Why only the public folder should be exposed

The project root contains app, config, routes, vendor, storage and the .env file. That file holds the database password, the APP_KEY encryption key, SMTP credentials and payment secrets. Every request is meant to enter through public/index.php. If the whole project sits in the web root, the server may hand out .env as plain text, which means a full leak of your database and keys.

Imagine a small Tashkent bookshop whose site misol.uz was built in Laravel. The developer unzipped the project into www/misol.uz with the ISPmanager file manager, the site did not load, and after a hasty "fix" the address misol.uz/.env displayed every password. The methods below prevent exactly that.

Method 1: point the document root at public

The cleanest option is to change the domain's document root. In ISPmanager, the WWW-domain settings include a root directory field (its label varies slightly between panel versions). It usually reads like /var/www/login/data/www/misol.uz; append /public and save. Laravel then runs in the structure it expects, and the storage:link symlink, Vite build assets and public/.htaccess rules all work unchanged. If the field is not editable for you, open a ticket from the sayt.uz cabinet and ask support to change it.

Method 2: rewrite to public with a root .htaccess

When the root cannot change, put a separate .htaccess in the project root that internally passes every request to public. The URL never shows /public, because this is an internal Apache rewrite, not a redirect.

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteRule ^(.*)$ public/$1 [L]
</IfModule>

# Extra safety: never serve sensitive files directly
<FilesMatch "^(\.env.*|composer\.(json|lock)|artisan)$">
    Require all denied
</FilesMatch>

This works, but it is more fragile: if the file is deleted later, the whole project is exposed again, which is why the FilesMatch block is there as insurance. Always open https://misol.uz/.env afterwards and confirm you get 403 or 404 rather than the file contents.

Method 3: move the core outside the web directory

Place the Laravel core in a separate home folder such as laravel-app, copy the contents of public into www/misol.uz, and adjust the paths in index.php. For Laravel 11 it looks roughly like this:

<?php
use Illuminate\Http\Request;

define('LARAVEL_START', microtime(true));

if (file_exists($maintenance = __DIR__.'/../../laravel-app/storage/framework/maintenance.php')) {
    require $maintenance;
}

require __DIR__.'/../../laravel-app/vendor/autoload.php';

$app = require_once __DIR__.'/../../laravel-app/bootstrap/app.php';
$app->usePublicPath(__DIR__);
$app->handleRequest(Request::capture());

The usePublicPath line keeps public_path() correct so uploads and the Vite manifest are found. Older versions have a slightly different index.php, so start from your version's original and change only the paths. The storage symlink must also be recreated to point from www/misol.uz/storage to laravel-app/storage/app/public.

Vendor, PHP version and .env

Shared plans often lack SSH and Composer, so build dependencies locally for production and upload them:

composer install --no-dev --optimize-autoloader
npm run build

The PHP version selected for the domain in ISPmanager must satisfy composer.json; Laravel 11 needs at least PHP 8.2. In .env set APP_ENV=production, APP_DEBUG=false, the correct APP_URL and database credentials with DB_HOST=localhost. Leaving debug mode on exposes file paths and request data on every error page, which is a ready-made map for an attacker.

Permissions, caches and the final check

storage and bootstrap/cache must be writable, or logs, sessions and compiled views will not be saved. Since PHP on shared hosting runs as your own user, 755 for folders and 644 for files is normally enough; 777 is unnecessary and risky. With SSH available, finish with:

php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache

Remember that after config:cache, env() returns null when called outside config files. Finally confirm three things: the home page loads, /.env is blocked, and images and CSS load correctly. With all three in place, the project runs safely on ordinary hosting.

Related articles

🛠️ 500 Internal Server Error: where to look for the cause 🗜️ Gzip and Brotli compression: shrink your site and speed up loading 🛡️ Personal Data Localization in Uzbekistan and Local Hosting 🤖 AI crawlers are loading your server: how to spot and limit them
🌐 Language
🇺🇿 O'zbek 🇺🇿 Ўзбек 🇷🇺 Русский 🇬🇧 English ✓