Shared hosting is where most client Laravel projects end up, and Hostinger is one of the cheapest places to put one. It also has three quirks that will break your first deploy: the PHP version the account defaults to, a public_html that expects a plain PHP site, and a CDN that keeps serving your old CSS for a week. This guide walks the whole deploy and fixes all three.
What you need before you start
- A Hostinger plan with SSH access (Premium and above)
- Your Laravel project in a Git repository
- A MySQL database created in hPanel
1. Set up SSH key access
hPanel gives you an SSH host, port and password. Typing that password on every deploy gets old immediately, so add a key once:
ssh-keygen -t ed25519 -f ~/.ssh/hostinger -C "deploy"
ssh-copy-id -i ~/.ssh/hostinger.pub -p 65002 u123456789@your-server-ip
Then add a host alias to ~/.ssh/config so you can type ssh myhost instead of the full command:
Host myhost
HostName 145.79.0.0
User u123456789
Port 65002
IdentityFile ~/.ssh/hostinger
Once the key works you can change the account password without breaking deployment, which is worth doing since that password arrived in an email.
2. Pick the right PHP version — this is where most deploys die
Laravel 11 and 12 pull in Symfony packages that require a recent PHP. Hostinger accounts often default the CLI to an older build, so composer install fails with a wall of messages like:
symfony/console v8.0.0 requires php >=8.4 -> your php version (8.3.30) does not satisfy that requirement.
Changing the PHP version in hPanel only changes what the web requests use. The SSH shell keeps its own default. Check what you actually have:
php -v
ls -d /opt/alt/php8*
If a newer build is installed, call it directly for Composer and Artisan:
/opt/alt/php84/usr/bin/php /usr/local/bin/composer install --no-dev -o
/opt/alt/php84/usr/bin/php artisan migrate --force
To make the web side match, add a handler line at the top of your .htaccess:
AddHandler application/x-httpd-alt-php84 .php
Put that in the file rather than only in hPanel, otherwise a future account change silently reverts it and the site 500s.
3. Decide where the app lives
Hostinger serves ~/domains/yourdomain.com/public_html as the web root. Laravel expects only its public directory to be reachable. You have two options.
The secure layout
Put the application one level above the web root and copy only the contents of public/ into public_html, then edit the two require paths in index.php. Nothing outside public/ is reachable, which is how Laravel is designed to run.
The everything-inside layout
Put the whole application inside public_html and route requests into public/ with a rewrite. Easier to picture, but your .env now sits under the web root, so it depends entirely on .htaccess holding:
RewriteEngine On
RewriteRule ^(\.env|artisan|composer\.(json|lock))$ - [F,L]
RewriteCond %{REQUEST_URI} !^/public/
RewriteRule ^(.*)$ public/$1 [L]
If you take this route, drop a .htaccess containing Require all denied inside app/, config/, vendor/ and storage/ as well. Then actually test it — request /.env in a browser and confirm you get a 403, not a download.
4. Environment and database
Create the database in hPanel first. Hostinger prefixes both the database and the user with your account ID, so a database you named blog is really u123456789_blog. Using the name you typed will fail with an access-denied error that looks like a password problem but is not.
APP_ENV=production
APP_DEBUG=false
APP_URL=https://yourdomain.com
DB_DATABASE=u123456789_blog
DB_USERNAME=u123456789_blog
DB_PASSWORD="your-password"
Keep .env out of your deploy sync so a push never overwrites live credentials, and generate the key on the server once with php artisan key:generate.
5. Deploy, migrate, cache
Whether you rsync from your machine or git pull on the server, the remote half is the same every time:
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
Exclude vendor/, node_modules/, .env and your uploads directory from whatever copies files up. Deleting a client's uploaded images because rsync ran with --delete is a bad afternoon.
6. The three things that still bite you
Asset builds fail on the server
Vite's newer bundler needs to spawn threads, and shared hosting process limits stop it, so npm run build panics. Build locally and upload the output, or commit the built assets.
storage:link does nothing
Hostinger disables PHP's symlink(), so artisan storage:link cannot create the link uploads are served through. There is a clean way around it — I wrote that up separately in fixing Laravel file uploads when symlink() is disabled.
Your CSS changes do not appear
Hostinger's CDN caches static files for seven days. You deploy, you reload, and you still see the old stylesheet — so you assume the deploy failed and deploy again. It did not fail; the file is simply cached. The fix is asset versioning, covered in why your site serves old CSS after deploying.
A deploy checklist worth keeping
- SSH key added, password login no longer needed
- PHP version correct for both CLI and web
.envpresent, excluded from sync,APP_DEBUG=false/.envreturns 403 in a browser- Migrations run, caches rebuilt
- Uploads directory excluded from deletion
- Assets versioned so the CDN cannot serve stale files
Get those seven right and Laravel runs on shared hosting perfectly well. Most of the horror stories come from one of them being missed, not from the platform.