
August 17, 2026
10 min read
Table of Contents
By Kokil Thapa | Last reviewed: August 2026
You need to set up a LEMP stack on Ubuntu that actually handles production traffic without collapsing under load or exposing security holes. While basic tutorials get Nginx, MySQL, and PHP running, they often skip the critical tuning required for modern frameworks like Laravel 12 or high-traffic WooCommerce stores. This guide covers the complete installation and hardening process for Ubuntu 24.04 LTS, focusing on the specific configurations I use when deploying client projects. If you are evaluating infrastructure options before starting, understanding these requirements helps clarify whether managed cloud hosting services in Nepal or a self-managed VPS better fits your budget and technical capacity.
What Is Required to Set Up a LEMP Stack on Ubuntu in 2026?
The LEMP acronym replaces Apache with Nginx (pronounced "Engine-X") as the web server, keeping Linux, MySQL/MariaDB, and PHP as the core components. In 2026, the minimum viable versions for a secure, performant stack differ significantly from older documentation you might encounter. Using outdated versions exposes you to unpatched vulnerabilities and compatibility issues with modern application code.
For Ubuntu 24.04 LTS (Noble Numbat), which is the current standard for new deployments, you should target these specific component versions:
- Nginx: 1.26+ (stable branch) or 1.27+ (mainline). Ubuntu 24.04 ships with 1.24 by default; adding the official Nginx PPA is recommended for latest stable releases.
- MySQL: 8.4 LTS is the current long-term support release. MySQL 8.0 remains supported but 8.4 offers improved defaults and performance schema enhancements.
- PHP: PHP 8.4 is the latest stable release as of late 2025 and fully supported through 2026. PHP 8.3 is also widely used and stable. Laravel 12 requires minimum PHP 8.2; Symfony 7.x requires PHP 8.2+. Never deploy PHP 8.1 or lower for new projects in 2026.
- OS: Ubuntu 24.04 LTS provides five years of security updates. Avoid non-LTS releases like 25.04 for production servers.
Before installation, ensure your server has at least 2GB RAM for comfortable operation of all three services. For Laravel applications with queue workers or WordPress with multiple plugins, 4GB is a safer baseline. Run sudo apt update && sudo apt upgrade -y first to patch existing packages. Also configure UFW firewall rules immediately after SSH access: allow ports 22 (SSH), 80 (HTTP), and 443 (HTTPS) before installing any services.
How Do You Install and Configure Nginx for Production?
Nginx serves as the front-line reverse proxy and static file server in the LEMP stack. Unlike Apache's process-per-request model, Nginx uses an event-driven architecture that handles thousands of concurrent connections with minimal memory overhead. This makes it ideal for serving Laravel APIs or WordPress sites where many requests are static assets.
Installation and Basic Hardening
sudo apt install nginx -y
sudo systemctl enable nginx
sudo systemctl start nginx
# Verify version and status
nginx -v
sudo systemctl status nginx After installation, edit the main configuration file to harden security headers and disable unnecessary features:
sudo nano /etc/nginx/nginx.conf
# Add inside http {} block:
server_tokens off;
client_max_body_size 64M;
types_hash_max_size 2048;
# Security headers (add to server blocks or include globally)
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always; The server_tokens off; directive prevents Nginx from disclosing its version number in response headers and error pages, reducing information available to attackers. The client_max_body_size limit prevents large upload attacks; adjust based on your application's needs (e.g., media uploads for WordPress or document uploads for legal-tech portals).
Creating Server Blocks for Applications
Never modify the default site configuration for production applications. Create dedicated server blocks in /etc/nginx/sites-available/ and symlink them to /etc/nginx/sites-enabled/. Here is a production-ready template for a Laravel 12 application:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/example.com/public;
index index.php index.html;
# Logging
access_log /var/log/nginx/example.com.access.log;
error_log /var/log/nginx/example.com.error.log;
# Laravel routing
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# PHP processing
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_buffer_size 16k;
fastcgi_buffers 4 16k;
}
# Deny access to hidden files
location ~ /\.(?!well-known).* {
deny all;
}
} Critical details often missed: the root must point to the /public directory for Laravel, never the project root. The fastcgi_pass uses a Unix socket rather than TCP localhost for better performance on single-server setups. After creating the config, test syntax with sudo nginx -t before reloading with sudo systemctl reload nginx.
How Should MySQL 8.4 Be Secured During Installation?
Database security failures cause more breaches than any other LEMP component. MySQL 8.4 introduces changes to authentication plugins and default behaviors that require explicit attention during setup. Running mysql_secure_installation is mandatory, but insufficient alone.
Installation and Authentication Setup
sudo apt install mysql-server -y
sudo systemctl enable mysql
sudo mysql_secure_installation During the secure installation wizard, make these specific choices:
- VALIDATE PASSWORD COMPONENT: Enable with strength level 2 (MEDIUM) for production. This enforces mixed case, numbers, and special characters.
- Remove anonymous users: YES. Anonymous accounts allow unauthenticated local access.
- Disallow root login remotely: YES. Root should only connect via Unix socket locally.
- Remove test database: YES. The test database is accessible by any user and poses a risk.
- Reload privilege tables: YES. Applies changes immediately.
MySQL 8.4 defaults to caching_sha2_password authentication plugin, which is secure but incompatible with some older PHP drivers. Verify your PHP MySQL extension supports it:
# Check PHP MySQL driver
php -i | grep mysqlnd
# If using legacy apps requiring mysql_native_password:
ALTER USER 'app_user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'StrongP@ss!'; Creating Application-Specific Users
Never run applications as root. Create dedicated users with minimal privileges for each application:
CREATE DATABASE laravel_app CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'laravel_user'@'localhost' IDENTIFIED BY 'Str0ng!Pass#2026';
GRANT ALL PRIVILEGES ON laravel_app.* TO 'laravel_user'@'localhost';
FLUSH PRIVILEGES; The utf8mb4 charset with utf8mb4_unicode_ci collation is essential for proper emoji and international character support, particularly important for Nepali-language legal documents or multilingual eCommerce catalogs. Restrict host to localhost unless remote database access is explicitly required (which it rarely should be).
How Do You Tune PHP 8.4-FPM for Real Workloads?
PHP-FPM (FastCGI Process Manager) is where most LEMP performance problems originate. Default configurations assume minimal load and fail catastrophically under real traffic. Tuning requires understanding your server's RAM and your application's memory footprint.
Calculating Worker Processes
The critical setting is pm.max_children, which determines how many PHP processes can handle requests simultaneously. Each Laravel worker consumes approximately 40-60MB RAM; WordPress workers typically use 30-50MB. Calculate using this formula:
Available RAM for PHP = Total RAM - (MySQL + Nginx + OS Reserve)
max_children = Available RAM / Average Process Size
# Example: 4GB server, MySQL uses 1GB, reserve 512MB for OS/Nginx
# Available: 4096 - 1024 - 512 = 2560MB
# Laravel avg 50MB: 2560 / 50 = 51 max_children Edit the pool configuration file:
sudo nano /etc/php/8.4/fpm/pool.d/www.conf
; Dynamic process manager for variable traffic
pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 1000
; Slow log for debugging
slowlog = /var/log/php-fpm/slow.log
request_slowlog_timeout = 5s The pm.max_requests = 1000 setting recycles workers after handling 1000 requests, preventing memory leaks from accumulating over time. This is especially important for long-running Laravel queue workers or WordPress installations with poorly coded plugins. The slow log captures any request taking longer than 5 seconds, invaluable for identifying bottlenecks in production.
OPcache Configuration for Performance
OPcache stores precompiled PHP bytecode in shared memory, eliminating parsing overhead on every request. Enable and tune it in /etc/php/8.4/fpm/conf.d/10-opcache.ini:
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
opcache.save_comments=1 Setting validate_timestamps=0 disables file change checking entirely, providing maximum performance. However, this means you must restart PHP-FPM after every deployment (sudo systemctl restart php8.4-fpm). On development servers, keep validation enabled. For production deployments using Deployer or similar tools, integrate the FPM restart into your deployment script automatically.
What Are Common Mistakes When Setting Up LEMP on Ubuntu?
After years of troubleshooting LEMP stacks for clients, certain mistakes appear repeatedly. Avoiding these saves hours of debugging and prevents security incidents.
| Mistake | Consequence | Correct Approach |
|---|---|---|
| Using TCP instead of Unix sockets for PHP-FPM | 10-15% higher latency under load due to TCP overhead | Configure fastcgi_pass unix:/run/php/php8.4-fpm.sock; |
| Running applications as root or www-data | Compromised app gains full system access | Create dedicated system users per application with minimal permissions |
| Leaving default MySQL bind-address | Database exposed to public internet if firewall misconfigured | Set bind-address = 127.0.0.1 in /etc/mysql/mysql.conf.d/mysqld.cnf |
| Not setting client_max_body_size in Nginx | File uploads fail silently with 413 errors | Match limit to application requirements (e.g., 64M for media uploads) |
| Skipping OPcache restart after deploys | Users see cached old code, causing errors and data corruption | Add systemctl restart php8.4-fpm to deployment pipeline |
| Using outdated PHP versions for new projects | Missing security patches, framework incompatibility | Minimum PHP 8.2 for Laravel 12/Symfony 7; prefer 8.4 for new installs |
One mistake deserves special emphasis: file ownership. After cloning a repository or extracting an archive, always set correct ownership:
# For Laravel applications
sudo chown -R www-data:www-data /var/www/example.com/storage
sudo chown -R www-data:www-data /var/www/example.com/bootstrap/cache
sudo chmod -R 775 /var/www/example.com/storage
# For WordPress
sudo chown -R www-data:www-data /var/www/example.com/wp-content Incorrect permissions cause cryptic "permission denied" errors in logs, failed uploads, and cache write failures. The storage and cache directories must be writable by the web server user; application code files should remain read-only to prevent tampering.
How Do You Verify and Maintain a Production LEMP Stack?
Installation completes the setup; verification ensures it works correctly under real conditions. Run these checks after configuring all components:
- PHP-FPM Status: Create a temporary
info.phpfile in your web root containing<?php phpinfo(); ?>, access it via browser, verify OPcache is enabled and loaded extensions match requirements, then delete the file immediately. - MySQL Connectivity: Test application database connections using the CLI:
mysql -u laravel_user -p laravel_app. Verify charset withSHOW VARIABLES LIKE 'character_set%';. - Nginx Configuration: Run
sudo nginx -Tto dump the complete effective configuration. Review for syntax errors, incorrect root paths, or missing security headers. - Socket Permissions: Verify PHP-FPM socket exists and has correct permissions:
ls -la /run/php/php8.4-fpm.sock. Should showsrw-rw----owned by www-data. - Firewall Rules: Confirm UFW allows only necessary ports:
sudo ufw status verbose. Ports 22, 80, 443 should be ALLOW; 3306 should be DENY or restricted to specific IPs.
Ongoing maintenance prevents degradation. Set up automated security updates with unattended-upgrades, configure log rotation for Nginx and PHP-FPM logs to prevent disk exhaustion, and schedule weekly MySQL backups using mysqldump or Percona XtraBackup. Monitor resource usage with tools like htop or Netdata to identify when scaling becomes necessary.
For developers building Laravel applications specifically, review Laravel developer best practices to ensure your application code complements your server configuration. Similarly, if you're deploying WordPress, understanding WordPress development patterns helps avoid plugin conflicts that undermine server-level optimizations.
Next Steps After Your LEMP Stack Is Running
A properly configured LEMP stack provides the foundation, but production systems require additional layers. Install Certbot for automated SSL/TLS certificates from Let's Encrypt — never run HTTP-only sites in 2026. Configure Redis for session storage and caching to reduce MySQL load. Set up automated backups with verification restores. Implement monitoring with alerting for disk space, memory pressure, and service failures.
If managing infrastructure feels overwhelming or your project requires specialized expertise, consider working with an experienced web developer in Nepal who understands both application code and server administration. Many businesses find that professional setup and ongoing maintenance costs less than debugging self-configured servers after outages.
Ready to deploy your application on a properly configured LEMP stack? Contact me to discuss your project requirements, get a server audit, or arrange professional setup and hardening for your Ubuntu infrastructure.

