Tutorial

Deploy a Laravel Application on a VPS: Nginx, PHP-FPM, and SSL

April 01, 2026

Back to Blog

Laravel is the most popular PHP framework in the world, powering everything from simple blogs to complex enterprise applications. While local development with php artisan serve is straightforward, deploying Laravel to a production VPS requires a proper stack: Nginx as the web server, PHP-FPM for process management, a database, and SSL encryption. Getting each piece configured correctly is essential for security and performance.

This guide walks you through deploying a Laravel application on a fresh Ubuntu VPS, step by step, from installing the prerequisites to serving your app over HTTPS with automated SSL certificates.

Prerequisites Overview

Ubuntu 22.04/24.04
PHP 8.2+
Composer
Nginx
MySQL/PostgreSQL
SSL
ComponentMinimum VersionPurpose
PHP8.2+Runtime engine for Laravel
Composer2.xPHP dependency manager
Nginx1.18+Web server and reverse proxy
MySQL or PostgreSQL8.0+ / 14+Application database
Git2.xCode deployment from repository
CertbotLatestFree SSL from Let's Encrypt

Step 1: System Update and Essential Packages

Start with a fully updated system and install the essential build tools.

$ sudo apt update && sudo apt upgrade -y
$ sudo apt install -y software-properties-common curl git unzip

Step 2: Install PHP and Required Extensions

Laravel 11 and 12 require PHP 8.2 or higher. Ubuntu's default repositories may not have the latest PHP version, so we use the Ondrej PPA which maintains up-to-date PHP packages.

# Add the PHP repository
$ sudo add-apt-repository ppa:ondrej/php -y
$ sudo apt update

# Install PHP 8.3 with required extensions
$ sudo apt install -y php8.3-fpm php8.3-cli php8.3-common \
php8.3-mysql php8.3-pgsql php8.3-mbstring php8.3-xml \
php8.3-bcmath php8.3-curl php8.3-zip php8.3-gd \
php8.3-intl php8.3-redis php8.3-tokenizer

# Verify installation
$ php -v
PHP 8.3.15 (cli) (built: Jan 10 2026 12:34:56)
Which PHP Extensions Does Laravel Need?
The essential extensions are: mbstring (string handling), xml (XML parsing), bcmath (precise math), curl (HTTP client), zip (archive handling), and your database driver (mysql or pgsql). Additional extensions like gd (image processing), intl (internationalization), and redis (cache/session) are highly recommended for production.

Step 3: Install Composer

Composer is PHP's dependency manager and is required to install Laravel's packages.

# Download and install Composer globally
$ curl -sS https://getcomposer.org/installer | php
$ sudo mv composer.phar /usr/local/bin/composer

# Verify installation
$ composer --version
Composer version 2.8.5 2026-01-15 16:23:10

Step 4: Install and Configure the Database

Most Laravel applications use MySQL or PostgreSQL. We will cover MySQL here, but the process is similar for PostgreSQL.

# Install MySQL Server
$ sudo apt install -y mysql-server

# Secure the installation
$ sudo mysql_secure_installation

# Create a database and user for your Laravel app
$ sudo mysql -u root
mysql> CREATE DATABASE laravel_app CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
mysql> CREATE USER 'laravel_user'@'localhost' IDENTIFIED BY 'StrongPassword123!';
mysql> GRANT ALL PRIVILEGES ON laravel_app.* TO 'laravel_user'@'localhost';
mysql> FLUSH PRIVILEGES;
mysql> EXIT;
Database Security
Never use the root MySQL user for your application. Always create a dedicated database user with privileges limited to only the application database. Use a strong, unique password and store it in your .env file — never hardcode credentials in your application code.

Step 5: Install Nginx

$ sudo apt install -y nginx
$ sudo systemctl enable nginx
$ sudo systemctl start nginx

# Verify Nginx is running
$ sudo systemctl status nginx
● nginx.service - A high performance web server
Loaded: loaded (/lib/systemd/system/nginx.service; enabled)
Active: active (running)

Step 6: Deploy Your Laravel Application

Now let us get your Laravel code onto the server. We will use Git for deployment, which is the recommended approach for production servers.

1

Create the Web Directory

# Create the application directory
$ sudo mkdir -p /var/www/myapp
$ sudo chown -R $USER:$USER /var/www/myapp
2

Clone Your Repository

$ cd /var/www/myapp
$ git clone https://github.com/youruser/yourapp.git .
Cloning into '.'...
Receiving objects: 100% (2456/2456), done.
3

Install Dependencies and Configure

# Install PHP dependencies (no dev packages for production)
$ composer install --no-dev --optimize-autoloader
Installing dependencies from lock file
Package operations: 87 installs, 0 updates, 0 removals

# Create the environment file
$ cp .env.example .env
$ nano .env

Update your .env file with production settings:

APP_NAME="My Application"
APP_ENV=production
APP_DEBUG=false
APP_URL=https://example.com

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_app
DB_USERNAME=laravel_user
DB_PASSWORD=StrongPassword123!

CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_CONNECTION=database
Critical Production Settings
APP_ENV must be production and APP_DEBUG must be false. Leaving debug mode on in production exposes sensitive information including database credentials, environment variables, and full stack traces to anyone who triggers an error.
4

Run Laravel Setup Commands

# Generate application key
$ php artisan key:generate
Application key set successfully.

# Run database migrations
$ php artisan migrate --force
Migration table created successfully.
Migrating: 2024_01_01_000000_create_users_table
Migrated: 2024_01_01_000000_create_users_table (45.23ms)

# Optimize for production
$ php artisan config:cache
$ php artisan route:cache
$ php artisan view:cache
Blade templates cached successfully.

Step 7: Set File Permissions

File permissions are critical for both security and functionality. Laravel needs to write to the storage and bootstrap/cache directories.

# Set ownership to the web server user
$ sudo chown -R www-data:www-data /var/www/myapp

# Set directory permissions
$ sudo find /var/www/myapp -type d -exec chmod 755 {} \;

# Set file permissions
$ sudo find /var/www/myapp -type f -exec chmod 644 {} \;

# Make storage and cache writable
$ sudo chmod -R 775 /var/www/myapp/storage
$ sudo chmod -R 775 /var/www/myapp/bootstrap/cache
Why www-data?
PHP-FPM runs as the www-data user by default. When Nginx passes a PHP request to PHP-FPM, the PHP process needs to read your application files and write to storage directories. Setting ownership to www-data ensures PHP-FPM has the necessary access. On multi-tenant servers, each application should run under its own user for isolation.

Step 8: Configure Nginx Server Block

The Nginx configuration is where most deployment issues occur. Laravel requires all requests to be routed through index.php in the public directory.

$ sudo nano /etc/nginx/sites-available/myapp

Add the following configuration:

server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/myapp/public;

add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header X-XSS-Protection "1; mode=block";

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 ~ \.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;
}
}
Critical: Root Must Point to /public
The Nginx root directive must point to Laravel's public directory, NOT the application root. Setting it to /var/www/myapp instead of /var/www/myapp/public would expose your .env file, vendor directory, and all application source code to the internet. This is a severe security vulnerability.
# Enable the site
$ sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/

# Remove default site (optional)
$ sudo rm /etc/nginx/sites-enabled/default

# Test configuration
$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

# Reload Nginx
$ sudo systemctl reload nginx

Step 9: Configure PHP-FPM

PHP-FPM (FastCGI Process Manager) handles PHP request processing. The default configuration works for most applications, but you should tune it for production.

$ sudo nano /etc/php/8.3/fpm/pool.d/www.conf

# Key settings to review:
user = www-data
group = www-data
listen = /var/run/php/php8.3-fpm.sock
listen.owner = www-data
listen.group = www-data

# For a server with 4GB RAM, reasonable settings:
pm = dynamic
pm.max_children = 20
pm.start_servers = 5
pm.min_spare_servers = 3
pm.max_spare_servers = 10
pm.max_requests = 500
SettingDescriptionGuideline
pm.max_childrenMaximum number of child processesTotal RAM / avg PHP memory per process
pm.start_serversChildren created on startup~25% of max_children
pm.min_spare_serversMinimum idle children~15% of max_children
pm.max_spare_serversMaximum idle children~50% of max_children
pm.max_requestsRequests before child restarts500 (prevents memory leaks)
# Restart PHP-FPM
$ sudo systemctl restart php8.3-fpm

