Skip to content

11. Hardening and Optimizing the LEMP Stack

11.1. Introduction to Hardening and Optimization

Section titled “11.1. Introduction to Hardening and Optimization”

After installing the LEMP stack, it’s essential to harden and optimize each component to ensure maximum security and performance. This section covers advanced configurations for Nginx, MariaDB, and PHP-FPM that will:

  1. Improve security by reducing attack surfaces
  2. Enhance performance for handling WordPress traffic
  3. Optimize resource usage on your server
  4. Prepare your server for production workloads

Nginx is highly configurable and can be fine-tuned for better performance and security. We’ll organize our configuration into modular files for easier management.

First, let’s create a backup of the original configuration and set up a directory structure for our modular configuration files:

# Navigate to Nginx configuration directory
cd /etc/nginx/
# Create an includes directory for modular configuration
sudo mkdir includes/
# Create a backup of the original configuration
sudo cp nginx.conf nginx.conf.bak
# Edit the main configuration file
sudo vim nginx.conf

Add the following directives to the main context (outside any blocks) in your nginx.conf file:

# Increase the maximum number of open files per worker
worker_rlimit_nofile 30000;
# Set worker processes to higher priority (-20 to 19, lower is higher priority)
worker_priority -10;
# Optimize internal timer resolution
timer_resolution 100ms;
# Enable PCRE JIT compilation for better regex performance
pcre_jit on;

These settings:

  • Increase the file descriptor limit for Nginx workers
  • Give Nginx workers higher CPU priority
  • Optimize timer resolution for better performance
  • Enable Just-In-Time compilation for regular expressions

In the events {} block, modify or add these directives:

events {
# Increase maximum connections per worker
worker_connections 4096;
# Enable accept mutex for better connection distribution
accept_mutex on;
# Set accept mutex delay
accept_mutex_delay 200ms;
# Use the efficient epoll event model on Linux
use epoll;
}

These settings optimize how Nginx handles connections:

  • Increases the number of simultaneous connections each worker can handle
  • Improves connection distribution among worker processes
  • Uses the efficient epoll event processing method available on Linux

11.2.4. Creating Modular Configuration Files

Section titled “11.2.4. Creating Modular Configuration Files”

Let’s create separate configuration files for different aspects of Nginx:

# Navigate to the includes directory
cd /etc/nginx/includes/
# Create configuration files for different aspects
sudo touch basic_settings.conf buffers.conf timeouts.conf file_handle_cache.conf gzip.conf brotli.conf
# List the files to confirm creation
ls -l

Create the basic settings configuration file:

# Edit the basic settings configuration
sudo vim /etc/nginx/includes/basic_settings.conf

Add the following content:

##
# BASIC SETTINGS
##
# Set character encoding
charset utf-8;
# Enable efficient file serving
sendfile on;
sendfile_max_chunk 512k;
# Optimize TCP settings
tcp_nopush on;
tcp_nodelay on;
# Security: Hide server information
server_tokens off;
more_clear_headers 'Server';
more_clear_headers 'X-Powered';
# Server name handling
server_name_in_redirect off;
server_names_hash_bucket_size 64;
# Hash table settings
variables_hash_max_size 2048;
types_hash_max_size 2048;
# MIME types
include /etc/nginx/mime.types;
default_type application/octet-stream;

These settings:

  • Set UTF-8 as the default character encoding
  • Enable efficient file serving with sendfile
  • Optimize TCP for better performance
  • Hide server information for improved security
  • Configure hash tables for better performance

Create the buffer settings configuration file:

# Edit the buffers configuration
sudo vim /etc/nginx/includes/buffers.conf

Add the following content:

##
# BUFFERS
##
# Client request buffers
client_body_buffer_size 256k;
client_body_in_file_only off;
client_header_buffer_size 64k;
client_max_body_size 100m; # Consider reducing after setup
# Connection and I/O buffers
connection_pool_size 512;
directio 4m;
ignore_invalid_headers on;
large_client_header_buffers 8 64k;
output_buffers 8 256k;
postpone_output 1460;
request_pool_size 32k;

These buffer settings:

  • Optimize memory usage for client requests
  • Set appropriate buffer sizes for headers and body content
  • Configure I/O operations for better performance
  • Set a reasonable maximum body size for file uploads

Create the timeout settings configuration file:

# Edit the timeouts configuration
sudo vim /etc/nginx/includes/timeouts.conf

Add the following content:

##
# TIMEOUTS
##
# Connection timeouts
keepalive_timeout 5;
keepalive_requests 500;
lingering_time 20s;
lingering_timeout 5s;
keepalive_disable msie6;
# Reset timed out connections
reset_timedout_connection on;
# Request timeouts
send_timeout 15s;
client_header_timeout 8s;
client_body_timeout 10s;

