VPS & deployment
Laravel on a Hostinger VPS: Nginx, PHP-FPM, MySQL and Queue Workers
Deploy Laravel on a Hostinger VPS: PHP 8.3-FPM, Composer, MySQL, a safe Nginx config, storage permissions, a systemd queue worker, the scheduler and free HTTPS.
By Mr. Ahmed · 20+ years in tech
Published · 11 min read
The short answer
- 01To 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
publicfolder, and add HTTPS with Certbot. - 02Most 500 errors on a fresh server come from
storageandbootstrap/cachenot being writable by thewww-datagroup, or a missingAPP_KEY. - 03Never point Nginx at the project root, and never ship
APP_DEBUG=true; either one can expose.envand your database password. - 04Run queue workers as a systemd service with
Restart=always, and restart them on every deploy because they keep old code in memory. - 05Simple 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. 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 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 covers this.
- An A record for your domain pointing at the server. See pointing your domain at the VPS.
- 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.
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 --versionStep 2: a database user with one job
The app gets its own MySQL user, scoped to a single database. Never connect as root.
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;
SQLStep 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.
- 1Clone the repository into /var/www and install production dependencies.
- 2Create
.envfrom the example and generate the app key. - 3Set the production values in
.env. - 4Fix ownership and permissions, run migrations, link storage and build the caches.
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:generateAPP_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-passwordsudo 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 optimizeStep 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.
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;
}
}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 nginxStep 5: HTTPS
Identical to any Nginx site: Certbot installs the certificate and a timer renews it.
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.comStep 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.
[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.targetsudo 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>&1Step 7: a redeploy script
Save this as deploy.sh in the project. It works unchanged with push-to-deploy via GitHub Actions. Workers hold the old code in memory, which is why the script restarts them on every release.
#!/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)"Verify it worked, and troubleshooting
Check services first, then the app's own log, then the public URL.
- 500 error: almost always permissions on
storageorbootstrap/cache, or a missingAPP_KEY. Readstorage/logs/laravel.logand rerun thechownandchmodfrom Step 3. - 502 Bad Gateway: the
fastcgi_passsocket does not match the installed PHP version. Compare it withls /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.
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 200Questions people ask
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.
Independent site, not Hostinger. Buy through my links and Hostinger pays me a referral fee; your price is the same or lower. How that works.