
Laravel is the most popular PHP framework in Southeast Asia. To deploy it correctly on a VPS you need to set up the full stack: Nginx + PHP-FPM + MySQL + Composer, configure file permissions, .env, and a Queue Worker. This guide takes you from a blank Ubuntu 22.04 VPS to a running production app — step by step.
Recommended VPS spec: 2 GB RAM or more, 30 GB+ SSD, Ubuntu 22.04 LTS for a mid-size Laravel application.
Understanding the Laravel Stack on VPS
Before diving into installation commands, it helps to understand how the different components work together. Every user request passes through multiple layers on the server:
- Nginx — Accepts HTTP/HTTPS connections from the internet and acts as a reverse proxy forwarding PHP requests to PHP-FPM.
- PHP-FPM (FastCGI Process Manager) — Processes PHP code and runs the Laravel application logic.
- MySQL — Stores all application data: users, posts, orders, settings.
- Laravel Application — Receives requests via
public/index.php, routes them through Controllers and Models, then returns a response. - Queue Worker (optional) — Handles heavy background tasks such as sending emails, resizing images, or exporting reports without blocking the request cycle.
Understanding this flow makes debugging far easier. If the page fails to load, check Nginx logs first. If you see a PHP error, inspect the PHP-FPM log. If data is not saving, look at MySQL errors.
Why VPS beats shared hosting for Laravel: Shared hosting restricts PHP extensions, blocks Artisan commands via terminal, and prevents running persistent background processes like Queue Workers. A VPS gives you full root access, the ability to install any extension, customise php.ini, and run workers 24 hours a day.
Harden the VPS Before Deploying Laravel
Before installing any software, secure the server. A fresh VPS connected to the internet will face automated port scans and brute-force attempts within minutes.
Configure UFW Firewall
# Allow only essential ports
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw statusCreate a Non-Root Deploy User
sudo adduser deployer
sudo usermod -aG sudo deployer
# Copy your SSH key to the new user
sudo rsync --archive --chown=deployer:deployer ~/.ssh /home/deployerDisable Password Login Over SSH
sudo nano /etc/ssh/sshd_config
# Set these two lines:
PasswordAuthentication no
PermitRootLogin no
sudo systemctl restart sshdAlways verify that key-based login works before disabling password authentication to avoid locking yourself out.
Step 1 — Update the System and Install Nginx
# Update package lists sudo apt update && sudo apt upgrade -y # Install Nginx sudo apt install nginx -y sudo systemctl enable nginx sudo systemctl start nginx
Step 2 — Install PHP 8.2 + Laravel Extensions
# Add PHP PPA (Ubuntu 22.04) sudo add-apt-repository ppa:ondrej/php -y sudo apt update # Install PHP 8.2 and all extensions Laravel requires sudo apt install php8.2 php8.2-fpm php8.2-mysql php8.2-xml \ php8.2-curl php8.2-mbstring php8.2-zip php8.2-bcmath \ php8.2-tokenizer php8.2-gd php8.2-intl -y # Verify the version php -v
Step 3 — Install MySQL
sudo apt install mysql-server -y
sudo mysql_secure_installation
# Create a database and user for Laravel
sudo mysql -u root -p
CREATE DATABASE laravel_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'laravel_user'@'localhost' IDENTIFIED BY 'StrongPassword123!';
GRANT ALL PRIVILEGES ON laravel_db.* TO 'laravel_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;Step 4 — Install Composer
curl -sS https://getcomposer.org/installer | php sudo mv composer.phar /usr/local/bin/composer composer --version
Step 5 — Upload Your Laravel Project
Transfer your project to the VPS using Git or SCP:
# Clone from Git (recommended) cd /var/www sudo git clone https://github.com/yourusername/your-laravel-app.git myapp sudo chown -R www-data:www-data /var/www/myapp # Install Composer dependencies (production mode) cd /var/www/myapp sudo -u www-data composer install --no-dev --optimize-autoloader
Step 6 — Configure .env and Application Key
cp .env.example .env nano .env # Edit the following values: APP_ENV=production APP_DEBUG=false APP_URL=https://yourdomain.com DB_DATABASE=laravel_db DB_USERNAME=laravel_user DB_PASSWORD=StrongPassword123! # Generate application key php artisan key:generate # Run database migrations php artisan migrate --force
Step 7 — Set Storage Permissions
sudo chown -R www-data:www-data /var/www/myapp/storage
sudo chown -R www-data:www-data /var/www/myapp/bootstrap/cache
sudo chmod -R 775 /var/www/myapp/storage
sudo chmod -R 775 /var/www/myapp/bootstrap/cache
# Create the public storage symlink
php artisan storage:linkStep 8 — Configure Nginx Virtual Host
sudo nano /etc/nginx/sites-available/myapp # Paste this configuration: server { listen 80; server_name yourdomain.com www.yourdomain.com; root /var/www/myapp/public; index index.php; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass unix:/run/php/php8.2-fpm.sock; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; include fastcgi_params; } location ~ /\.ht { deny all; } } # Enable the site sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx
Step 9 — Install SSL with Certbot
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
# Certbot will configure HTTPS and auto-renewal automaticallyStep 10 — Configure Queue Worker (if using Queues)
# Create a Supervisor config for the queue worker sudo apt install supervisor -y sudo nano /etc/supervisor/conf.d/laravel-worker.conf [program:laravel-worker] process_name=%(program_name)s_%(process_num)02d command=php /var/www/myapp/artisan queue:work --sleep=3 --tries=3 autostart=true autorestart=true user=www-data numprocs=2 redirect_stderr=true stdout_logfile=/var/www/myapp/storage/logs/worker.log # Reload Supervisor sudo supervisorctl reread sudo supervisorctl update sudo supervisorctl start laravel-worker:*
Step 11 — Optimize for Production
# Cache config, routes, and views php artisan config:cache php artisan route:cache php artisan view:cache # Optimize Composer autoloader composer dump-autoload --optimize
Set Up Laravel Scheduler with Cron
Laravel's built-in Task Scheduler lets you manage all scheduled tasks in PHP code rather than creating many individual Cron jobs. Only one Cron entry is needed on the server:
# Edit the crontab for the www-data user sudo crontab -u www-data -e # Add this single line to run the scheduler every minute * * * * * cd /var/www/myapp && php artisan schedule:run >> /dev/null 2>&1
With this in place, every task defined in your application's scheduler (daily emails, weekly cleanups, hourly data syncs) will fire automatically at the right time. Example tasks:
# routes/console.php (Laravel 10+)
use Illuminate\Support\Facades\Schedule;
Schedule::command('emails:send-digest')->dailyAt('08:00');
Schedule::command('logs:clean')->weekly();
Schedule::command('cache:prune-stale-tags')->hourly();Monitoring and Debugging Your Laravel Deployment
After going live, monitoring ensures the application stays healthy. Laravel's logging system writes all errors to storage/logs/laravel.log by default.
Watch Logs in Real Time
tail -f /var/www/myapp/storage/logs/laravel.log
Common Problems and Fixes
| Symptom | Likely Cause | Fix |
|---|---|---|
| 500 Internal Server Error | Wrong permissions or missing .env values | Check storage/ permissions and .env completeness |
| 404 Not Found | Nginx root or try_files misconfigured | Verify the root path points to public/ |
| 502 Bad Gateway | PHP-FPM not running | sudo systemctl restart php8.2-fpm |
| Jobs stuck in queue | Supervisor not running | sudo supervisorctl start laravel-worker:* |
Managing Multiple Environments on a VPS
Production Laravel applications typically span at least three environments: Local (developer machines), Staging (QA testing), and Production (the live server). Keeping environments separate prevents test data from contaminating real data and lets your team verify changes safely before they reach users.
Environment File Structure
# How to structure environment files
.env # Active config (never commit — listed in .gitignore)
.env.example # Template for onboarding developers (commit this)
.env.staging # Staging values (server-side only, not committed)
.env.production # Production values (server-side only, not committed)
A Well-Structured Production .env
APP_NAME="MyApp"
APP_ENV=production
APP_DEBUG=false
APP_URL=https://yourdomain.com
LOG_CHANNEL=daily
LOG_LEVEL=error
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_prod
DB_USERNAME=laravel_prod_user
DB_PASSWORD=VeryStrongProductionPassword!
CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
Golden rule: Every .env file must be listed in .gitignore. Only .env.example (with no real secrets) should ever be committed. A leaked production .env can expose database credentials and encryption keys instantly.
Laravel Horizon: Queue Dashboard on VPS
Laravel Horizon is the official queue monitoring dashboard built by the Laravel team. It gives you real-time visibility into job throughput, processing times, failure rates, and worker balance — making queue debugging far faster than reading raw log files.
Install Laravel Horizon
composer require laravel/horizon
php artisan horizon:install
Configure Workers in config/horizon.php
'environments' => [
'production' => [
'supervisor-1' => [
'maxProcesses' => 10,
'balanceMaxShift' => 1,
'balanceCooldown' => 3,
],
],
'local' => [
'supervisor-1' => [
'maxProcesses' => 3,
],
],
],
Supervisor Config for Horizon
sudo nano /etc/supervisor/conf.d/laravel-horizon.conf
[program:laravel-horizon]
process_name=%(program_name)s
command=php /var/www/myapp/artisan horizon
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/www/myapp/storage/logs/horizon.log
stopwaitsecs=3600
sudo supervisorctl reread && sudo supervisorctl update
sudo supervisorctl start laravel-horizon
Restrict the Horizon Dashboard
// app/Providers/HorizonServiceProvider.php
protected function gate(): void
{
Gate::define('viewHorizon', function ($user) {
return in_array($user->email, [
'[email protected]',
]);
});
}
After setup, access the Horizon dashboard at https://yourdomain.com/horizon. You can see pending, processing, and failed jobs — and retry failures directly from the UI without SSH access.
Rate Limiting and API Abuse Protection
Production applications need protection against bots and clients that hammer the API. Laravel ships with built-in rate limiting via the throttle middleware, backed by Redis for distributed counting across multiple queue workers or processes.
Define Rate Limits in RouteServiceProvider
// In App\Providers\RouteServiceProvider
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
RateLimiter::for('login', function (Request $request) {
return [
Limit::perMinute(5)->by($request->input('email')),
Limit::perMinute(20)->by($request->ip()),
];
});
Add Nginx-Level Rate Limiting as First Defence
# In /etc/nginx/nginx.conf, http block
limit_req_zone $binary_remote_addr zone=api:10m rate=60r/m;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
# In your Laravel server block
location /api/ {
limit_req zone=api burst=20 nodelay;
limit_req_status 429;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location /login {
limit_req zone=login burst=3 nodelay;
limit_req_status 429;
try_files $uri $uri/ /index.php?$query_string;
}
| Layer | Tool | Benefit |
|---|---|---|
| Network | UFW Firewall | Block ports before traffic reaches Nginx |
| Web Server | Nginx limit_req | Rate-limit before PHP is invoked |
| Application | Laravel throttle | Per-user limits with Redis tracking |
| Process | Fail2ban | Auto-ban IPs that trigger too many 429s |
Layering protection at Nginx and Laravel simultaneously (Defence in Depth) means an abusive client is rejected before PHP-FPM is even invoked, saving CPU and RAM during traffic spikes.
Zero-Downtime Code Updates
When pushing new code to production, follow this sequence to avoid errors reaching users during the update window:
# 1. Enable maintenance mode php artisan down --retry=60 # 2. Pull latest code git pull origin main # 3. Install or update dependencies composer install --no-dev --optimize-autoloader # 4. Run pending migrations php artisan migrate --force # 5. Clear and rebuild caches php artisan optimize:clear php artisan optimize # 6. Restart queue workers sudo supervisorctl restart laravel-worker:* # 7. Take the site back online php artisan up
Using php artisan down before running migrations prevents users from hitting database errors during structural changes, such as adding or removing columns.
Pre-launch Checklist: APP_DEBUG=false ✓ → APP_ENV=production ✓ → SSL Active ✓ → Storage Permissions ✓ → Queue Worker Running ✓ → Config Cache ✓ → Cron job set ✓ — Ready for production.
⚠️ Security reminder: Never commit your .env file to Git. Enable UFW to close unnecessary ports and consider changing the default SSH port to reduce brute-force exposure.
Laravel-Ready VPS Starting at an Affordable Price
AsiaGB VPS ships with Ubuntu 22.04 LTS, full root access, SSD, and a Thai IP address — everything you need to deploy Laravel in minutes.
View VPS Plans