These timeout settings:

  • Optimize connection handling and resource usage
  • Prevent slow clients from consuming server resources
  • Close idle connections after 5 seconds
  • Allow up to 500 requests per connection before closing

Create the Gzip compression configuration file:

# Edit the gzip configuration
sudo vim /etc/nginx/includes/gzip.conf

Add the following content:

##
# GZIP
##
# Enable gzip compression
gzip on;
gzip_vary on;
gzip_disable "MSIE [1-6]\.";
gzip_static on;
# Gzip settings
gzip_min_length 1400;
gzip_buffers 32 8k;
gzip_http_version 1.0;
gzip_comp_level 5;
gzip_proxied any;
# MIME types to compress
gzip_types text/plain text/css text/xml application/javascript application/x-javascript
application/xml application/xml+rss application/ecmascript application/json
image/svg+xml;

Create the Brotli compression configuration file:

# Edit the brotli configuration
sudo vim /etc/nginx/includes/brotli.conf

Add the following content:

##
# BROTLI
##
# Enable Brotli compression
brotli on;
brotli_comp_level 6;
brotli_static on;
# MIME types to compress with Brotli
brotli_types application/atom+xml application/javascript application/json application/rss+xml
application/vnd.ms-fontobject application/x-font-opentype application/x-font-truetype
application/x-font-ttf application/x-javascript application/xhtml+xml application/xml
font/eot font/opentype font/otf font/truetype image/svg+xml image/vnd.microsoft.icon
image/x-icon image/x-win-bitmap text/css text/javascript text/plain text/xml;

Compression benefits:

  • Reduces bandwidth usage by 70-90% for text-based content
  • Improves page load times for visitors
  • Brotli typically achieves 15-25% better compression than Gzip

Create the file handle cache configuration file:

# Edit the file handle cache configuration
sudo vim /etc/nginx/includes/file_handle_cache.conf

Add the following content:

##
# FILE HANDLE CACHE
##
# Cache open file descriptors and information
open_file_cache max=50000 inactive=60s;
open_file_cache_valid 120s;
open_file_cache_min_uses 2;
open_file_cache_errors off;

These settings:

  • Cache information about open files to reduce disk I/O
  • Store up to 50,000 file entries in the cache
  • Remove entries not accessed for 60 seconds
  • Validate cache entries every 120 seconds
  • Require at least 2 uses before caching a file

11.2.10. Integrating Configuration Files into Nginx

Section titled “11.2.10. Integrating Configuration Files into Nginx”

Now that we’ve created all our modular configuration files, we need to integrate them into the main Nginx configuration file.

First, let’s edit the main configuration file:

# Edit the main Nginx configuration file
sudo vim /etc/nginx/nginx.conf

In the http {} block of your nginx.conf file, you’ll need to remove or comment out the following default sections:

##
# Basic Settings
##
##
# SSL Settings
##
##
# Logging Settings
##
##
# Gzip Settings
##

Replace the removed sections with include directives to our modular configuration files. Your http {} block should look similar to this:

http {
# Basic Settings
include /etc/nginx/includes/basic_settings.conf;
# Buffer Settings
include /etc/nginx/includes/buffers.conf;
# Timeout Settings
include /etc/nginx/includes/timeouts.conf;
# File Handle Cache Settings
include /etc/nginx/includes/file_handle_cache.conf;
# SSL Settings
# (Keep any existing SSL settings here)
# Logging Settings
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# Compression Settings
include /etc/nginx/includes/gzip.conf;
include /etc/nginx/includes/brotli.conf;
# Virtual Host Configs
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}

This modular approach makes your configuration:

  • Easier to maintain and update
  • More organized and readable
  • Simpler to troubleshoot when issues arise

11.2.11. Testing and Applying the Configuration

Section titled “11.2.11. Testing and Applying the Configuration”

After making all changes, it’s essential to test the configuration before applying it:

# Test the Nginx configuration for syntax errors
sudo nginx -t

If the test is successful, reload Nginx to apply the changes:

# Reload Nginx with the new configuration
sudo systemctl reload nginx

To ensure our file descriptor limit settings are applied correctly:

# Find Nginx worker processes
ps aux | grep nginx
# Check the file descriptor limits for a worker process
# Replace XXXX with the actual process ID from the previous command
sudo cat /proc/XXXX/limits | grep "open files"

If you need to increase the file descriptor limit further, you can modify the worker_rlimit_nofile directive in the main context of nginx.conf:

# Edit nginx.conf again if needed
sudo vim /etc/nginx/nginx.conf

Increase the value if necessary:

# Increase file descriptor limit if needed
worker_rlimit_nofile 45000;

After any changes, test and reload Nginx again:

# Test configuration
sudo nginx -t
# Reload Nginx
sudo systemctl reload nginx

Bash aliases can save you time by creating shortcuts for frequently used commands. This is especially helpful when managing your server.

11.3.1. Setting Up Server Management Aliases

Section titled “11.3.1. Setting Up Server Management Aliases”

Create or edit your .bash_aliases file:

# Edit your bash aliases file
vim ~/.bash_aliases

Add the following useful aliases for server management:

# System update and maintenance
alias server_update='sudo apt update && sudo apt upgrade && sudo apt autoremove'
# Nginx management
alias ngt='sudo nginx -t' # Test Nginx configuration
alias ngr='sudo systemctl reload nginx' # Reload Nginx
alias ngsa='cd /etc/nginx/sites-available/ && ls' # Navigate to sites-available
alias ngin='cd /etc/nginx/includes/ && ls' # Navigate to includes directory
# PHP-FPM management
alias fpmr='sudo systemctl restart php8.3-fpm' # Restart PHP-FPM

To make your aliases available in your current session:

# Source your bash aliases file
source ~/.bash_aliases

To test that your aliases are working:

# Test the nginx configuration using the alias
ngt

MariaDB is the database server that stores all your WordPress data. Properly configuring it is crucial for both security and performance.

First, run the MariaDB secure installation script to remove insecure defaults and lock down access:

# Run the security script
sudo mysql_secure_installation

11.4.2. Understanding the Security Script Options

Section titled “11.4.2. Understanding the Security Script Options”

When running the mysql_secure_installation script, you’ll be presented with several security options. Here’s what each option means and the recommended settings:

  1. Root Password: If this is your first time running the script, you’ll be asked to set a root password. Choose a strong password and store it securely.

  2. Unix Socket Authentication: This option allows you to authenticate using the Unix socket file instead of a password. For most WordPress setups, you can answer n.

  3. Change Root Password: If you’ve already set a root password, you can choose to change it or keep it by answering n.

  4. Remove Anonymous Users: Anonymous users are a security risk and should be removed in production environments. Answer Y.

  5. Disallow Root Login Remotely: Preventing remote root login is a security best practice. Answer Y.

  6. Remove Test Database: The test database is not needed in production and should be removed. Answer Y.

  7. Reload Privilege Tables: This applies all changes immediately. Answer Y.

After securing MariaDB, let’s optimize its performance for WordPress. First, create a backup of the configuration file:

# Navigate to the MariaDB configuration directory
cd /etc/mysql/mariadb.conf.d/
# Create a backup of the server configuration
sudo cp 50-server.cnf 50-server.cnf.bak
# Edit the configuration file
sudo vim 50-server.cnf

The Performance Schema provides instrumentation to help monitor MariaDB server execution at a low level. Add these lines to the [mysqld] section:

# Performance Schema
performance_schema=ON
performance-schema-instrument='stage/%=ON'
performance-schema-consumer-events-stages-current=ON
performance-schema-consumer-events-stages-history=ON
performance-schema-consumer-events-stages-history-long=ON

Add this line to improve connection speed by preventing DNS lookups:

# Disable DNS lookups for client connections
skip-name-resolve

This prevents the server from attempting to resolve client hostnames, which can slow down connections, especially when DNS is slow or unavailable.

MariaDB includes a query cache feature that can improve performance for read-heavy workloads by caching the results of SELECT queries. You can check the current query cache settings:

# Log into MariaDB
sudo mysql -u root -p
# Check if query cache is available
MariaDB [(none)]> show variables like 'have_query_cache';
# Check query cache settings
MariaDB [(none)]> show variables like 'query_cache%';

For WordPress sites with moderate traffic, you can enable and configure the query cache by adding these lines to the [mysqld] section:

# Query Cache Configuration
query_cache_type = 1
query_cache_size = 16M
query_cache_limit = 1M

Binary logs record all changes to your database and are used for replication and point-in-time recovery. By default, these logs are kept for 10 days, which can consume significant disk space. You can check and modify the retention period:

# Check current binary log expiration setting
MariaDB [(none)]> show variables like 'expire_logs_days';
# Set binary logs to expire after 3 days
MariaDB [(none)]> set global expire_logs_days = 3;
# Verify the change
MariaDB [(none)]> show variables like 'expire_logs_days';
# Flush binary logs to apply changes
MariaDB [(none)]> flush binary logs;

To make this change permanent, add this line to the [mysqld] section of your MariaDB configuration:

# Binary Log Settings
expire_logs_days = 3

InnoDB is the default storage engine for MariaDB and is optimized for performance and reliability. The buffer pool is one of the most important settings to tune for performance:

# Check current InnoDB buffer pool settings
MariaDB [(none)]> show variables like '%innodb_buffer%';
# Check InnoDB log file settings
MariaDB [(none)]> show variables like '%innodb_log%';

For optimal performance, add these settings to the [mysqld] section of your MariaDB configuration:

