By Erika Heidi and Vinayak Baranwal
Laravel is an open-source PHP framework that provides a comprehensive set of tools and resources to build modern PHP applications. With a complete ecosystem leveraging its built-in features, Laravel’s popularity has grown rapidly, with many developers adopting it as their framework of choice for streamlined development processes.
In production environments worldwide, Laravel on the LEMP stack processes millions of requests daily with response times averaging 50-200ms for dynamic content. The combination delivers measurably superior performance: Nginx handles 10,000+ concurrent connections using just 2.5MB RAM per connection, while Laravel’s optimized caching can reduce database queries by 80-95% in typical applications.
This comprehensive guide walks you through installing and configuring a Laravel application on Ubuntu using the LEMP stack (Linux, Nginx, MySQL, PHP). You’ll learn to set up a production-ready Laravel environment with proper security configurations, SSL certificates, and performance optimizations that mirror real-world deployments at scale.
Ubuntu Compatibility: This tutorial works across all current Ubuntu LTS and stable releases. Commands are version-agnostic. Install PHP 8.2+ as shown in Step 1 regardless of your Ubuntu version’s default PHP. All components (Nginx, MySQL, Laravel) work consistently across Ubuntu distributions.
Performance Optimization: The tutorial addresses multiple ways to enhance the speed and responsiveness of your Laravel application. You’ll configure Nginx caching to reduce server load, tune PHP-FPM settings for improved concurrency and resource management, and apply Laravel-specific optimization techniques such as route and config caching, ensuring your deployment can handle growth and deliver fast user experiences.
Troubleshooting Guide: Anticipating potential issues, the guide provides a comprehensive section on resolving common errors that may arise during installation or operation. You’ll find practical advice for debugging configuration mistakes, permission problems, connectivity errors, as well as strategies to read and interpret logs, enabling you to maintain a stable and reliable Laravel environment.
Security Best Practices: Security measures are integrated at every level of the stack. The guide emphasizes database security with access controls, configures appropriate file permissions to restrict unauthorized access, and implements HTTPS using SSL certificates. Through these steps, you’ll ensure your Laravel application is protected against common vulnerabilities, keeping both your server and user data safe.
To complete this guide, you’ll need an Ubuntu server with the following components already configured:
Sizing Guide: For production Laravel applications, allocate resources based on traffic:
Before installing Laravel, set up the base LEMP components:
Server Setup: Create a sudo user and configure the firewall using Initial Server Setup with Ubuntu.
LEMP Stack: Install Nginx, MySQL, and PHP-FPM following How to Install Linux, Nginx, MySQL, PHP (LEMP stack) on Ubuntu. For quick deployment, use the LEMP 1-Click Install on DigitalOcean. CentOS users, see Installing LEMP on CentOS 8.
Composer: Install the PHP dependency manager using How to Install Composer on Ubuntu.
After completing these prerequisites, return here to install Laravel-specific PHP extensions and configure your application.
Why choose Nginx instead of Apache for Laravel? Nginx offers measurable benefits: far higher concurrency (10,000+ simultaneous connections vs roughly 1,000 with Apache prefork), significantly lower memory usage (about 2.5MB vs 6–8MB per connection), and faster static asset delivery (around 15,000 req/s vs 5,000 req/s on comparable hardware). For Laravel applications with many CSS, JS, and image assets, its event-driven model scales efficiently without proportional memory growth, making it the common production choice.
Note: These performance metrics are typical for well-tuned systems under specific workloads and hardware configurations. Actual results may vary depending on server hardware, software versions, configuration tuning, and the nature of your application’s workload.
Laravel requires several PHP extensions to function properly. These extensions provide essential functionality for character encoding, XML processing, mathematical operations, and database connectivity.
PHP Version Check: Laravel 11 requires PHP 8.2 or later. Check your current version with php -v. If you see PHP 7.4 or 8.0/8.1, follow the upgrade steps below. This tutorial uses PHP 8.2 commands—if you install PHP 8.3, replace php8.2 with php8.3 in all commands.
For optimal Laravel 11 compatibility and performance, install PHP 8.2 or later:
- sudo add-apt-repository ppa:ondrej/php
- sudo apt update
- sudo apt install php8.2 php8.2-fpm php8.2-mysql php8.2-mbstring php8.2-xml php8.2-bcmath php8.2-curl php8.2-zip php8.2-gd
PHP Upgrade Impact: The Ondřej Surý PPA provides the latest stable PHP versions for Ubuntu. After upgrading, update your Nginx configuration to reference the new PHP-FPM socket path (e.g., /var/run/php/php8.2-fpm.sock). Performance gain: PHP 8.2 delivers 15-20% faster execution vs PHP 8.0 and 40-50% better performance than PHP 7.4.
Update your package manager and install the required extensions:
- sudo apt update
Install the essential PHP extensions:
- sudo apt install php-mbstring php-xml php-bcmath php-curl php-zip php-gd
If you skip these extensions, Laravel will fail during installation or runtime:
mbstring extension is missingext-zip is missingVerify all extensions loaded correctly:
- php -m | grep -E "(mbstring|xml|bcmath|curl|zip|gd)"
You should see each extension name listed. If any are missing, repeat the apt install command.
Production Addition: Install php-redis for caching and php-memcached for session storage. These aren’t required but improve performance significantly: sudo apt install php-redis php-memcached.
With PHP extensions installed, you’re ready to set up the database.
We’ll create a sample travel list application to demonstrate Laravel’s database integration. This application will store travel destinations with visit status, showcasing Laravel’s Eloquent ORM and database connectivity.
For production environments, always follow these security principles:
Security Note: The caching_sha2_authentication method (MySQL 8.0 default) requires PHP 7.4+ with specific configurations. For compatibility with older PHP versions or simplified setup, we’ll use mysql_native_password authentication.
Connect to MySQL as the root user:
- sudo mysql
Create the application database:
- CREATE DATABASE travellist CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
Character Set: Using utf8mb4 ensures full Unicode support, including emoji and international characters, which is essential for modern web applications.
Create a dedicated database user with secure authentication:
- CREATE USER 'travellist_user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your_secure_password_here';
Security Improvement: The user travellist_user is restricted to connections from 'localhost' only. This is an intentional security enhancement compared to using '%', which would allow remote connections from any host. Remote access is now disabled by default. If you need to connect to MySQL from another server, you must explicitly configure remote access by creating the user with a different host (e.g., 'travellist_user'@'%') and updating MySQL’s bind-address and firewall settings.
Password Security: Replace your_secure_password_here with a strong password containing uppercase, lowercase, numbers, and special characters. Consider using a password manager to generate and store secure passwords.
Grant appropriate privileges to the user:
- GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER ON travellist.* TO 'travellist_user'@'localhost';
- FLUSH PRIVILEGES;
Principle of Least Privilege: We grant only the specific permissions needed for the application, not global privileges. This limits potential damage if the application is compromised.
Exit the MySQL console:
- exit
Test the database user permissions:
- mysql -u travellist_user -p
Connection Testing: The -p flag prompts for the password. This verifies that your database user can authenticate and access the database.
Verify database access:
- SHOW DATABASES;
Expected output:
+--------------------+
| Database |
+--------------------+
| information_schema |
| travellist |
+--------------------+
2 rows in set (0.01 sec)
Create the places table with proper indexing and constraints:
- USE travellist;
-
- CREATE TABLE places (
- id INT AUTO_INCREMENT PRIMARY KEY,
- name VARCHAR(255) NOT NULL,
- visited BOOLEAN DEFAULT FALSE,
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
- INDEX idx_visited (visited),
- INDEX idx_name (name)
- );
Database Design: We’ve added created_at and updated_at timestamps for audit trails, and indexes on frequently queried columns for better performance.
Insert sample travel destinations:
- INSERT INTO places (name, visited) VALUES
- ('Tokyo', FALSE),
- ('Budapest', TRUE),
- ('Nairobi', FALSE),
- ('Berlin', TRUE),
- ('Lisbon', TRUE),
- ('Denver', FALSE),
- ('Moscow', FALSE),
- ('Oslo', FALSE),
- ('Rio de Janeiro', TRUE),
- ('Cincinnati', FALSE),
- ('Helsinki', FALSE);
Verify the data:
- SELECT * FROM places ORDER BY visited DESC, name ASC;
Expected output:
+----+---------------+---------+---------------------+---------------------+
| id | name | visited | created_at | updated_at |
+----+---------------+---------+---------------------+---------------------+
| 2 | Budapest | 1 | 2024-01-15 10:30:00 | 2024-01-15 10:30:00 |
| 4 | Berlin | 1 | 2024-01-15 10:30:00 | 2024-01-15 10:30:00 |
| 5 | Lisbon | 1 | 2024-01-15 10:30:00 | 2024-01-15 10:30:00 |
| 9 | Rio de Janeiro| 1 | 2024-01-15 10:30:00 | 2024-01-15 10:30:00 |
| 1 | Tokyo | 0 | 2024-01-15 10:30:00 | 2024-01-15 10:30:00 |
| 3 | Nairobi | 0 | 2024-01-15 10:30:00 | 2024-01-15 10:30:00 |
| 6 | Denver | 0 | 2024-01-15 10:30:00 | 2024-01-15 10:30:00 |
| 7 | Moscow | 0 | 2024-01-15 10:30:00 | 2024-01-15 10:30:00 |
| 8 | Oslo | 0 | 2024-01-15 10:30:00 | 2024-01-15 10:30:00 |
| 10 | Cincinnati | 0 | 2024-01-15 10:30:00 | 2024-01-15 10:30:00 |
| 11 | Helsinki | 0 | 2024-01-15 10:30:00 | 2024-01-15 10:30:00 |
+----+---------------+---------+---------------------+---------------------+
11 rows in set (0.00 sec)
Exit MySQL:
- exit
Your database is now ready for Laravel integration. Next, we’ll create the Laravel application.
We’ll create a Laravel application using Composer, which manages PHP dependencies and provides the Laravel installer. This approach ensures you get the latest stable version with all required dependencies.
Laravel Version: This guide uses Laravel 11.x, the current LTS release requiring PHP 8.2+. Laravel 11 introduces per-second rate limiting, improved model casts, and streamlined directory structure. For production environments, always use LTS versions which receive bug fixes for 18 months and security fixes for 2 years.
Navigate to your home directory:
- cd ~
Create a new Laravel application:
- composer create-project laravel/laravel travellist
Composer Options: The --prefer-dist flag downloads stable releases instead of source code, which is faster and more reliable for production environments.
Expected output:
Creating a "laravel/laravel" project at "./travellist"
Installing laravel/laravel (v11.31.0)
- Installing laravel/laravel (v11.31.0): Downloading (100%)
Created project in travellist
> @php -r "file_exists('.env') || copy('.env.example', '.env');"
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 80 installs, 0 updates, 0 removals
- Installing symfony/polyfill-ctype (v1.28.0): Downloading (100%)
- Installing phpoption/phpoption (1.9.0): Downloading (100%)
- Installing vlucas/phpdotenv (v5.5.0): Downloading (100%)
- Installing symfony/css-selector (v6.3.0): Downloading (100%)
...
Navigate to the application directory and test Laravel’s command-line interface:
- cd travellist
- php artisan --version
You’ll see output like:
Laravel Framework 11.31.0
The exact minor version (11.x) will vary based on when you install, but ensure it starts with “11” to confirm you have Laravel 11.
Test the Artisan command-line tool:
- php artisan list
Artisan CLI: Laravel’s Artisan command-line interface provides over 50 built-in commands for common tasks like database migrations, route caching, and queue management.
This confirms Laravel is properly installed. Next, we’ll configure the application for your database and environment.
Laravel uses environment variables to manage configuration across different environments. The .env file contains sensitive information and environment-specific settings that override the default configuration files.
Security Warning: Never commit the .env file to version control. It contains database credentials, API keys, and other sensitive information. Laravel automatically excludes it from Git via .gitignore.
Laravel follows a hierarchical configuration system:
config/ directory files.env file (highest priority)Open the .env file for editing:
- nano .env
Open .env and update these database values to match Step 2:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=travellist
DB_USERNAME=travellist_user
DB_PASSWORD=your_secure_password_here
Replace your_secure_password_here with the password you set in Step 2.
Update these based on your environment:
APP_NAME="Travel List Application"
APP_ENV=local # Change to "production" when deploying
APP_DEBUG=true # MUST be false in production (exposes sensitive data)
APP_URL=http://your_domain_or_ip
LOG_LEVEL=debug # Use "error" in production to reduce log size
These work as-is for now:
SESSION_DRIVER=file # Upgrade to redis/memcached for multiple servers
CACHE_DRIVER=file # Upgrade to redis for better performance
QUEUE_CONNECTION=sync # Upgrade to redis/database for background jobs
Environment-Specific Settings:
APP_DEBUG=true, APP_ENV=localAPP_DEBUG=false, APP_ENV=productionAPP_DEBUG=true, APP_ENV=testingLaravel requires a unique application key for encryption. Generate it using Artisan:
- php artisan key:generate
Application Key: This key is used for encrypting cookies, sessions, and other sensitive data. Laravel automatically generates a secure key if none exists.
Verify your database configuration:
- php artisan tinker --execute="DB::connection()->getPdo();"
Expected output:
=> PDO {#1234}
Database Testing: This command tests the database connection without running migrations. If successful, you’ll see a PDO object returned.
For production environments, run these optimization commands:
- php artisan config:cache
- php artisan route:cache
- php artisan view:cache
Performance Optimization: These commands cache configuration, routes, and views to improve application performance in production environments.
Your Laravel application is now configured. Next, we’ll set up Nginx to serve your application.
Nginx requires proper configuration to serve Laravel applications efficiently. We’ll set up a production-ready configuration with security headers, performance optimizations, and proper file permissions.
Move the Laravel application to the standard web directory:
- sudo mv ~/travellist /var/www/travellist
Web Directory: /var/www is the standard location for web applications on Ubuntu. This provides better security isolation and follows Linux filesystem hierarchy standards.
Configure ownership and permissions for Laravel’s writable directories:
- sudo chown -R www-data:www-data /var/www/travellist
- sudo chmod -R 755 /var/www/travellist
- sudo chmod -R 775 /var/www/travellist/storage
- sudo chmod -R 775 /var/www/travellist/bootstrap/cache
Permission Strategy:
755 for directories (owner: read/write/execute, group/others: read/execute)775 for Laravel’s writable directories (storage, cache)www-data ownership ensures Nginx can read files and write to storageCreate a new virtual host configuration:
- sudo nano /etc/nginx/sites-available/travellist
Use this optimized configuration for Laravel:
server {
listen 80;
listen [::]:80;
server_name your_domain_or_ip;
root /var/www/travellist/public;
index index.php index.html index.htm;
# Security Headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob;" always;
# Gzip Compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private must-revalidate auth;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss;
# Handle Laravel Routes
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# PHP Processing
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_hide_header X-Powered-By;
}
# Static Files Caching
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Security: Deny access to hidden files
location ~ /\. {
deny all;
}
# Security: Deny access to sensitive files
location ~ /(\.env|\.git|composer\.(json|lock)|package\.json) {
deny all;
}
# Favicon and robots.txt
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
}
Configuration Features:
PHP-FPM Socket Path: The configuration uses php8.2-fpm.sock matching our PHP 8.2 installation. If you installed a different PHP version, update this path. To verify your socket: ls /var/run/php/ will show available sockets like php8.2-fpm.sock or php8.3-fpm.sock. The socket name always matches your installed PHP version.
Create a symbolic link to enable the site:
- sudo ln -s /etc/nginx/sites-available/travellist /etc/nginx/sites-enabled/
Site Management: The symbolic link allows Nginx to serve the site. To disable a site, remove the link from sites-enabled/ without deleting the configuration file.
Test the configuration for syntax errors:
- sudo nginx -t
Expected output:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Reload Nginx to apply changes:
- sudo systemctl reload nginx
Access your application in a browser:
http://your_domain_or_ip
Expected Result: You should see Laravel’s welcome page, confirming that Nginx is properly serving your Laravel application.
Your Laravel application is now running on Nginx. Next, we’ll customize the application to display data from your database.
Now we’ll create a functional Laravel application that displays travel destinations from your database. This demonstrates Laravel’s MVC architecture, database connectivity, and Blade templating.
Laravel follows the Model-View-Controller (MVC) pattern:
Edit the main route file to query your database:
- nano /var/www/travellist/routes/web.php
Replace the default route with this database-driven implementation:
<?php
use Illuminate\Support\Facades\DB;
Route::get('/', function () {
$visited = DB::select('SELECT * FROM places WHERE visited = ?', [1]);
$togo = DB::select('SELECT * FROM places WHERE visited = ?', [0]);
return view('travellist', [
'visited' => $visited,
'togo' => $togo
]);
});
Database Queries: We use parameterized queries to prevent SQL injection attacks. Laravel’s query builder automatically escapes user input.
Create a responsive Blade template for your travel list:
- nano /var/www/travellist/resources/views/travellist.blade.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Travel Bucket List</title>
<style>
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
.section { margin: 30px 0; padding: 20px; border-radius: 8px; }
.visited { background-color: #d4edda; border-left: 4px solid #28a745; }
.togo { background-color: #fff3cd; border-left: 4px solid #ffc107; }
h1 { color: #333; text-align: center; }
h2 { color: #666; margin-top: 0; }
ul { list-style: none; padding: 0; }
li { padding: 8px 0; border-bottom: 1px solid #eee; }
.count { font-weight: bold; color: #666; }
</style>
</head>
<body>
<h1>🌍 My Travel Bucket List</h1>
<div class="section visited">
<h2>✅ Places I've Visited ({{ count($visited) }})</h2>
@if(count($visited) > 0)
<ul>
@foreach($visited as $place)
<li>{{ $place->name }}</li>
@endforeach
</ul>
@else
<p>No places visited yet. Time to start traveling!</p>
@endif
</div>
<div class="section togo">
<h2>🎯 Places I Want to Visit ({{ count($togo) }})</h2>
@if(count($togo) > 0)
<ul>
@foreach($togo as $place)
<li>{{ $place->name }}</li>
@endforeach
</ul>
@else
<p>All destinations visited! Time to add more places to your bucket list.</p>
@endif
</div>
</body>
</html>
Blade Templating: Laravel’s Blade engine provides powerful templating features including conditionals, loops, and component inheritance. The @if, @foreach, and {{ }} syntax makes templates clean and readable.
Access your application in a browser:
http://your_domain_or_ip
Expected Result: You should see a styled travel list showing visited and unvisited destinations from your database, with counts and responsive design.
Test that your application is properly connected to the database:
- cd /var/www/travellist
- php artisan tinker --execute="echo 'Database connection: ' . (DB::connection()->getPdo() ? 'Success' : 'Failed');"
Database Verification: This command tests the database connection and confirms that Laravel can communicate with MySQL successfully.
Your Laravel application is now fully functional with database integration!
Production sites require HTTPS for security and SEO. Let’s Encrypt provides free SSL certificates.
Before running Certbot, verify:
dig +short your_domain.com - it must return your server’s IP addresssudo systemctl status nginxIf your domain doesn’t point to the server yet, you’ll get: Challenge failed for domain your_domain.com. Fix your DNS first.
Install Certbot and the Nginx plugin:
- sudo apt update
- sudo apt install certbot python3-certbot-nginx
Run Certbot with your domain:
- sudo certbot --nginx -d your_domain.com -d www.your_domain.com
Certbot will:
When prompted “Please choose whether or not to redirect HTTP traffic to HTTPS”, select 2 (Redirect) for automatic HTTPS enforcement.
Check your Nginx config to see the changes:
- sudo cat /etc/nginx/sites-available/travellist
Certbot added these lines:
listen 443 ssl; # SSL port
ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your_domain.com/privkey.pem;
Let’s Encrypt certificates expire after 90 days. Test the auto-renewal:
- sudo certbot renew --dry-run
If successful, you’ll see: Congratulations, all simulated renewals succeeded. The system will automatically renew certificates before expiration via a systemd timer.
Before deploying to production, run these caching commands. Each caches a different part of Laravel:
- cd /var/www/travellist
-
- # Cache configuration files (15-25ms faster per request)
- php artisan config:cache
-
- # Cache route definitions (10-15ms faster per request)
- php artisan route:cache
-
- # Pre-compile Blade templates (5-10ms faster per request)
- php artisan view:cache
-
- # Optimize autoloader (faster class loading)
- php artisan optimize
When to Run These: After every code deployment. If you change routes or config, clear the cache first: php artisan cache:clear.
Don’t Run on Development: Caching prevents .env changes from taking effect. Only cache on production.
Add these optimizations to your Nginx configuration:
# Add to server block
client_max_body_size 20M;
client_body_timeout 60s;
client_header_timeout 60s;
keepalive_timeout 65s;
send_timeout 60s;
# FastCGI optimizations
fastcgi_connect_timeout 60s;
fastcgi_send_timeout 60s;
fastcgi_read_timeout 60s;
fastcgi_buffer_size 128k;
fastcgi_buffers 4 256k;
fastcgi_busy_buffers_size 256k;
Optimize PHP-FPM for your server:
PHP-FPM Configuration Path: Use /etc/php/8.2/fpm/pool.d/www.conf for PHP 8.2. If you installed PHP 8.3, use /etc/php/8.3/fpm/pool.d/www.conf instead. The path pattern is always /etc/php/VERSION/fpm/pool.d/www.conf.
- sudo nano /etc/php/8.2/fpm/pool.d/www.conf
Each PHP-FPM worker uses ~30-50MB RAM. Calculate max workers:
max_children = (Total RAM - System RAM) / 50MB
For a 2GB server (1.5GB usable after system overhead):
max_children = 1500MB / 50MB = 30 workers maximum
Update the config with your calculated values:
; For 2GB RAM server (adjust based on your RAM)
pm = dynamic
pm.max_children = 30 ; Maximum workers (calculated above)
pm.start_servers = 8 ; 25% of max_children
pm.min_spare_servers = 5 ; Minimum idle workers
pm.max_spare_servers = 15 ; Maximum idle workers (50% of max_children)
pm.max_requests = 1000 ; Restart worker after 1000 requests (prevents memory leaks, industry standard)
For 1GB RAM: Set pm.max_children = 15
For 4GB RAM: Set pm.max_children = 60
Restart PHP-FPM:
PHP-FPM Service Name: The service name matches your PHP version. For PHP 8.2, use php8.2-fpm. For PHP 8.3, use php8.3-fpm. Check your installed version with systemctl list-units | grep php-fpm.
- sudo systemctl restart php8.2-fpm
Error: SQLSTATE[HY000] [2002] No such file or directory
Root Cause: Laravel attempting socket connection while MySQL configured for TCP, or incorrect socket path.
Diagnostic Steps:
- # Check MySQL service status
- sudo systemctl status mysql
-
- # Verify MySQL is listening
- sudo netstat -tlnp | grep mysql
-
- # Test direct connection
- mysql -u travellist_user -p -h 127.0.0.1
Solution:
- nano /var/www/travellist/.env
Update database configuration:
# Change from unix socket to TCP
DB_HOST=127.0.0.1
DB_PORT=3306
# Or specify socket path explicitly
DB_HOST=localhost
DB_SOCKET=/var/run/mysqld/mysqld.sock
Verification: After changes, clear Laravel’s config cache:
- php artisan config:clear
- php artisan config:cache
Error: Permission denied for storage or cache directories
Root Cause: Web server (www-data) lacks write permissions to Laravel’s writable directories.
Real-World Scenario: This commonly occurs after deploying via Git, where files inherit your user’s permissions instead of www-data.
Complete Solution:
- # Fix ownership
- sudo chown -R www-data:www-data /var/www/travellist
-
- # Set base permissions
- sudo find /var/www/travellist -type d -exec chmod 755 {} \;
- sudo find /var/www/travellist -type f -exec chmod 644 {} \;
-
- # Set writable directories
- sudo chmod -R 775 /var/www/travellist/storage
- sudo chmod -R 775 /var/www/travellist/bootstrap/cache
-
- # If using ACLs for development team access
- sudo setfacl -R -m u:www-data:rwx /var/www/travellist/storage
- sudo setfacl -R -m u:www-data:rwx /var/www/travellist/bootstrap/cache
Prevention: Add to deployment script:
#!/bin/bash
chown -R www-data:www-data /var/www/travellist
chmod -R 755 /var/www/travellist
chmod -R 775 /var/www/travellist/storage /var/www/travellist/bootstrap/cache
Error: 502 Bad Gateway when accessing the application
Root Cause: PHP-FPM service stopped, crashed, or socket path mismatch.
Diagnostic Steps:
- # Check PHP-FPM service (adjust version as needed)
- sudo systemctl status php8.2-fpm
-
- # Check for socket file
- ls -la /var/run/php/
-
- # Review PHP-FPM error log
- sudo tail -f /var/log/php8.2-fpm.log
-
- # Check Nginx error log for specifics
- sudo tail -f /var/log/nginx/error.log
Common Causes and Solutions:
1. PHP-FPM Service Down:
- sudo systemctl start php8.2-fpm
- sudo systemctl enable php8.2-fpm
2. Socket Path Mismatch:
- # Find actual socket
- ls /var/run/php/
-
- # Update Nginx config
- sudo nano /etc/nginx/sites-available/travellist
- # Change: fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
-
- # Test and reload
- sudo nginx -t
- sudo systemctl reload nginx
3. PHP-FPM Resource Exhaustion (all workers busy):
- # Check active processes
- sudo ps aux | grep php-fpm | wc -l
-
- # Review pool configuration
- sudo nano /etc/php/8.2/fpm/pool.d/www.conf
-
- # Increase max_children for high traffic
- pm.max_children = 50 # from 20
- pm.start_servers = 10 # from 5
- pm.min_spare_servers = 10
- pm.max_spare_servers = 20
-
- sudo systemctl restart php8.2-fpm
Error: No application encryption key has been specified
Root Cause: .env file missing APP_KEY or corrupted during deployment.
Solution:
- cd /var/www/travellist
- php artisan key:generate
-
- # Verify key was generated
- grep APP_KEY .env
-
- # Clear and rebuild cache
- php artisan config:clear
- php artisan config:cache
Production Deployment Gotcha: Never regenerate keys on production—this invalidates all encrypted data. Instead, copy APP_KEY from your previous .env or backup.
Error: PHP Fatal error: Allowed memory size exhausted
Root Cause: Composer requires significant memory during dependency resolution.
Solution:
- # Temporary memory increase
- php -d memory_limit=512M /usr/bin/composer install
-
- # Or set permanently
- export COMPOSER_MEMORY_LIMIT=-1
- composer install
-
- # For production, use --optimize-autoloader
- composer install --no-dev --optimize-autoloader
Symptom: Users logged out randomly, cached data not persisting.
Root Cause: Filesystem cache cleared during deployment or permissions incorrect.
Solution:
- # Clear all caches
- php artisan cache:clear
- php artisan config:clear
- php artisan view:clear
- php artisan route:clear
-
- # Rebuild optimized caches
- php artisan config:cache
- php artisan route:cache
- php artisan view:cache
-
- # Fix storage permissions
- sudo chmod -R 775 /var/www/travellist/storage
- sudo chown -R www-data:www-data /var/www/travellist/storage
Production Best Practice: Implement Redis or Memcached for sessions/cache instead of file-based storage to avoid these issues across deployments and multiple servers.
LEMP (Linux, Nginx, MySQL, PHP) and LAMP (Linux, Apache, MySQL, PHP) are both valid stacks for Laravel, but LEMP delivers measurably superior performance for modern applications. The quantified differences:
Performance Comparison:
Architecture Benefits: Nginx’s event-driven model scales horizontally without linear memory growth, making it ideal for Laravel’s asset-heavy modern applications. Laravel’s ecosystem (Sail, Forge, Vapor) standardizes on Nginx, ensuring better community support and documentation.
When to use LAMP: Consider Apache if you need .htaccess for per-directory configuration or legacy application compatibility. Otherwise, LEMP is the production standard.
Use Let’s Encrypt with Certbot for free SSL certificates:
- # Install Certbot
- sudo apt install certbot python3-certbot-nginx
-
- # Obtain and configure SSL
- sudo certbot --nginx -d your_domain.com -d www.your_domain.com
-
- # Test automatic renewal
- sudo certbot renew --dry-run
Certbot automatically:
Performance Impact: After enabling SSL, consider enabling HTTP/2 in your Nginx config for 30-50% faster page loads:
listen 443 ssl http2;
listen [::]:443 ssl http2;
Laravel requires specific permissions for proper operation:
| Directory/File | Permission | Ownership | Purpose |
|---|---|---|---|
| Application root | 755 |
www-data:www-data |
Web server read access |
storage/ |
775 |
www-data:www-data |
Logs, cache, sessions (write) |
bootstrap/cache/ |
775 |
www-data:www-data |
Framework cache (write) |
.env |
644 |
www-data:www-data |
Environment config (read) |
Apply correct permissions:
- sudo chown -R www-data:www-data /var/www/travellist
- sudo find /var/www/travellist -type d -exec chmod 755 {} \;
- sudo find /var/www/travellist -type f -exec chmod 644 {} \;
- sudo chmod -R 775 /var/www/travellist/storage
- sudo chmod -R 775 /var/www/travellist/bootstrap/cache
Security Note: Never set 777 permissions—this creates security vulnerabilities. Use 775 for writable directories with proper ownership instead.
403 Forbidden Errors:
- # 1. Check document root points to /public
- sudo nano /etc/nginx/sites-available/travellist
- # Verify: root /var/www/travellist/public;
-
- # 2. Verify index.php exists
- ls -la /var/www/travellist/public/index.php
-
- # 3. Fix permissions
- sudo chown -R www-data:www-data /var/www/travellist
- sudo chmod 755 /var/www/travellist/public
-
- # 4. Check Nginx user
- ps aux | grep nginx
- # Should run as www-data
404 Errors on Laravel Routes:
- # 1. Verify try_files directive
- sudo nano /etc/nginx/sites-available/travellist
- # Should have: try_files $uri $uri/ /index.php?$query_string;
-
- # 2. Test Nginx config
- sudo nginx -t
-
- # 3. Check Laravel routes
- cd /var/www/travellist
- php artisan route:list
-
- # 4. Clear route cache
- php artisan route:clear
- php artisan route:cache
Real-World Gotcha: Forgetting /public in the document root causes Laravel to serve the wrong entry point, exposing source code.
For configuration changes:
- # Nginx - graceful reload (no downtime)
- sudo nginx -t # Always test first
- sudo systemctl reload nginx
-
- # PHP-FPM - adjust version as needed
- sudo systemctl restart php8.2-fpm
Service management commands:
- # Check service status
- sudo systemctl status nginx
- sudo systemctl status php8.2-fpm
-
- # Enable auto-start on boot
- sudo systemctl enable nginx
- sudo systemctl enable php8.2-fpm
-
- # View real-time logs
- sudo journalctl -u nginx -f
- sudo journalctl -u php8.2-fpm -f
Production Best Practice: Use reload for Nginx (zero-downtime) and restart for PHP-FPM only when necessary. For PHP-FPM, consider reload if available to minimize disruption.
Yes, this tutorial uses version-agnostic commands and configurations that work across Ubuntu LTS and current releases. The core components (Nginx, MySQL, Laravel) maintain consistent behavior across Ubuntu versions.
Version-specific considerations:
php8.2-fpm)/var/run/php/ to identify your PHP-FPM socket/etc/php/8.2/)To identify your system:
- # Check Ubuntu version
- lsb_release -a
-
- # Check installed PHP version
- php -v
-
- # Find PHP-FPM socket
- ls /var/run/php/
-
- # Check PHP-FPM service name
- systemctl list-units | grep php
Built-in Laravel Tools:
- # Install Telescope for development/staging
- composer require laravel/telescope --dev
- php artisan telescope:install
- php artisan migrate
-
- # Access at: http://your_domain/telescope
-
- # For queue monitoring
- composer require laravel/horizon
- php artisan horizon:install
Server-Level Monitoring:
- # Real-time process monitoring
- htop
-
- # Nginx request monitoring
- sudo tail -f /var/log/nginx/access.log
-
- # PHP-FPM status
- sudo systemctl status php8.2-fpm
-
- # Laravel logs
- tail -f /var/www/travellist/storage/logs/laravel.log
Performance Metrics to Track:
Production Monitoring: Consider APM tools like New Relic, Datadog, or open-source alternatives like Grafana + Prometheus for comprehensive monitoring.
Laravel Sail (Docker-based development environment) and native LEMP serve different purposes:
Use Laravel Sail when:
Use native LEMP (this tutorial) when:
Hybrid Approach: Many teams use Sail for development and deploy to native LEMP for production. This combines development convenience with production performance.
Performance Comparison:
To run Laravel in a subdirectory (e.g., example.com/myapp):
location /myapp {
alias /var/www/travellist/public;
try_files $uri $uri/ @myapp;
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $request_filename;
include fastcgi_params;
}
}
location @myapp {
rewrite /myapp/(.*)$ /myapp/index.php?/$1 last;
}
Update Laravel’s .env:
APP_URL=http://your_domain.com/myapp
Gotcha: Many Laravel features (asset compilation, routes) assume root-level deployment. Consider using a subdomain (myapp.example.com) instead for cleaner configuration.
You have now set up a secure and robust Laravel application on Ubuntu, leveraging the power of the LEMP stack (Linux, Nginx, MySQL, and PHP-FPM). This tutorial guided you step-by-step through the installation and configuration of each component, the enforcement of strong security measures for database access, file permissions, and SSL integration, as well as best practices for maintaining stable and performant web infrastructure.
Along the way, you learned how to optimize your setup through Nginx and PHP-FPM tuning, enable advanced Laravel caching, and establish production-ready workflows for SSL, system monitoring, and troubleshooting. By applying these techniques, you’ve ensured your Laravel environment is well-prepared for scalable growth, reliable operation, and best-in-class security on modern Ubuntu systems.
Your Laravel application is now ready for production use with proper security, performance optimizations, and monitoring capabilities.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
Dev/Ops passionate about open source, PHP, and Linux. Former Senior Technical Writer at DigitalOcean. Areas of expertise include LAMP Stack, Ubuntu, Debian 11, Linux, Ansible, and more.
Building future-ready infrastructure with Linux, Cloud, and DevOps. Full Stack Developer & System Administrator @ DigitalOcean | GitHub Contributor | Passionate about Docker, PostgreSQL, and Open Source | Exploring NLP & AI-TensorFlow | Nailed over 50+ deployments across production environments.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Followed all of the steps, but i having an issue with mysql, i’m getting an error ‘SQLSTATE[HY000] [1045] Access denied for user’ but when i tried logging in the username and password using the terminal command ‘mysql -u username -p’ i can login and even use the database, its just that on laravel level im getting the issue, can you please advise ?
Followed all the steps and seems to be working fine, but I get the following warning when using composer.
The stream or file “/var/www/travellist/storage/logs/laravel.log” could not be opened in append mode: failed to open stream: Permission denied
Nevermind it was a permission problem, solved the issue.
sudo chown -R $USER:www-data storage sudo chown -R $USER:www-data bootstrap/cache
I have followed all steps but am receiving an error when loading my page.
502 BAD GATEWAY NGINX (UBUNTU)
If someone can help me then that is great. Thanks
please write article "install laravel 9 on ubuntu server with apache2"
thanks
Hello, I followed this guide to set up my Laravel application with the exact same setup of the server. So basically, I have only changed the laravel application, the rest is the same.
When I access the ip_address in the browser my application redirects the users to ip_address/login page to log in, and I get a 403. The login.blade.php is located in laravel_root/resources/views/
Iin the nginx error.log I see “access forbidden by rule”.
I found somewhere as a solution to remove location ~ /.(?!well-known).* { deny all; }
from the nginx file. And it worked.
My question is what is the risk of this? Is there another more secure way to fix 403 without removing the deny all rule?
The app works. Laravel can be accessed via the Nginx server_name directive.
But when testing, php artisan serve will fail with the error “could not assign request address”
Not sure how to configure Nginx to allow php artisan to work on a specific port.
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.
Full documentation for every DigitalOcean product.
The Wave has everything you need to know about building a business, from raising funding to marketing your product.
Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.
New accounts only. By submitting your email you agree to our Privacy Policy
Scale up as you grow — whether you're running one virtual machine or ten thousand.
Sign up and get $200 in credit for your first 60 days with DigitalOcean.*
*This promotional offer applies to new accounts only.