Laravel on a VPS: deployment guide for Indian developers
13 min read · 15-Jun-2024
villagehosting.in team
15 June 2024
Laravel is the dominant PHP framework in India. Deploying it on a VPS gives you complete control over PHP version, queue workers, scheduler, and Redis — things you can't configure on shared hosting. Here is the full configuration from PHP-FPM to queue workers.
Never run php artisan serve in production
php artisan serve is Laravel's development server — single-threaded, not designed for concurrent connections, and exposes full error details. Production deployments require PHP-FPM with NGINX or Apache. This guide covers the correct PHP-FPM setup.
Prerequisites
- Ubuntu 22.04 VPS with SSH access (root or sudo user)
- A domain pointed at your VPS IP
- Your Laravel project in a git repository
Step 1: Install the LEMP stack
sudo apt update && sudo apt upgrade -y
# PHP 8.3 and required extensions
sudo add-apt-repository ppa:ondrej/php -y
sudo apt install php8.3 php8.3-fpm php8.3-cli php8.3-mysql php8.3-pgsql \
php8.3-redis php8.3-gd php8.3-curl php8.3-xml php8.3-mbstring \
php8.3-zip php8.3-bcmath php8.3-intl php8.3-tokenizer -y
# NGINX
sudo apt install nginx -y
# MySQL (or PostgreSQL — change to postgresql postgresql-contrib for Postgres)
sudo apt install mysql-server -y
# Composer
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer
# Node.js (for asset compilation)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install nodejs -y
Step 2: Create a MySQL database
sudo mysql
CREATE DATABASE myapp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'myappuser'@'localhost' IDENTIFIED BY 'strong_password';
GRANT ALL PRIVILEGES ON myapp.* TO 'myappuser'@'localhost';
FLUSH PRIVILEGES;
EXIT;
Step 3: Deploy the application
# Create app user
sudo useradd -m -d /home/myapp -s /bin/bash myapp
sudo su - myapp
# Clone repository
git clone https://github.com/youruser/yourproject.git /home/myapp/yourproject
cd /home/myapp/yourproject
# Install dependencies
composer install --optimize-autoloader --no-dev
npm install && npm run build
# Set permissions
chmod -R 755 /home/myapp/yourproject
chmod -R 775 storage bootstrap/cache
Step 4: Configure .env
cp .env.example .env
nano .env
Key production settings:
APP_NAME=YourApp
APP_ENV=production
APP_DEBUG=false
APP_URL=https://yourdomain.in
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=myapp
DB_USERNAME=myappuser
DB_PASSWORD=strong_password
CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
MAIL_MAILER=smtp
Generate application key:
php artisan key:generate
Step 5: Run migrations and optimize
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan storage:link
config:cache, route:cache, and view:cache are essential for production — they pre-compile configuration and routes, cutting each request's bootstrap time significantly.
Step 6: Configure PHP-FPM
Edit the PHP-FPM pool for your app:
sudo nano /etc/php/8.3/fpm/pool.d/myapp.conf
[myapp]
user = myapp
group = myapp
listen = /run/php/php8.3-myapp.sock
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 20
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 10
pm.max_requests = 500
php_admin_value[memory_limit] = 256M
php_admin_value[upload_max_filesize] = 50M
php_admin_value[post_max_size] = 50M
sudo systemctl restart php8.3-fpm
Step 7: Configure NGINX
sudo nano /etc/nginx/sites-available/myapp
server {
listen 80;
server_name yourdomain.in www.yourdomain.in;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name yourdomain.in www.yourdomain.in;
ssl_certificate /etc/letsencrypt/live/yourdomain.in/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.in/privkey.pem;
root /home/myapp/yourproject/public;
index index.php;
client_max_body_size 50M;
# Serve existing files directly, route everything else to index.php
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# Cache static assets
location ~* \.(css|js|jpg|jpeg|png|gif|ico|webp|woff2|svg)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-myapp.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_read_timeout 120;
}
location ~ /\.ht {
deny all;
}
}
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Step 8: Install SSL
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d yourdomain.in -d www.yourdomain.in
Step 9: Queue workers with Supervisor
Laravel queues need a long-running process. Supervisor keeps it running:
sudo apt install supervisor -y
sudo nano /etc/supervisor/conf.d/myapp-worker.conf
[program:myapp-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /home/myapp/yourproject/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=myapp
numprocs=2
redirect_stderr=true
stdout_logfile=/home/myapp/logs/worker.log
stopwaitsecs=3600
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start myapp-worker:*
Step 10: Cron for Laravel scheduler
sudo crontab -u myapp -e
Add:
* * * * * cd /home/myapp/yourproject && php artisan schedule:run >> /dev/null 2>&1
This runs every minute, and Laravel's scheduler handles the actual frequency of each task.
Step 11: Redis for cache and sessions
sudo apt install redis-server -y
sudo systemctl enable redis-server
Verify:
redis-cli ping
# PONG
With CACHE_DRIVER=redis and SESSION_DRIVER=redis in .env, Laravel stores sessions and cache in Redis — much faster than file-based storage.
Zero-downtime deployment
For updates without dropping requests:
# On your deployment server or CI/CD
cd /home/myapp/yourproject
git pull origin main
composer install --optimize-autoloader --no-dev
npm run build
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan migrate --force
# Reload PHP-FPM gracefully (no downtime)
sudo kill -USR2 $(cat /run/php/php8.3-fpm.pid)
# Restart queue workers after deploy
sudo supervisorctl restart myapp-worker:*
For true zero-downtime with atomic deploys (symlink-based), use Laravel Forge or Deployer PHP.
File permissions
Laravel needs to write to storage/ and bootstrap/cache/. The web server user (www-data) must have write access:
sudo chown -R myapp:www-data /home/myapp/yourproject/storage
sudo chown -R myapp:www-data /home/myapp/yourproject/bootstrap/cache
sudo chmod -R 775 /home/myapp/yourproject/storage
sudo chmod -R 775 /home/myapp/yourproject/bootstrap/cache
A properly deployed Laravel application on a VPS handles hundreds of concurrent users comfortably and gives you full control over PHP version, queue concurrency, and caching strategy.