# InnoDB Settings
innodb_buffer_pool_size = 800M
innodb_log_file_size = 200M

After making these changes, restart MariaDB to apply them:

# Restart MariaDB
sudo systemctl restart mariadb

For larger servers with more RAM, you can further optimize InnoDB:

# Advanced InnoDB settings for servers with 8GB+ RAM
innodb_buffer_pool_size = 2048M
innodb_log_file_size = 512M

11.4.4. Using MySQLTuner for Advanced Optimization

Section titled “11.4.4. Using MySQLTuner for Advanced Optimization”

MySQLTuner is a Perl script that analyzes your MariaDB performance and provides recommendations for improving it:

# Download MySQLTuner
wget http://mysqltuner.pl/ -O mysqltuner.pl
# Make it executable
chmod +x mysqltuner.pl
# Run MySQLTuner (best after server has been running for at least 24 hours)
sudo ./mysqltuner.pl

MySQLTuner will analyze your current configuration and usage patterns, then provide specific recommendations for optimizing your MariaDB installation. This is especially valuable after your WordPress site has been running for some time, as it can identify bottlenecks based on actual usage.

11.4.5. Increasing MariaDB File Descriptor Limits

Section titled “11.4.5. Increasing MariaDB File Descriptor Limits”

MariaDB, like other services, is limited by the number of open files it can have. For busy databases, the default limits may be too low:

# Check current file descriptor limits for MariaDB
ps aux | grep mysql
cat /proc/$(ps aux | grep mysql | grep -v grep | awk '{print $2}')/limits | grep "Max open files"

To increase the file descriptor limits for MariaDB:

# Navigate to systemd directory
cd /etc/systemd/system/
# Create a directory for MariaDB service overrides if it doesn't exist
sudo mkdir -p mariadb.service.d/
# Navigate to the new directory
cd mariadb.service.d/
# Create a limits configuration file
sudo nano limits.conf

Add the following to the limits.conf file:

[Service]
LimitNOFILE=40000

Apply the changes:

# Reload systemd to recognize the changes
sudo systemctl daemon-reload
# Restart MariaDB
sudo systemctl restart mariadb
# Verify the new limits
cat /proc/$(ps aux | grep mysql | grep -v grep | awk '{print $2}')/limits | grep "Max open files"

You should see that the “Max open files” value has been increased to 40000, which will allow MariaDB to handle more concurrent connections and operations.

PHP-FPM (FastCGI Process Manager) is the PHP implementation that works with Nginx. Properly configuring PHP-FPM is essential for both security and performance.

11.5.1. PHP Security and Performance Settings

Section titled “11.5.1. PHP Security and Performance Settings”

Edit the PHP configuration file:

# Find the main PHP configuration file
sudo find /etc/php/8.3/ -name php.ini
# Edit the PHP configuration file (adjust path if needed)
sudo vim /etc/php/8.3/fpm/php.ini

Add or modify the following settings:

; Security Settings
allow_url_fopen = Off ; Prevents PHP from opening remote files
cgi.fix_pathinfo = 0 ; Prevents path traversal vulnerabilities
expose_php = Off ; Hides PHP version in HTTP headers
; Performance Settings
upload_max_filesize = 100M ; Maximum allowed size for uploaded files
post_max_size = 125M ; Maximum size of POST data (should be larger than upload_max_filesize)
max_input_vars = 3000 ; Maximum input variables (important for complex WordPress forms)
memory_limit = 256M ; Maximum memory a script can consume
; Resource Limits
rlimit_files = 32768 ; Maximum number of open files
rlimit_core = unlimited ; Allow core dumps for debugging

For better performance, you can also optimize the PHP-FPM pool configuration:

# Edit the www.conf file
sudo vim /etc/php/8.3/fpm/pool.d/www.conf

Find and modify these settings:

; Process Manager Settings
pm = dynamic ; Dynamic process manager
pm.max_children = 50 ; Maximum number of child processes
pm.start_servers = 5 ; Number of child processes created at startup
pm.min_spare_servers = 5 ; Minimum number of idle server processes
pm.max_spare_servers = 35 ; Maximum number of idle server processes
pm.max_requests = 500 ; Number of requests each child process should execute before respawning

After making these changes, restart PHP-FPM and reload Nginx:

# Restart PHP-FPM
sudo systemctl restart php8.3-fpm
# Reload Nginx
sudo systemctl reload nginx

You have now successfully hardened and optimized your LEMP stack (Linux, Nginx, MariaDB, and PHP) for WordPress hosting. These configurations provide a good balance of security and performance for most WordPress sites.

Remember to monitor your server’s performance and adjust these settings as needed based on your specific workload and traffic patterns. Tools like MySQLTuner, Nginx status pages, and PHP-FPM status pages can help you identify bottlenecks and fine-tune your configuration.