Step 10: SSL Certificate with Let's Encrypt

Every production website needs HTTPS. Certbot makes obtaining and renewing free SSL certificates from Let's Encrypt completely automated.

# Install Certbot
$ sudo apt install -y certbot python3-certbot-nginx

# Obtain and install SSL certificate
$ sudo certbot --nginx -d example.com -d www.example.com
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Requesting a certificate for example.com and www.example.com
Successfully received certificate.
Deploying certificate to VirtualHost /etc/nginx/sites-enabled/myapp
Congratulations! You have successfully enabled HTTPS

# Verify auto-renewal is configured
$ sudo certbot renew --dry-run
Congratulations, all simulated renewals succeeded

Certbot automatically modifies your Nginx configuration to include SSL settings and adds a redirect from HTTP to HTTPS.

Step 11: Set Up Queue Worker (Optional but Recommended)

If your Laravel application uses queued jobs (email sending, image processing, notifications), you need a queue worker running continuously. Systemd is the best way to manage this.

$ sudo nano /etc/systemd/system/laravel-queue.service
[Unit]
Description=Laravel Queue Worker
After=network.target

[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/myapp
ExecStart=/usr/bin/php artisan queue:work --sleep=3 --tries=3 --max-time=3600
Restart=always
RestartSec=5
StandardOutput=append:/var/log/laravel-queue.log
StandardError=append:/var/log/laravel-queue.log

[Install]
WantedBy=multi-user.target
$ sudo systemctl daemon-reload
$ sudo systemctl enable laravel-queue
$ sudo systemctl start laravel-queue

Step 12: Laravel Task Scheduler

Laravel's task scheduler needs a single cron entry that runs every minute:

# Open the crontab for www-data
$ sudo crontab -u www-data -e

# Add this line:
* * * * * cd /var/www/myapp && php artisan schedule:run >> /dev/null 2>&1

Deployment Verification Checklist

After completing all steps, verify that everything is working correctly:

  • Visit https://example.com — your Laravel app should load
  • Check https://example.com redirects from http:// to https://
  • Verify no errors in /var/www/myapp/storage/logs/laravel.log
  • Confirm Nginx error log is clean: tail /var/log/nginx/error.log
  • Test PHP-FPM is running: systemctl status php8.3-fpm
  • Verify database connection: php artisan db:show
  • Check queue worker: systemctl status laravel-queue
  • Test SSL certificate: visit SSL Labs

Common Deployment Errors and Fixes

ErrorCauseFix
403 ForbiddenWrong permissions on public/chmod 755 public/
500 Internal Server ErrorMissing .env or APP_KEYcp .env.example .env && php artisan key:generate
502 Bad GatewayPHP-FPM not runningsystemctl restart php8.3-fpm
Permission denied on storage/Wrong ownershipchown -R www-data:www-data storage/
Class not foundAutoloader not optimizedcomposer dump-autoload -o
SQLSTATE connection refusedWrong DB credentials in .envVerify DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD
Mixed content warningsAPP_URL not set to https://Update APP_URL in .env, clear config cache

Simplified Laravel Deployment with Panelica

The manual process described above involves configuring multiple services, managing file permissions, and writing Nginx configurations by hand. With a server management panel like Panelica, much of this complexity is automated.

1-Click
Create domain with automatic Nginx vhost generation
Auto SSL
Let's Encrypt certificates issued and renewed automatically

With Panelica, deploying Laravel becomes significantly simpler: create a domain in the panel, select your PHP version (8.1 through 8.4), create a MySQL or PostgreSQL database through the web interface, and Panelica handles the Nginx server block, PHP-FPM pool configuration, SSL certificate, and file permissions automatically. Each user gets their own isolated PHP-FPM pool for security, and the per-user file permissions are managed through Panelica's 5-layer isolation architecture.

Key Takeaway
Deploying Laravel to production is a multi-step process, but each step is logical and well-documented. The key areas where most deployments fail are file permissions, Nginx root path configuration, and missing environment variables. Whether you configure everything manually or use a panel to automate the process, understanding each component gives you the knowledge to debug any issue that arises.
Share: