Running Laravel on shared hosting always turns into a fight with memory limits, missing daemon processes for queues, and painfully slow asset delivery. Moving to a cheap VPS fixes all of that. Here is how I set up a fresh Ubuntu 24.04 instance on a Hostinger VPS with PHP 8.3-FPM, Nginx, Namecheap DNS, Certbot SSL, and edge CDN caching for Vite assets.

1. Provision the VPS and Install the Core PHP 8.3 Stack
Spin up an Ubuntu 24.04 LTS instance in your Hostinger dashboard. SSH in as root, update your package lists, and pull in Ondřej Surý’s PPA so you get PHP 8.3 with all the extensions Laravel actually needs.
sudo apt update && sudo apt upgrade -y
sudo apt install -y software-properties-common curl git unzip nginx supervisor
sudo add-apt-repository -y ppa:ondrej/php
sudo apt update sudo apt install -y php8.3-fpm php8.3-cli php8.3-common php8.3-mysql
php8.3-xml php8.3-curl php8.3-mbstring php8.3-zip php8.3-bcmath
php8.3-intl php8.3-redis php8.3-sqlite3Install Composer globally so you can handle dependencies on the box.
curl -sS https://getcomposer.org/installer -o /tmp/composer-setup.php
sudo php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer
composer --versionIf you plan on running SSR or building assets directly on the server, see our guide to deploying Node.js on Hostinger VPS.
2. Configure Non-Root Deployment User and MySQL Database
Do not run your app or deploy tasks as root. Create a dedicated deployer user and add it to the www-data group right away.
sudo adduser --gecos "" deployer
sudo usermod -aG sudo deployer
sudo usermod -aG www-data deployer
sudo mkdir -p /var/www/my-laravel-app
sudo chown -R deployer:www-data /var/www/my-laravel-app
sudo chmod 775 /var/www/my-laravel-appNext, install MySQL.
sudo apt install -y mysql-server
sudo mysql_secure_installationHop into MySQL as root and create your application database and dedicated user.
sudo mysql -u rootCREATE DATABASE laravel_production CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'laravel_user'@'localhost' IDENTIFIED BY 'YourStrongPasswordHere!';
GRANT ALL PRIVILEGES ON laravel_production.* TO 'laravel_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;3. Pull Laravel, Set Permissions, and Run Optimizations
Switch over to your deployer user, clone your repo into /var/www, install production dependencies, and set up your .env file.
su - deployer
cd /var/www/my-laravel-app
git clone https://github.com/yourusername/your-repo.git .
composer install --no-dev --optimize-autoloader
cp .env.example .env
php artisan key:generateUpdate .env with your actual app URL, database credentials, and production environment settings.
APP_NAME=LaravelApp
APP_ENV=production
APP_KEY=base64:yourGeneratedKeyGoesHere=
APP_DEBUG=false
APP_URL=https://app.example.com DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_production
DB_USERNAME=laravel_user
DB_PASSWORD=YourStrongPasswordHere! QUEUE_CONNECTION=database
SESSION_DRIVER=databaseRun your migrations and cache your config, routes, and views for production.
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan storage:linkFix permissions so PHP-FPM can write to storage and bootstrap/cache while your deployer user still owns the repository.
sudo chown -R deployer:www-data /var/www/my-laravel-app
sudo chmod -R 775 /var/www/my-laravel-app/storage
sudo chmod -R 775 /var/www/my-laravel-app/bootstrap/cache4. Configure Nginx with Long-Lived Static Asset Headers
Set up an Nginx server block pointing to Laravel’s public/ folder. I like to add aggressive cache headers specifically for /build/assets/ so browsers and CDNs cache hashed Vite files for a full year.
Create /etc/nginx/sites-available/my-laravel-app:
server { listen 80; listen [::]:80; server_name app.example.com; root /var/www/my-laravel-app/public; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "no-referrer-when-downgrade" always; index index.php index.html; charset utf-8; # Vite hashed assets - cache for 1 year location ~* ^/build/assets/.*.(?:css|js|woff2?|png|jpe?g|gif|svg|webp|ico)$ { expires 1y; add_header Cache-Control "public, max-age=31536000, immutable"; access_log off; try_files $uri =404; } # General static files location ~* .(?:ico|css|js|gif|jpe?g|png|woff2?|eot|ttf|svg)$ { expires 30d; add_header Cache-Control "public, max-age=2592000"; access_log off; try_files $uri =404; } 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 ~ .php$ { fastcgi_pass unix:/var/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; }
}Symlink the config to sites-enabled, test the syntax, and reload Nginx.
sudo ln -s /etc/nginx/sites-available/my-laravel-app /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginxFor more on tuning cache headers, check out our guide on immutable cache-control and cache busting in Nginx.
5. Configure Namecheap DNS Records
Before running Certbot, point your domain to your Hostinger VPS IP in Namecheap.
- Log in to Namecheap and open your Domain List.
- Click Manage next to your domain, then open the Advanced DNS tab.
- Add an A Record with host
app(or@for root) pointing to your Hostinger VPS public IP. Set TTL to1 minwhile testing so changes apply fast. - If you’re using a CDN CNAME or subdomain, create that CNAME Record pointing to your CDN distribution.
Check that DNS has propagated before touching Certbot:
dig +short app.example.comIf the IP doesn’t match your VPS, wait a few minutes before moving on.
6. Provision Free SSL with Let’s Encrypt Certbot
Grab Certbot and its Nginx plugin to issue and auto-renew TLS certificates.
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d app.example.comRun Certbot, provide your email, agree to the terms, and let it handle the HTTP-01 challenge and Nginx config updates automatically.
If Certbot chokes on domain verification, check our guide on fixing Certbot ACME HTTP-01 challenge errors on Nginx.
Run a dry run to make sure auto-renewal actually works:
sudo certbot renew --dry-run7. Setup Supervisor for Laravel Queues and Systemd Cron
If your app uses background jobs, you need Supervisor to keep queue workers alive. You also need a cron job for Laravel’s task scheduler.
Create your Supervisor config at /etc/supervisor/conf.d/laravel-worker.conf:
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/my-laravel-app/artisan queue:work database --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=deployer
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/my-laravel-app/storage/logs/worker.log
stopwaitsecs=3600Tell Supervisor to reread and update its processes:
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-worker:*Now open the crontab for your deployer user:
crontab -e -u deployerAdd the Laravel scheduler heartbeat:
* * * * * cd /var/www/my-laravel-app && php artisan schedule:run >> /dev/null 2>&18. Configure Edge CDN Static Asset Caching
Laravel serves dynamic responses on web routes, but Vite generates content-hashed assets (like app-B3f8a9e.js). Putting a CDN in front of your Hostinger VPS takes the load off your server.
- Origin Configuration: Point your CDN origin to
app.example.comor your VPS IP with the Host header set toapp.example.com. - Bypass Dynamic Routes: Make sure your CDN passes cookies and
Authorizationheaders through without caching HTML pages. CachingSet-Cookieheaders at the edge will break user sessions. - Edge Cache Rules: Tell the CDN to cache
/build/*,/images/*, and/fonts/*using theCache-Controlheaders we configured in Nginx. - Origin Lockdown: Stop scrapers and bots from bypassing your CDN by following our guide to restricting Nginx access to CDN origin shield IPs.
Test that your caching headers are firing properly with curl:
curl -I https://app.example.com/build/assets/app.cssYou should see cache-control: public, max-age=31536000, immutable in the response.
Frequently Asked Questions
Why do I get a 500 Server Error immediately after cloning Laravel?
It’s almost always a permissions issue or a missing app key. Verify that storage/ and bootstrap/cache/ are writable by www-data and make sure you ran php artisan key:generate after copying .env.example to .env.
How do I deploy future application updates without downtime?
Put together a quick deploy script on the VPS to pull changes, run migrations, update dependencies, rebuild caches, and reload PHP-FPM:
#!/bin/bash
set -e
cd /var/www/my-laravel-app
php artisan down || true
git pull origin main
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan queue:restart
php artisan upShould I build Vite assets on the VPS or in GitHub Actions?
Build them in GitHub Actions and push the compiled public/build folder to the server. Running npm run build on a low-RAM VPS can trigger the Linux OOM killer and abruptly kill your MySQL server or PHP workers.
Next Steps
Your Laravel app is now running on a Hostinger VPS with HTTPS, proper queue and cron handlers, and edge caching for static assets. If you want to containerize this setup instead, check out our guide on deploying Docker Compose apps on Hostinger VPS.

