This guide shows you how to deploy a Laravel app to Hostinger shared hosting with Bitbucket Pipelines — automatically, on every push to main. No FTP, no manual composer install over SSH each time. Laravel adds two wrinkles a static site doesn’t have: it needs Composer dependencies built, and its public/ folder has to line up with the host’s public_html. We’ll handle both.
The shape of the pipeline
Shared hosting has PHP but no reliable Composer, no root, and tight resource limits — so we do the heavy lifting in CI: Bitbucket installs dependencies and builds assets, then ships the result to Hostinger over SSH. On every push, Pipelines runs composer install (and an asset build if you use Vite), then rsyncs the project to your account and runs a few post-deploy Artisan commands.
Why build in CI instead of on the server?
It’s tempting to just git pull and composer install over SSH on the server itself. Avoid it. Shared hosts cap memory and CPU, so a composer install on a fresh deploy often dies with an out-of-memory error halfway through — leaving your vendor/ folder in a broken, half-installed state while the site is live. Building in Bitbucket’s container sidesteps every one of those limits: full memory, a clean environment each run, and a build that either fully succeeds before anything is shipped or fails without touching production. You also get a record of every deploy and the ability to roll back by re-running an older pipeline. The server’s only job becomes receiving finished files and running migrations — work it can handle comfortably.
Prerequisites
- A Hostinger plan with SSH — see how to SSH into Hostinger (remember, port 65002).
- A deploy SSH key added to Hostinger, with the private half stored in Bitbucket.
- Your Laravel app in a Bitbucket repo, with a production
.envalready placed on the server (never commit secrets).
Step 1 — Solve the public/ vs public_html problem first
Laravel serves from public/, but Hostinger serves your domain from public_html. The cleanest fix is to upload the app outside the web root and point the domain’s document root at the app’s public folder (Hostinger lets you set the document root per domain in hPanel). If you can’t, use the index.php path patch. We cover all three approaches in detail in the Laravel public_html guide — decide your layout before automating, because it determines your deploy target path.
For this walkthrough we’ll deploy the whole app to ~/laravel-app and assume the domain’s document root points at ~/laravel-app/public.
Step 2 — Store connection details in Bitbucket
Add the deploy key under Repository settings → Pipelines → SSH keys and add your server (with the port, e.g. your-ip:65002) under Known hosts. Then add repository variables:
| Variable | Example |
|---|---|
SSH_USER | u123456789 |
SSH_HOST | 123.45.67.89 |
REMOTE_PATH | /home/u123456789/laravel-app/ |
Step 3 — The bitbucket-pipelines.yml
Commit this to the repo root. It builds on a PHP image, ships the files, then runs post-deploy commands over SSH:
image: php:8.3-cli
pipelines:
branches:
main:
- step:
name: Build (composer)
caches:
- composer
script:
- apt-get update && apt-get install -y unzip git
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
- composer install --no-dev --optimize-autoloader --no-interaction
artifacts:
- vendor/**
- step:
name: Deploy to Hostinger
deployment: production
script:
- pipe: atlassian/rsync-deploy:1.3.0
variables:
USER: $SSH_USER
SERVER: $SSH_HOST
REMOTE_PATH: $REMOTE_PATH
LOCAL_PATH: '.'
SSH_PORT: '65002'
EXTRA_ARGS: '--exclude=.env --exclude=.git --exclude=storage --exclude=node_modules'
- step:
name: Post-deploy (artisan)
script:
- pipe: atlassian/ssh-run:0.8.1
variables:
SSH_USER: $SSH_USER
SERVER: $SSH_HOST
PORT: '65002'
COMMAND: 'cd laravel-app && php artisan migrate --force && php artisan config:cache && php artisan route:cache'
Why it’s built this way:
--no-dev --optimize-autoloaderkeeps the production install lean and fast.- The excludes matter. Never overwrite the server’s
.envorstorage/(user uploads, logs), and don’t ship.git. artisan migrate --forceis required in non-interactive (production) mode;config:cacheandroute:cachespeed up the live app.- Separate steps mean a failed migration doesn’t leave a half-copied site — the deploy step has already completed atomically via rsync.
Step 4 — Handle assets (if you use Vite)
If your app compiles front-end assets, add a Node build before the deploy step (or use a multi-tool image). The simplest approach is a build sub-step:
- step:
name: Build assets
image: node:20
caches:
- node
script:
- npm ci
- npm run build
artifacts:
- public/build/**
The compiled public/build then rides along in the rsync deploy.
Step 5 — Push and verify
git add bitbucket-pipelines.yml
git commit -m "Automate Laravel deploy to Hostinger"
git push origin main
Watch the three steps go green in Bitbucket’s Pipelines view, then load your domain. From now on, every push to main rebuilds dependencies and redeploys.
Troubleshooting
- 500 error after deploy. Usually a stale config cache or a missing
.env/APP_KEYon the server. SSH in and runphp artisan config:clear, then confirm.envexists. - “Permission denied” on deploy. Deploy key not on Hostinger, or Bitbucket using the wrong key. Re-check your Hostinger SSH keys.
- Connection refused. Port must be
65002in both the rsync and ssh-run pipes. - Site loads the wrong folder. Your document root isn’t pointing at
public/— revisit Step 1. - Migrations hang. Add
--force(production is non-interactive) and make sure DB credentials in the server.envare correct.
Deploying an Angular front-end instead? The static-build version is in our guide on deploying Angular to Hostinger with Bitbucket Pipelines.
Frequently asked questions
Can I deploy Laravel to shared hosting with CI/CD?
Yes. Build dependencies in Bitbucket Pipelines, rsync the app to your Hostinger account over SSH, then run artisan migrate --force and cache commands as a post-deploy step — no root or Docker needed.
How do I run artisan migrate during deployment?
Add a post-deploy step that SSHes into the server (using the atlassian/ssh-run pipe on port 65002) and runs php artisan migrate --force. The --force flag is required because production runs non-interactively.
Where do I put the .env file?
Place the production .env on the server once, and exclude it from the rsync (--exclude=.env) so deploys never overwrite it. Never commit secrets to the repository.
