How to Set Up a LEMP Stack on Ubuntu and Debian: Complete Installation Guide
The LEMP stack consists of Linux, Nginx (pronounced “Engine-X”), MySQL/MariaDB, and PHP. This powerful combination is preferred over LAMP for high-traffic websites due to Nginx’s superior performance in handling concurrent connections. This guide covers installing and configuring a complete LEMP stack on Ubuntu 22.04/24.04 and Debian 11/12.
📑 Table of Contents
- LEMP Stack Components
- Prerequisites
- Step 1: Update System Packages
- Step 2: Install Nginx
- Configure Firewall
- Step 3: Install MariaDB (MySQL Alternative)
- MariaDB Secure Installation Prompts
- Verify MariaDB Installation
- Step 4: Install PHP and PHP-FPM
- Verify PHP-FPM Socket
- Step 5: Configure Nginx for PHP
- Create a Server Block
- Enable the Site
- Step 6: Test PHP Processing
- Step 7: Test Database Connection
- Create a Test Database and User
- Create Database Test Script
- Step 8: Add SSL with Let’s Encrypt
- PHP-FPM Configuration
- Optimize PHP-FPM Pool
- PHP Configuration
- Nginx Performance Optimization
- MariaDB Optimization
- Installing WordPress on LEMP
- Nginx Configuration for WordPress
- Security Hardening
- Nginx Security Headers
- Secure File Permissions
- Useful Commands
- Troubleshooting
- 502 Bad Gateway
- Permission Denied Errors
- MySQL Connection Refused
- Conclusion
LEMP Stack Components
- Linux – The operating system
- Engine-X (Nginx) – High-performance web server
- MySQL/MariaDB – Database server
- PHP – Server-side scripting language
Prerequisites
- Ubuntu 22.04/24.04 or Debian 11/12 server
- Root or sudo access
- Domain name pointing to your server (optional but recommended)
Step 1: Update System Packages
# Update package index
sudo apt update
# Upgrade existing packages
sudo apt upgrade -y
Step 2: Install Nginx
# Install Nginx
sudo apt install nginx -y
# Start and enable Nginx
sudo systemctl start nginx
sudo systemctl enable nginx
# Verify installation
sudo systemctl status nginx
# Check Nginx version
nginx -v
Configure Firewall
# If using UFW
sudo ufw allow 'Nginx Full'
sudo ufw status
# Verify Nginx is running
curl -I localhost
Open your browser and visit http://your_server_ip. You should see the default Nginx welcome page.
Step 3: Install MariaDB (MySQL Alternative)
MariaDB is a drop-in replacement for MySQL with better performance and more features.
# Install MariaDB
sudo apt install mariadb-server mariadb-client -y
# Start and enable MariaDB
sudo systemctl start mariadb
sudo systemctl enable mariadb
# Secure the installation
sudo mysql_secure_installation
MariaDB Secure Installation Prompts
# Follow the prompts:
Enter current password for root: [Press Enter - no password set]
Switch to unix_socket authentication: n
Change the root password: Y
New password: [Enter strong password]
Remove anonymous users: Y
Disallow root login remotely: Y
Remove test database: Y
Reload privilege tables: Y
Verify MariaDB Installation
# Log into MariaDB
sudo mysql -u root -p
# Check version
SELECT VERSION();
# Exit
EXIT;
Step 4: Install PHP and PHP-FPM
PHP-FPM (FastCGI Process Manager) handles PHP processing for Nginx.
# Install PHP and common extensions
sudo apt install php-fpm php-mysql php-cli php-common php-curl \
php-gd php-mbstring php-xml php-zip php-intl php-bcmath -y
# Check PHP version
php -v
# Check PHP-FPM status
sudo systemctl status php*-fpm
Verify PHP-FPM Socket
# Find PHP-FPM socket path
ls /run/php/
# Output example: php8.2-fpm.sock
Step 5: Configure Nginx for PHP
Create a Server Block
# Create configuration file
sudo nano /etc/nginx/sites-available/example.com
Add the following configuration:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/example.com;
index index.php index.html index.htm;
# Logging
access_log /var/log/nginx/example.com.access.log;
error_log /var/log/nginx/example.com.error.log;
# Main location block
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# PHP processing
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
# Deny access to .htaccess files
location ~ /\.ht {
deny all;
}
# Cache static files
location ~* \.(jpg|jpeg|png|gif|ico|css|js|pdf|txt)$ {
expires 7d;
add_header Cache-Control "public, no-transform";
}
}
Enable the Site
# Create document root
sudo mkdir -p /var/www/example.com
# Set ownership
sudo chown -R www-data:www-data /var/www/example.com
# Create symbolic link to enable site
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
# Remove default site (optional)
sudo rm /etc/nginx/sites-enabled/default
# Test configuration
sudo nginx -t
# Reload Nginx
sudo systemctl reload nginx
Step 6: Test PHP Processing
# Create PHP test file
sudo nano /var/www/example.com/info.php
Add:
<?php
phpinfo();
?>
Visit http://your_server_ip/info.php to see PHP information.
# Remove test file after verification (security)
sudo rm /var/www/example.com/info.php
Step 7: Test Database Connection
Create a Test Database and User
# Log into MariaDB
sudo mysql -u root -p
# Create database
CREATE DATABASE testdb;
# Create user
CREATE USER 'testuser'@'localhost' IDENTIFIED BY 'StrongPassword123!';
# Grant privileges
GRANT ALL PRIVILEGES ON testdb.* TO 'testuser'@'localhost';
# Apply changes
FLUSH PRIVILEGES;
EXIT;
Create Database Test Script
sudo nano /var/www/example.com/dbtest.php
<?php
$host = 'localhost';
$db = 'testdb';
$user = 'testuser';
$pass = 'StrongPassword123!';
try {
$pdo = new PDO("mysql:host=$host;dbname=$db", $user, $pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
echo "<h1>Database Connection Successful!</h1>";
echo "<p>LEMP Stack is working correctly.</p>";
} catch(PDOException $e) {
echo "Connection failed: " . $e->getMessage();
}
?>
Visit http://your_server_ip/dbtest.php to test.
# Remove test file
sudo rm /var/www/example.com/dbtest.php
Step 8: Add SSL with Let’s Encrypt
# Install Certbot
sudo apt install certbot python3-certbot-nginx -y
# Obtain SSL certificate
sudo certbot --nginx -d example.com -d www.example.com
# Follow prompts to complete installation
# Verify auto-renewal
sudo certbot renew --dry-run
PHP-FPM Configuration
Optimize PHP-FPM Pool
sudo nano /etc/php/8.2/fpm/pool.d/www.conf
Key settings to adjust:
; Process manager settings
pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 35
pm.max_requests = 500
; Timeouts
request_terminate_timeout = 300
; Error logging
php_admin_flag[log_errors] = on
php_admin_value[error_log] = /var/log/php/error.log
PHP Configuration
sudo nano /etc/php/8.2/fpm/php.ini
Recommended settings:
; Memory and execution
memory_limit = 256M
max_execution_time = 300
max_input_time = 300
; Upload limits
upload_max_filesize = 64M
post_max_size = 64M
; Error handling
display_errors = Off
log_errors = On
error_log = /var/log/php/error.log
; Security
expose_php = Off
allow_url_fopen = Off
; Session security
session.cookie_httponly = 1
session.cookie_secure = 1
session.use_strict_mode = 1
# Create log directory
sudo mkdir -p /var/log/php
sudo chown www-data:www-data /var/log/php
# Restart PHP-FPM
sudo systemctl restart php8.2-fpm
Nginx Performance Optimization
sudo nano /etc/nginx/nginx.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 1024;
multi_accept on;
use epoll;
}
http {
# Basic Settings
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off;
# Buffer sizes
client_body_buffer_size 10K;
client_header_buffer_size 1k;
client_max_body_size 64m;
large_client_header_buffers 4 32k;
# Timeouts
client_body_timeout 12;
client_header_timeout 12;
send_timeout 10;
# Gzip compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json
application/javascript application/xml+rss
application/atom+xml image/svg+xml;
# FastCGI cache
fastcgi_cache_path /var/cache/nginx levels=1:2
keys_zone=WORDPRESS:100m inactive=60m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
MariaDB Optimization
sudo nano /etc/mysql/mariadb.conf.d/50-server.cnf
Add under [mysqld]:
[mysqld]
# InnoDB settings
innodb_buffer_pool_size = 256M
innodb_log_file_size = 64M
innodb_flush_log_at_trx_commit = 2
innodb_flush_method = O_DIRECT
# Query cache (for MariaDB)
query_cache_type = 1
query_cache_size = 32M
query_cache_limit = 2M
# Connection settings
max_connections = 150
wait_timeout = 600
# Temporary tables
tmp_table_size = 64M
max_heap_table_size = 64M
# Logging
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2
# Restart MariaDB
sudo systemctl restart mariadb
Installing WordPress on LEMP
# Create database for WordPress
sudo mysql -u root -p
CREATE DATABASE wordpress;
CREATE USER 'wpuser'@'localhost' IDENTIFIED BY 'StrongPassword!';
GRANT ALL PRIVILEGES ON wordpress.* TO 'wpuser'@'localhost';
FLUSH PRIVILEGES;
EXIT;
# Download WordPress
cd /tmp
wget https://wordpress.org/latest.tar.gz
tar -xzvf latest.tar.gz
# Move to web root
sudo mv wordpress/* /var/www/example.com/
# Set permissions
sudo chown -R www-data:www-data /var/www/example.com
sudo find /var/www/example.com -type d -exec chmod 755 {} \;
sudo find /var/www/example.com -type f -exec chmod 644 {} \;
Nginx Configuration for WordPress
server {
listen 80;
server_name example.com www.example.com;
root /var/www/example.com;
index index.php index.html;
# Logging
access_log /var/log/nginx/wordpress.access.log;
error_log /var/log/nginx/wordpress.error.log;
# WordPress permalinks
location / {
try_files $uri $uri/ /index.php?$args;
}
# PHP handling
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# Security - deny access to sensitive files
location ~ /\.ht {
deny all;
}
location = /wp-config.php {
deny all;
}
location ~* /(?:uploads|files)/.*\.php$ {
deny all;
}
# Cache static files
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
}
# Block XML-RPC
location = /xmlrpc.php {
deny all;
access_log off;
log_not_found off;
}
}
Security Hardening
Nginx Security Headers
# Add to server block
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
Secure File Permissions
# Web files
sudo chown -R www-data:www-data /var/www/example.com
sudo find /var/www/example.com -type d -exec chmod 755 {} \;
sudo find /var/www/example.com -type f -exec chmod 644 {} \;
# Config files (more restrictive)
sudo chmod 600 /var/www/example.com/wp-config.php
Useful Commands
# Service management
sudo systemctl status nginx php8.2-fpm mariadb
sudo systemctl restart nginx php8.2-fpm mariadb
# View logs
sudo tail -f /var/log/nginx/error.log
sudo tail -f /var/log/php/error.log
sudo tail -f /var/log/mysql/error.log
# Test Nginx configuration
sudo nginx -t
# Check PHP-FPM status
sudo php-fpm8.2 -t
# Database backup
sudo mysqldump -u root -p wordpress > backup.sql
Troubleshooting
502 Bad Gateway
- Check PHP-FPM is running:
sudo systemctl status php8.2-fpm - Verify socket path in Nginx config matches PHP-FPM socket
- Check PHP-FPM logs:
sudo tail /var/log/php8.2-fpm.log
Permission Denied Errors
sudo chown -R www-data:www-data /var/www/example.com
sudo chmod -R 755 /var/www/example.com
MySQL Connection Refused
# Check if MariaDB is running
sudo systemctl status mariadb
# Verify user has correct privileges
sudo mysql -u root -p
SHOW GRANTS FOR 'username'@'localhost';
Conclusion
Your LEMP stack is now ready for production use. This setup provides excellent performance for PHP applications like WordPress, Laravel, and other frameworks. Remember to keep your system updated, monitor logs regularly, and implement additional security measures like Fail2ban for comprehensive protection.
For high-traffic sites, consider adding Redis or Memcached for caching, and implementing a CDN for static content delivery.
Was this article helpful?
About Ramesh Sundararamaiah
Red Hat Certified Architect
Expert in Linux system administration, DevOps automation, and cloud infrastructure. Specializing in Red Hat Enterprise Linux, CentOS, Ubuntu, Docker, Ansible, and enterprise IT solutions.