Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save SubhanRaj/62631fdea39d73cc4e034debccabf296 to your computer and use it in GitHub Desktop.

Select an option

Save SubhanRaj/62631fdea39d73cc4e034debccabf296 to your computer and use it in GitHub Desktop.
This gist provides a step by step guide on how to host a Laravel project on Hostinger shared hosting.

This gist provides a step by step guide on how to host a Laravel project on Hostinger shared hosting.

How to host a Laravel project on Hostinger shared hosting

Introduction

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.

What is working

Following is working:

  1. Works for all Laravel versions 10 through 13 on Hostinger's Shared Hosting
  2. Does not have to move any files out of Laravel's public folder — it stays as the actual public folder, just renamed (see step 10)
  3. Working Storage link functionality
  4. A no-SSH fallback for every Artisan command, for shared hosting plans that don't include SSH access

Steps to host a Laravel project on Hostinger shared hosting

  1. Create a new website on Hostinger's hPanel: Add your domain to create a new website.

  2. 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.

  3. 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.

  4. Delete all files and folders in the root directory: This includes public_html and DO_NOT_UPLOAD_HERE. You're replacing Hostinger's default scaffold with the Laravel project root.

  5. 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 large vendor/ 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 composer command to Composer 1 — type composer2 explicitly or --no-dev can behave differently, or the install can fail outright depending on your composer.json's PHP version requirement.

    Either way, skip npm install / npm run build unless your project actually ships a compiled frontend build — plenty of Laravel projects only have package.json because 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.

  6. 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 exclude vite.config.js / webpack.mix.js / package.json / package-lock.json.

    zip doesn'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.md is 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}/ and storage/logs/ from the zip (they're normally empty locally, nothing worth uploading), Laravel's artisan cache commands still expect those directories to exist — not just be empty — and fail (e.g. View path not found. on view:clear) if they're missing entirely. Recreate them right after extracting, before running any artisan command:

    mkdir -p storage/framework/cache/data storage/framework/sessions \
      storage/framework/testing storage/framework/views storage/logs
  7. 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.

  8. Setup .env file: In a Laravel project the .env file holds all the configuration variables. Either create a new .env file, rename .env.example to .env, or upload your local .env if 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: usually localhost for Hostinger shared MySQL.
    • DB_PORT: usually 3306.
    • 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 .env doesn't already have one:

    php artisan key:generate --ansi
  9. Migrating the Database: From the root directory of your website, run:

    php artisan migrate

    This creates all the required tables, assuming migrations exist and .env is 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=SomeSeeder to run one at a time) rather than a bare php artisan db:seed, which runs everything DatabaseSeeder chains. It's common for a DatabaseSeeder to 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 running storage:link (step 14), or the seeded rows will point at missing files.

  10. Rename the Public folder: Hostinger's webserver serves from public_html at the account root, not from an arbitrary public/ subfolder. Rename it:

    mv public public_html
  11. Open the filesystems.php file: Located in the config folder of your Laravel project.

  12. Modify the links section: Laravel's default assumes public/ is still called public. Since it's now public_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.

  13. Open an SSH terminal and go to the root directory of your website.

  14. Create the storage symlink and clear caches:

    php artisan storage:link
    php artisan optimize:clear
  15. Cache the routes, views, and config for production:

    php artisan optimize

    Re-run this (or at least optimize:clear then optimize) 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.

  16. Have a running Laravel project on Hostinger shared hosting: Now you have a running Laravel project on Hostinger shared hosting.

No SSH access?

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.

Ongoing: queue and scheduler

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.

Post-deploy checklist

  • APP_ENV=production, APP_DEBUG=false
  • .env DB credentials correct, php artisan migrate ran cleanly
  • public/ renamed to public_html, config/filesystems.php updated (step 12), storage:link created
  • php artisan optimize run 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
@Symon-kalola

Copy link
Copy Markdown

thank you.. this was helpful

@Benezerds

Copy link
Copy Markdown

Thank you very much for the comprehensive guide. I will try it out!

@Jiysea

Jiysea commented Oct 20, 2024

Copy link
Copy Markdown

Thanks for this guide. However, I'm using the Single Web Hosting option from Hostinger so basically I don't have any SSH access. Do you have any alternative ways to host it without SSH access?

@MoneerKamal

Copy link
Copy Markdown

thanks you saved my life

@feljohn07

Copy link
Copy Markdown

need help on this error

`[u574655838@us-bos-web1570 mediumaquamarine-emu-171723.hostingersite.com]$ php artisan storage:link

Error

Call to undefined function Illuminate\Filesystem\symlink()

at vendor/laravel/framework/src/Illuminate/Filesystem/Filesystem.php:355
351▕ */
352▕ public function link($target, $link)
353▕ {
354▕ if (! windows_os()) {
➜ 355▕ return symlink($target, $link);
356▕ }
357▕
358▕ $mode = $this->isDirectory($target) ? 'J' : 'H';
359▕

  +14 vendor frames

15 artisan:13
Illuminate\Foundation\Application::handleCommand()`

@nahom17

nahom17 commented Feb 10, 2025

Copy link
Copy Markdown

cd public_html
unlink storage

ln -s ../storage/app/public storage

@keinermendoza

Copy link
Copy Markdown

Thanks for this guide. However, I'm using the Single Web Hosting option from Hostinger so basically I don't have any SSH access. Do you have any alternative ways to host it without SSH access?

If you don't have access to SSH (like in my case), you can use Cron Jobs to run the commands.

In the sidebar, at the bottom, there is an Advanced option—select it. Then, select Cron Jobs.
You will see in the main area that the default option is PHP—that's fine. You can paste the following commands to run the migrations:

domains/<replace.com>/artisan migrate

NOTE: You can run any Artisan command you need using this format.

Replace <replace.com> with your domain.

Set the schedule to every minute, hour, day, month, and week, then save.
Wait until the current minute ends and check the output.


If you need to run a command outside of PHP, select Custom instead of PHP. For example, to create a symbolic link without modifying the configuration:
ln -s /home/<u123456789>/domains/<replace.com>/storage/app/public /home/<u123456789>/domains/<replace.com>/public_html/storage

In this case, replace with your user ID.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment