# Laravel on a Hostinger VPS: Nginx, PHP-FPM, MySQL and Queue Workers

URL: https://www.savemores.store/vps/deploy-laravel
Author: Mr. Ahmed · Published 2026-10-17 · Updated 2026-10-17 · VPS & deployment

## Short answer

- To deploy Laravel on a Hostinger VPS, install Nginx, PHP 8.3-FPM, MySQL and Composer on Ubuntu 24.04, point the Nginx root at the project's `public` folder, and add HTTPS with Certbot.
- Most 500 errors on a fresh server come from `storage` and `bootstrap/cache` not being writable by the `www-data` group, or a missing `APP_KEY`.
- Never point Nginx at the project root, and never ship `APP_DEBUG=true`; either one can expose `.env` and your database password.
- Run queue workers as a systemd service with `Restart=always`, and restart them on every deploy because they keep old code in memory.
- Simple Laravel sites can run on Hostinger's Premium or Business shared plans; always-on queues, Redis or Reverb WebSockets need a VPS.

## VPS or shared hosting for Laravel?

A straightforward Laravel site runs fine on Hostinger's Premium or Business [shared plans](https://www.savemores.store/hostinger/web-hosting). They include SSH, Composer and a recent PHP, and you point the domain at the `public` folder.

Move to a VPS when you need queue workers running constantly, Redis, WebSockets via Reverb, or full control of PHP extensions. That is the setup below. [KVM 1](https://www.savemores.store/hostinger-deals/kvm-1) is a sensible starting size.

## Prerequisites

Have the groundwork in place before Step 1.

- Ubuntu 24.04 LTS on the VPS, with a sudo user and a firewall allowing ports 80 and 443. The [VPS security checklist](https://www.savemores.store/vps/secure-new-vps) covers this.
- An A record for your domain pointing at the server. See [pointing your domain at the VPS](https://www.savemores.store/vps/point-domain-dns).
- The app in a Git repository the server can pull from.

## Step 1: PHP, Nginx, MySQL and Composer

Ubuntu 24.04 ships PHP 8.3, which satisfies current Laravel releases; check the `php` constraint in your `composer.json` to be sure. On Ubuntu 26.04, replace `8.3` in the package names with whatever `php -v` reports, here and in the config files later.

```bash
sudo apt update
sudo apt install -y nginx mysql-server unzip git \
  php8.3-fpm php8.3-cli php8.3-mysql php8.3-mbstring php8.3-xml \
  php8.3-curl php8.3-zip php8.3-bcmath php8.3-intl php8.3-gd

sudo apt install -y composer
composer --version
```

## Step 2: a database user with one job

The app gets its own MySQL user, scoped to a single database. Never connect as root.

```bash
sudo mysql <<'SQL'
CREATE DATABASE laravel CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'laravel'@'localhost' IDENTIFIED BY 'use-a-long-random-password';
GRANT ALL PRIVILEGES ON laravel.* TO 'laravel'@'localhost';
FLUSH PRIVILEGES;
SQL
```

## Step 3: clone, install, configure

Your deploy user owns the code. The web server's `www-data` group needs write access to exactly two places, `storage` and `bootstrap/cache`. Getting this wrong causes most of the 500 errors on a fresh Laravel server.

1. Clone the repository into /var/www and install production dependencies.
2. Create `.env` from the example and generate the app key.
3. Set the production values in `.env`.
4. Fix ownership and permissions, run migrations, link storage and build the caches.

```bash
sudo mkdir -p /var/www && sudo chown $USER:$USER /var/www
cd /var/www
git clone https://github.com/you/your-app.git laravel
cd laravel

composer install --no-dev --optimize-autoloader
cp .env.example .env
php artisan key:generate
```

.env: the production lines that matter

```bash
APP_ENV=production
APP_DEBUG=false
APP_URL=https://example.com

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_DATABASE=laravel
DB_USERNAME=laravel
DB_PASSWORD=use-a-long-random-password
```

Permissions, migrations and caches:

```bash
sudo chown -R $USER:www-data storage bootstrap/cache
sudo chmod -R 775 storage bootstrap/cache

php artisan migrate --force
php artisan storage:link
php artisan optimize
```

> From the bench: With `APP_DEBUG=true` in production, every error page prints your environment variables, database password included. Check that line twice before the domain goes live. It is the cheapest security fix you will ever make.

## Step 4: the Nginx server block

The document root must be `public`, not the project folder. Point Nginx at the project root and anyone can download `.env` by asking for it. The final `location` block denies dotfiles as a second line of defense.

/etc/nginx/sites-available/laravel

```nginx
server {
    listen 80;
    server_name example.com www.example.com;
    root /var/www/laravel/public;

    index index.php;
    charset utf-8;
    client_max_body_size 20M;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

    location ~ ^/index\.php(/|$) {
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_hide_header X-Powered-By;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}
```

```bash
sudo ln -s /etc/nginx/sites-available/laravel /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx
```

## Step 5: HTTPS

Identical to any Nginx site: Certbot installs the certificate and a timer renews it.

```bash
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
```

## Step 6: the queue worker and the scheduler

If the app dispatches jobs, something must process them continuously. A systemd unit restarts the worker when it crashes and starts it at boot. The scheduler needs one cron line. Replace `deploy` in the unit with your own username.

/etc/systemd/system/laravel-worker.service

```ini
[Unit]
Description=Laravel queue worker
After=network.target mysql.service

[Service]
User=deploy
Group=www-data
WorkingDirectory=/var/www/laravel
ExecStart=/usr/bin/php artisan queue:work --sleep=3 --tries=3 --max-time=3600
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
```

```bash
sudo systemctl daemon-reload
sudo systemctl enable --now laravel-worker

# Scheduler: run 'crontab -e' as deploy and add
* * * * * cd /var/www/laravel && php artisan schedule:run >> /dev/null 2>&1
```

## Step 7: a redeploy script

Save this as `deploy.sh` in the project. It works unchanged with [push-to-deploy via GitHub Actions](https://www.savemores.store/vps/github-actions-deploy). Workers hold the old code in memory, which is why the script restarts them on every release.

```bash
#!/usr/bin/env bash
set -euo pipefail
cd /var/www/laravel

php artisan down --retry=15 || true
git pull origin main
composer install --no-dev --optimize-autoloader --no-interaction
php artisan migrate --force
php artisan optimize
sudo systemctl restart laravel-worker
sudo systemctl reload php8.3-fpm
php artisan up

echo "Deployed at $(date -Iseconds)"
```

> From the bench: For CI to run those two `systemctl` lines without a password prompt, add a sudoers rule limited to exactly those commands using `sudo visudo -f /etc/sudoers.d/deploy`. Resist the urge to grant blanket passwordless sudo; scope it to what the script needs.

## Verify it worked, and troubleshooting

Check services first, then the app's own log, then the public URL.

- **500 error**: almost always permissions on `storage` or `bootstrap/cache`, or a missing `APP_KEY`. Read `storage/logs/laravel.log` and rerun the `chown` and `chmod` from Step 3.
- **502 Bad Gateway**: the `fastcgi_pass` socket does not match the installed PHP version. Compare it with `ls /run/php/`.
- **Jobs pile up or run old code**: the worker is stopped or was not restarted after a deploy. `sudo systemctl restart laravel-worker`.
- **`.env` is downloadable**: the Nginx root is the project folder instead of `public`. Fix it immediately and rotate every secret in the file.

```bash
sudo systemctl status php8.3-fpm laravel-worker --no-pager
crontab -l                                   # scheduler line present
tail -n 50 /var/www/laravel/storage/logs/laravel.log
curl -I https://example.com                  # 200 with a valid certificate
curl -I https://example.com/.env             # must NOT return 200
```

## FAQ

### Can I host Laravel on Hostinger shared hosting?

Yes, on plans with SSH access, which means Premium and above. Upload the project, run Composer over SSH and set the domain's document root to the public folder. Always-on queue workers and Redis need a VPS.

### Which Hostinger VPS plan is enough for Laravel?

KVM 1 runs a typical Laravel app with MySQL and a queue worker comfortably. Move to KVM 2 when you add Redis, Horizon with several queues, or noticeably more traffic.

### Why does Laravel show a 500 error after deploying?

Usually because storage or bootstrap/cache is not writable by the www-data group, or APP_KEY is missing. Check storage/logs/laravel.log and reapply the ownership and permission commands.

### Should I use Laravel Forge or a control panel instead?

If you would rather not manage the server by hand, yes. Laravel Forge or a panel like CloudPanel provisions the same stack for you on the same Hostinger VPS.
