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
| Component | Minimum Version | Purpose |
|---|---|---|
| PHP | 8.2+ | Runtime engine for Laravel |
| Composer | 2.x | PHP dependency manager |
| Nginx | 1.18+ | Web server and reverse proxy |
| MySQL or PostgreSQL | 8.0+ / 14+ | Application database |
| Git | 2.x | Code deployment from repository |
| Certbot | Latest | Free 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 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.
$ 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)
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.
$ 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.
$ 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;
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 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.
Create the Web Directory
$ sudo mkdir -p /var/www/myapp
$ sudo chown -R $USER:$USER /var/www/myapp
Clone Your Repository
$ git clone https://github.com/youruser/yourapp.git .
Cloning into '.'...
Receiving objects: 100% (2456/2456), done.
Install Dependencies and Configure
$ 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_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
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.
Run Laravel Setup Commands
$ 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.
$ 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
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.
Add the following configuration:
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;
}
}
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.
$ 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.
# 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
| Setting | Description | Guideline |
|---|---|---|
pm.max_children | Maximum number of child processes | Total RAM / avg PHP memory per process |
pm.start_servers | Children created on startup | ~25% of max_children |
pm.min_spare_servers | Minimum idle children | ~15% of max_children |
pm.max_spare_servers | Maximum idle children | ~50% of max_children |
pm.max_requests | Requests before child restarts | 500 (prevents memory leaks) |
$ 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.
$ 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.
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 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:
$ 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.comredirects fromhttp://tohttps:// - 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
| Error | Cause | Fix |
|---|---|---|
| 403 Forbidden | Wrong permissions on public/ | chmod 755 public/ |
| 500 Internal Server Error | Missing .env or APP_KEY | cp .env.example .env && php artisan key:generate |
| 502 Bad Gateway | PHP-FPM not running | systemctl restart php8.3-fpm |
| Permission denied on storage/ | Wrong ownership | chown -R www-data:www-data storage/ |
| Class not found | Autoloader not optimized | composer dump-autoload -o |
| SQLSTATE connection refused | Wrong DB credentials in .env | Verify DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD |
| Mixed content warnings | APP_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.
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.
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.