This post contains affiliate links. If you purchase through our links, we may earn a commission at no extra cost to you.

Shared hosting will quietly break your Laravel app. Queue workers won’t run, you can’t pin a PHP version, and the moment your .env file gets exposed in a public directory, you’ve got a real problem. A VPS fixes all of that, and Vultr’s High Performance tier gives you NVMe SSD storage at $12/month — the same price as the “optimized” shared plans that don’t even support PHP-FPM. This guide takes you from a blank Ubuntu 24.04 server to a production-ready Laravel deployment with SSL, Supervisor-managed queue workers, and cron scheduling. I’ve laid out every real command in the order you actually run them.
What You’ll Need (Prerequisites)
- A domain name pointed at your server’s IP (A record set before Step 8)
- Your Laravel app in a Git repo (GitHub, GitLab, or self-hosted)
- Basic SSH comfort — you don’t need to be a sysadmin, but you should know how to connect and run commands
- About 30–45 minutes
No Docker. No CI/CD pipeline required. This is the manual, direct setup that works.
Step 1 — Create Your Vultr VPS
Pick the right plan. The $5/month 1 GB RAM option looks tempting, but it’ll OOM (out-of-memory kill) when PHP-FPM, MySQL, Nginx, and a queue worker are all running at the same time. Don’t find that out in production.
Recommended plan: Cloud Compute → High Performance (AMD), 1 vCPU, 2 GB RAM, 50 GB NVMe SSD — $12/month. New accounts get $300 in free cloud credits (valid for 30 days).
Check out our Vultr pricing breakdown if you want to compare the full plan matrix before committing.
Setup steps:
- Log in to Vultr → click Deploy
- Select Cloud Compute → High Performance (AMD)
- Pick the datacenter closest to your users
- OS: Ubuntu 24.04 LTS
- Plan: 2 GB RAM / $12/month
- Enable backups ($2.40/mo — worth it)
- Add your SSH public key
- Click Deploy Now
Wait about 60 seconds for provisioning, then grab the IP from the dashboard.
Step 2 — Connect and Update the Server
ssh root@YOUR_SERVER_IP
Update packages and create a non-root deploy user:
apt update && apt upgrade -y
adduser deploy
usermod -aG sudo deploy
Copy your SSH key to the new user, then disable root login by editing /etc/ssh/sshd_config and setting PermitRootLogin no. From here, use the deploy user for everything.
Set up UFW firewall before you install anything else:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
💡 Tip: Prefer a visual approach? You can build these rules with our free Firewall Rule Builder — it generates ufw and iptables commands right in your browser.
Step 3 — Install PHP 8.3, Nginx, and Composer
PHP 8.3 is the 2026 production standard for Laravel — it’s what ships in Ubuntu 24.04’s default repos, and it satisfies both Laravel 12 and Laravel 13 requirements.
Install Nginx:
sudo apt install nginx -y
sudo systemctl enable nginx
Install PHP 8.3 and all the extensions Laravel needs:
sudo apt install php8.3 php8.3-fpm php8.3-mysql php8.3-mbstring \
php8.3-xml php8.3-curl php8.3-zip php8.3-bcmath php8.3-tokenizer \
php8.3-json php8.3-redis -y
sudo systemctl enable php8.3-fpm
Install Redis (for cache, sessions, and queue connections):
sudo apt install redis-server -y
sudo systemctl enable redis
Install Composer:
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php composer-setup.php
sudo mv composer.phar /usr/local/bin/composer
Verify everything installed cleanly:
php -v
nginx -v
composer --version
Step 4 — Install and Configure MySQL
sudo apt install mysql-server -y
sudo mysql_secure_installation
Run through the secure installation prompts — set a root password, remove anonymous users, disable remote root login. Then create your Laravel database and user:
sudo mysql -u root -p
CREATE DATABASE laravel_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'laravel'@'localhost' IDENTIFIED BY 'YourStrongPassword!';
GRANT ALL PRIVILEGES ON laravel_db.* TO 'laravel'@'localhost';
FLUSH PRIVILEGES;
EXIT;
utf8mb4 is the right charset — it handles emoji and full Unicode, unlike the legacy utf8 alias.
Step 5 — Clone Your Laravel App and Configure .env
cd /var/www
sudo git clone https://github.com/youruser/yourapp.git laravel
sudo chown -R www-data:www-data /var/www/laravel
Install Composer dependencies (no dev packages in production):
cd /var/www/laravel
sudo -u www-data composer install --no-dev --optimize-autoloader
Copy the environment file and generate an app key:
cp .env.example .env
php artisan key:generate
Open .env and fill in your production values:
APP_ENV=production
APP_DEBUG=false
APP_URL=https://yourdomain.com
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_db
DB_USERNAME=laravel
DB_PASSWORD=YourStrongPassword!
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
APP_DEBUG=false is non-negotiable in production. Leaving it true exposes stack traces — and your env values — in browser error messages.
Step 6 — Set Permissions and Run Artisan Commands
Laravel needs write access to storage and bootstrap/cache:
sudo chmod -R 755 /var/www/laravel/storage
sudo chmod -R 755 /var/www/laravel/bootstrap/cache
Run migrations, create the storage symlink, and cache everything:
php artisan migrate --force
php artisan storage:link
php artisan optimize
php artisan optimize is the Laravel 12+ single-command replacement for the old config:cache && route:cache && view:cache sequence. It handles config, events, routes, and views in one shot. Run it on every deployment.
Step 7 — Configure Nginx for Laravel
Create the server block at /etc/nginx/sites-available/laravel.conf:
server {
listen 80;
listen [::]:80;
server_name yourdomain.com www.yourdomain.com;
root /var/www/laravel/public;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
index index.php;
charset utf-8;
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:/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;
}
}
The fastcgi_pass unix:/var/run/php/php8.3-fpm.sock line connects Nginx to PHP-FPM via a Unix socket — lower overhead than a TCP connection. The deny all block on dotfiles keeps your .env from being served directly.
Enable the config and reload:
sudo ln -s /etc/nginx/sites-available/laravel.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx
Visit your server’s IP in a browser — you should see your Laravel app.
Step 8 — Enable SSL with Certbot
Your domain’s DNS A record needs to be pointing at the VPS IP before this step.
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx --redirect -d yourdomain.com -d www.yourdomain.com \
-m [email protected] --agree-tos
Certbot verifies domain ownership, issues a 90-day Let’s Encrypt certificate, rewrites your Nginx config to redirect HTTP → HTTPS, and sets up a systemd timer for auto-renewal. Verify the renewal process works:
sudo certbot renew --dry-run
Open the HTTPS port if you haven’t already:
sudo ufw allow 443/tcp
sudo ufw reload
Step 9 — Set Up Queue Workers with Supervisor
Supervisor keeps your queue workers running as background processes and restarts them automatically if they crash.
sudo apt install supervisor -y
sudo systemctl enable supervisor
Create the worker config at /etc/supervisor/conf.d/laravel-worker.conf:
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/laravel/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
user=www-data
numprocs=3
redirect_stderr=true
stdout_logfile=/var/www/laravel/storage/logs/worker.log
stopwaitsecs=3600
numprocs=3 runs three parallel workers — enough for most small-to-medium apps on the 2 GB plan. If you’re using Laravel Horizon instead, set numprocs=1 and change the command to php /var/www/laravel/artisan horizon. Horizon manages its own worker pool internally.
Load and start the workers:
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-worker:*
sudo supervisorctl status
You should see all three worker processes in RUNNING state.
Step 10 — Schedule Tasks with Cron
Laravel’s scheduler needs exactly one cron entry regardless of how many scheduled tasks your app has. Open the crontab for www-data:
💡 Tip: Not sure about the cron syntax? Our free Cron Expression Builder lets you set the schedule visually and previews the next five runs.
sudo crontab -u www-data -e
Add this single line:
* * * * * php /var/www/laravel/artisan schedule:run >> /dev/null 2>&1
The scheduler fires every minute and Laravel decides internally which tasks are due. All your scheduling logic lives in app/Console/Kernel.php (or the Schedule facade in Laravel 11+) — this cron entry just kicks the door open once a minute.
Performance Tips Before You Go Live

1. Enable OPcache. Add these settings to /etc/php/8.3/fpm/conf.d/10-opcache.ini:
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=1
opcache.save_comments=1
OPcache stores compiled PHP bytecode in memory, eliminating repeated file parsing on every request. On a 2 GB VPS, memory_consumption=256 is safe and leaves plenty of headroom.
2. Run php artisan optimize on every deploy. Not just once during setup — every time you push code. It caches config, events, routes, and views. If you’re changing .env values, run php artisan config:clear first, then optimize.
3. Use Redis for everything. The .env settings CACHE_DRIVER=redis, SESSION_DRIVER=redis, and QUEUE_CONNECTION=redis offload Laravel’s caching layer from the database. Your MySQL instance will thank you.
4. Tune Composer’s autoloader. The --optimize-autoloader flag on composer install generates a classmap instead of scanning directories on every request. Already covered in Step 5, but don’t skip it.
5. Consider Nginx FastCGI caching for high-traffic public pages. It caches PHP responses at the Nginx layer, bypassing PHP-FPM entirely for repeat requests. This is advanced territory — hold off until you’re actually seeing load.
For a deeper look at Vultr’s infrastructure and how it performs under real workloads, see our Vultr review.
Next Steps
If you followed this guide, you’ve got a production-grade Laravel setup on Vultr: Nginx serving traffic, PHP 8.3-FPM handling requests, MySQL storing data, Redis powering cache and queues, Supervisor keeping workers alive, Certbot managing SSL, and cron driving the scheduler. That’s the full stack, running on $12/month.
The next improvements worth thinking about:
- Zero-downtime deploys: Implement the atomic symlink approach or add Deployer to your workflow
- Laravel Horizon: Replace bare Supervisor workers with Horizon if you need a queue dashboard and dynamic scaling
- Vultr Backups: Already enabled from Step 1 — test a restore before you need one
- Monitoring: Add basic server monitoring (Vultr has built-in metrics; UptimeRobot handles uptime alerts for free)
New accounts get $300 in free cloud credits to get your server running — enough to run a $12/month VPS for over two years if you’re methodical about it.
Affiliate disclosure: This post contains affiliate links. If you purchase through our links, we may earn a commission at no extra cost to you.

