This gist provides a step by step guide on how to host a Laravel project on Hostinger shared hosting.
When I was looking to deploy a Laravel project on Hostinger shared hosting, I found that there was no proper guide available on the internet. So I decided to write a step-by-step guide on how to host a Laravel project on Hostinger shared hosting. I hope this will help someone who is looking to host a Laravel project on Hostinger shared hosting.
As Hostinger's shared hosting does not provide the same functionalities as a VPS or Dedicated Server, so there are some limitations. However, I have tried to overcome those limitations and make the Laravel project work on Hostinger shared hosting.
Following is working:
- Works for all Laravel versions 10 through 13 on Hostinger's Shared Hosting
- Does not have to move any files out of Laravel's
publicfolder — it stays as the actual public folder, just renamed (see step 10) - Working Storage link functionality
- A no-SSH fallback for every Artisan command, for shared hosting plans that don't include SSH access
-
Create a new website on Hostinger's hPanel: Add your domain to create a new website.
-
Create a new MySQL database on Hostinger's hPanel: Also create a new user, assign the user to the database with full privileges, and note down the database name, username, and password.
-
Navigate to the root directory of your website: Use the File Manager on Hostinger's hPanel to go to the root directory of your website.
-
Delete all files and folders in the root directory: This includes
public_htmlandDO_NOT_UPLOAD_HERE. You're replacing Hostinger's default scaffold with the Laravel project root. -
Get
vendor/onto the server — locally before upload, or on the server over SSH:Option A — install locally, upload
vendor/with the rest:composer install --no-dev --optimize-autoloader
Option B — skip
vendor/in the upload, install on the server instead (avoids uploading a largevendor/folder over SFTP; needs SSH — see step 7):composer2 install --no-dev --optimize-autoloader
Run this from the website root over SSH, after uploading (step 6). Hostinger's SSH environment defaults the bare
composercommand to Composer 1 — typecomposer2explicitly or--no-devcan behave differently, or the install can fail outright depending on yourcomposer.json's PHP version requirement.Either way, skip
npm install/npm run buildunless your project actually ships a compiled frontend build — plenty of Laravel projects only havepackage.jsonbecause Composer/Laravel tooling references Vite/Mix config from the skeleton, without any real JS bundle to build. Check before assuming you need Node on the server.See the official Laravel documentation for the general production checklist.
-
Zip and upload your Laravel project: Upload the entire project (not just
public/) to the website root directory — via Git deploy (if your plan supports it), SFTP, or the hPanel File Manager zip-upload-then-extract flow. The root should end up looking like a normal Laravel project root:app/,bootstrap/,config/,public/,routes/,artisan,composer.json, etc.If zipping locally, run the zip command from inside the project root itself (not its parent directory) —
zip -r ... .zips the contents of the current directory, so running it elsewhere nests everything inside an extra folder once extracted on the server:zip -r deploy.zip . -x ".git/*" "node_modules/*" ".env" \ "storage/framework/cache/*" "storage/framework/sessions/*" \ "storage/framework/views/*" "storage/logs/*" \ "*.DS_Store" "*/.DS_Store" "**/._*" ".AppleDouble/*" \ "*Thumbs.db" "*ehthumbs.db" "*Desktop.ini" '$RECYCLE.BIN/*' \ "*.directory" ".Trash-*/*" \ ".vscode/*" ".idea/*" ".fleet/*" ".nova/*" ".zed/*" ".claude/*" \ "*.swp" "*.swo" "*~" \ "phpunit.xml" "tests/*" ".editorconfig" ".gitattributes" ".env.example" \ "deploy.zip"
Add
"vendor/*"to the exclusions too if you're going with Option B above. If your project has a JS build step you don't ship (no compiled output used by the app — see step 5), also excludevite.config.js/webpack.mix.js/package.json/package-lock.json.zipdoesn't read.gitignore, so even if these are already git-ignored in your project they still need to be listed explicitly here — otherwise they get swept into the zip anyway. The list above covers macOS (.DS_Store, AppleDouble._*files), Windows (Thumbs.db,Desktop.ini,$RECYCLE.BIN/), Linux (.directory,.Trash-*/), common editors/tools (VS Code, JetBrains/IDEA, Fleet, Nova, Zed, Claude Code's.claude/, plus generic*.swp/*.swo/*~swap files), and repo-only files with no runtime purpose (test suite + its config,.editorconfig,.gitattributes,.env.example) — all useful on GitHub, dead weight on shared hosting where inode count matters as much as disk space. Trim or extend to match whatever you actually use.README.mdis generally fine to leave in — harmless to have visible in the webroot if you don't mind it, and some people like keeping it there.Exclude
.env,node_modules/, and.git/from the upload if you're not deploying via Git directly.Recreate the storage skeleton after extracting. If you excluded
storage/framework/{cache,sessions,testing,views}/andstorage/logs/from the zip (they're normally empty locally, nothing worth uploading), Laravel'sartisancache commands still expect those directories to exist — not just be empty — and fail (e.g.View path not found.onview:clear) if they're missing entirely. Recreate them right after extracting, before running anyartisancommand:mkdir -p storage/framework/cache/data storage/framework/sessions \ storage/framework/testing storage/framework/views storage/logs
-
Get SSH access to your website: SSH access is available on most Hostinger shared plans via hPanel → Advanced → SSH Access. Use PuTTY, Windows Terminal, macOS Terminal, or any other SSH client to connect. If your plan doesn't include SSH, skip to the No SSH access? section below and run the Artisan commands in the remaining steps as Cron Jobs instead.
-
Setup .env file: In a Laravel project the
.envfile holds all the configuration variables. Either create a new.envfile, rename.env.exampleto.env, or upload your local.envif you already have one. Then open it and set:APP_URL: the URL of your website.APP_ENV:production.APP_DEBUG:false— never leave debug mode reachable on a live domain.DB_HOST: usuallylocalhostfor Hostinger shared MySQL.DB_PORT: usually3306.DB_DATABASE/DB_USERNAME/DB_PASSWORD: from step 2.MAIL_*: production mail credentials, if your project sends any email (password resets, OTP logins, notifications, etc.) — verify these actually deliver before going live, not after.
Generate an app key if
.envdoesn't already have one:php artisan key:generate --ansi
-
Migrating the Database: From the root directory of your website, run:
php artisan migrate
This creates all the required tables, assuming migrations exist and
.envis configured correctly.If your project has seeders with real starting content, this is the point to run them — but check what each seeder actually does first (
php artisan db:seed --class=SomeSeederto run one at a time) rather than a barephp artisan db:seed, which runs everythingDatabaseSeederchains. It's common for aDatabaseSeederto chain demo/factory-generated seeders alongside real ones (test users, dummy records) that you don't want polluting a live database.If any seeded rows reference uploaded files (images, documents) under
storage/app/public/..., those files are gitignored by default and won't be in your upload — zip and upload that folder separately, and extract it into the same path on the server before runningstorage:link(step 14), or the seeded rows will point at missing files. -
Rename the Public folder: Hostinger's webserver serves from
public_htmlat the account root, not from an arbitrarypublic/subfolder. Rename it:mv public public_html
-
Open the filesystems.php file: Located in the
configfolder of your Laravel project. -
Modify the
linkssection: Laravel's default assumespublic/is still calledpublic. Since it's nowpublic_html, change:'links' => [ public_path('storage') => storage_path('app/public'), ],
to
'links' => [ base_path('public_html/storage') => storage_path('app/public'), ],
This step is required for any uploaded files under
storage/app/public/...to be reachable at all once the folder is renamed — not just a nice-to-have. -
Open an SSH terminal and go to the root directory of your website.
-
Create the storage symlink and clear caches:
php artisan storage:link php artisan optimize:clear
-
Cache the routes, views, and config for production:
php artisan optimize
Re-run this (or at least
optimize:clearthenoptimize) after every deploy that changes.env, routes, or config — stale cached config is the #1 cause of "it works locally but not on the server" on this kind of setup. -
Have a running Laravel project on Hostinger shared hosting: Now you have a running Laravel project on Hostinger shared hosting.
If your Hostinger plan doesn't include SSH, use Cron Jobs (hPanel → Advanced → Cron Jobs) to run one-off Artisan commands instead of the CLI, e.g.:
domains/<your-domain>/artisan migrate
domains/<your-domain>/artisan storage:link
domains/<your-domain>/artisan optimize
Set the schedule to run once (e.g. a far-future minute you then delete), or use hPanel's "Run now" if available, then remove/disable the cron entry afterward so it doesn't repeat.
Shared hosting has no Redis and no persistent worker process. If your project uses queues or the scheduler, set QUEUE_CONNECTION=database and drive both from a single Hostinger Cron Job hitting Artisan directly, rather than running queue:work as a daemon (shared hosting won't keep a long-running process alive):
* * * * * /usr/bin/php /home/<user>/domains/<your-domain>/artisan schedule:run >> /dev/null 2>&1
Then use $schedule->command('queue:work --stop-when-empty') (or similar) inside routes/console.php / your scheduler if you need queued jobs processed regularly, instead of a long-running queue:work.
-
APP_ENV=production,APP_DEBUG=false -
.envDB credentials correct,php artisan migrateran cleanly -
public/renamed topublic_html,config/filesystems.phpupdated (step 12),storage:linkcreated -
php artisan optimizerun after final config - Any transactional email (password reset, OTP/2FA, contact forms, notifications) actually delivers — test each one, not just one
- HTTPS is enforced (Hostinger free SSL, force-HTTPS in hPanel or via
public_html/.htaccess) - Auth flows that gate sensitive areas (login, 2FA/OTP, password reset) work end-to-end on the live domain, not just locally
thank you.. this was helpful