π‘ Newer LTS generation available: Planning a fresh install on the current long-term version? The guide Set up a LEMP stack on Ubuntu 26.04 LTS documents Nginx with native HTTP/3 and QUIC, MariaDB 11.4 and PHP 8.5.
The LEMP stack is one of the most proven and resource-efficient architectures for running dynamic web applications on Linux. Linux forms the stable operating-system foundation, Nginx acts as a high-performance asynchronous web server and reverse proxy, MariaDB provides relational database management and PHP runs the server-side application logic.
Compared with classic LAMP architectures (Apache with mod_php), the LEMP stack is marked by strict process decoupling. Nginx handles static requests through an event-driven non-blocking architecture and delegates dynamic script calls over Unix domain sockets to the FastCGI Process Manager (php-fpm). That cuts memory use under parallel connections drastically and protects the overall system from load spikes.
The following setup builds a hardened, production-ready LEMP stack on Ubuntu 24.04 LTS (Noble Numbat). It covers system hardening with UFW and Fail2ban, performance tuning of MariaDB and PHP 8.3-FPM, and encrypted delivery through Let's Encrypt with modern TLS 1.3.
π‘ Long-term support (LTS): Ubuntu 24.04 LTS guarantees official security updates and maintained package states until April 2029. In production this predictable five-year cycle provides high operational stability with minimal maintenance effort.
β οΈ Prerequisites: Administrative access (
sudo) to a clean Ubuntu 24.04 LTS installation, basic shell-administration knowledge and a publicly resolvable DNS record (A/AAAA) for your domain if a Let's Encrypt TLS certificate is to be issued.
System requirements and component matrix
Before packages are installed, server hardware should be matched to the planned workload. Nginx is extremely frugal, while relational database queries and parallel PHP workers have a direct effect on RAM and I/O performance.
| Component | Package version in Ubuntu 24.04 | Default port / socket | Minimum | Recommended for production |
|---|---|---|---|---|
| Operating system | Ubuntu 24.04 LTS | - | 1 vCPU, 1 GB RAM | 2β4 vCPUs, 4β8 GB RAM |
| Nginx | 1.24+ | TCP 80, 443 | 128 MB RAM | 512 MB RAM |
| MariaDB | 10.11 LTS | TCP 3306 (127.0.0.1) | 512 MB RAM | 2 to 4 GB RAM (InnoDB buffer) |
| PHP-FPM | 8.3 | /run/php/php8.3-fpm.sock |
256 MB RAM | 1 to 2 GB RAM (for workers) |
| Storage | NVMe / SSD | - | 10 GB free | 25+ GB with dedicated backup |
1. System preparation and baseline hardening
Every clean server deployment starts with an updated package index and a restrictive firewall configuration. That prevents unprotected default services from being reachable directly from the internet during setup.
Refresh package sources and clean up
First we bring the local package cache up to date and install pending kernel and security updates:
# Update package lists and upgrade installed packages
sudo apt update && sudo apt upgrade -y
# Remove unused leftover dependencies without residue
sudo apt autoremove --purge -y && sudo apt autoclean
Diagnosis and resource check:
Before installing larger server services we verify available disk space, memory and system time:
# Check disk space on the root partition
df -h /
# Check RAM availability and swap status
free -h
# Ensure time synchronization via NTP
timedatectl status
Firewall (UFW) and brute-force protection (Fail2ban)
Uncomplicated Firewall (UFW) manages the kernel packet filter (nftables) with clear default policies: all incoming traffic is blocked, outgoing connections are allowed.
# Install UFW and Fail2ban
sudo apt install ufw fail2ban -y
# Define default policies
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Unlock SSH before enabling (port 22)
sudo ufw allow OpenSSH
# Enable the firewall and check status
sudo ufw --force enable
sudo ufw status verbose
β οΈ Critical pitfall: Never enable UFW without first explicitly allowing the SSH service (
sudo ufw allow OpenSSHorsudo ufw allow 22/tcp). Otherwise you lose remote access immediately with the enable command and need your hoster's emergency console.
# Check existing web-server services (e.g. Apache leftovers)
sudo ss -tuln | grep -E ':(80|443)' || echo "Ports 80 and 443 are free"
2. Install Nginx as the web server
Nginx acts as the frontend of our stack. It accepts incoming TCP connections, terminates TLS, serves static assets (CSS, JavaScript, images) directly and forwards dynamic PHP requests to the FPM socket.
Install Nginx and extend the firewall rule
# Install Nginx from the official Ubuntu repositories
sudo apt install nginx -y
# Register the service for automatic start and start it
sudo systemctl enable nginx
sudo systemctl start nginx
# Allow HTTP (port 80) and HTTPS (port 443) in the firewall
sudo ufw allow 'Nginx Full'
sudo ufw status
Verify the operating state:
# Check the running status of the systemd unit
systemctl status nginx --no-pager
# Test the local HTTP response
curl -I http://127.0.0.1
The response must return status code HTTP/1.1 200 OK together with the header Server: nginx/....
β οΈ Avoid port conflicts: If Nginx does not start after installation, a preinstalled Apache service (
apache2) often blocks port 80. Check withsudo ss -tuln | grep :80. If Apache is active, disable it withsudo systemctl stop apache2 && sudo systemctl disable apache2.
Nginx directory structure
On Debian and Ubuntu, Nginx follows a modular layout that separates configuration templates (sites-available) from active instances (sites-enabled) via symbolic links:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Nginx directory structure β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β /etc/nginx/ β
β βββ nginx.conf Global main configuration β
β βββ conf.d/ Modular server configurations β
β βββ sites-available/ VHost templates (inactive) β
β βββ sites-enabled/ Active VHost symlinks β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
nginx.conf: Global parameters such as worker processes, event models, gzip compression and logging formats.sites-available/: Configuration files for individual virtual hosts (domains).sites-enabled/: Contains symlinks to files insites-available/. Only VHosts in this folder are actively loaded by Nginx.conf.d/: Global modular configuration snippets for upstream pools or security policies.
3. Provide the MariaDB database server
As the relational database system we use MariaDB 10.11 LTS. MariaDB offers full MySQL compatibility, but stands out through the modern InnoDB storage engine, improved thread pools and high performance on web workloads.
Installation and automatic start
# Install MariaDB server and client tools
sudo apt install mariadb-server mariadb-client -y
# Enable the service and check status
sudo systemctl enable mariadb
sudo systemctl status mariadb --no-pager
Hardening with mariadb-secure-installation
Immediately after installation the database contains insecure defaults (anonymous user accounts, a remotely accessible root account and a public test database). We clean these weaknesses:
# Run the interactive security wizard
sudo mariadb-secure-installation
Recommended answers for production:
- Enter current password for root: press
Enter(by default no password is set). - Switch to unix_socket authentication [Y/n]:
Y(allows root login via system root without a plaintext password). - Change the root password? [Y/n]:
Y(choose a complex, long password and store it externally). - Remove anonymous users? [Y/n]:
Y(remove anonymous accounts completely). - Disallow root login remotely? [Y/n]:
Y(restrict root access strictly tolocalhost). - Remove test database and access to it? [Y/n]:
Y(delete test tables). - Reload privilege tables now? [Y/n]:
Y(reload privilege tables immediately).
π‘ Socket authentication on Linux: Ubuntu uses the
unix_socketplugin for the MariaDB root account by default. That means: the system userrootcan log in withsudo mariadbdirectly without a password. An external root password is needed mainly when third-party tools without sudo rights request local administrative access.
MariaDB directory structure and tuning
Configuration is organized modularly through /etc/mysql/:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MariaDB directory structure β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β /etc/mysql/ β
β βββ my.cnf Central inclusion symlink β
β βββ mariadb.conf.d/ Server configurations β
β βββ 50-server.cnf Main daemon configuration β
β βββ 60-galera.cnf Cluster and replication profiles β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
To optimize for production web servers we adjust InnoDB resource allocation in /etc/mysql/mariadb.conf.d/50-server.cnf:
# Open the configuration for editing
sudo nano /etc/mysql/mariadb.conf.d/50-server.cnf
Core parameters in the [mysqld] section:
[mysqld]
# Restrict network binding strictly to localhost
bind-address = 127.0.0.1
# Performance and InnoDB tuning
innodb_buffer_pool_size = 512M # At 2 GB RAM; at 4 GB+ set to 50-70% of RAM
innodb_log_file_size = 128M # Writes transactions efficiently to the log
innodb_flush_log_at_trx_commit = 2 # Reduces disk I/O under write load
innodb_file_per_table = 1 # Creates its own .ibd files per table
max_connections = 100 # Prevents overload from too many clients
Explanation of the tuning parameters:
| Parameter | Recommended value | Effect in server operation |
|---|---|---|
bind-address |
127.0.0.1 |
Protects MariaDB from direct network access from outside |
innodb_buffer_pool_size |
50β70% of free RAM | Holds table data and indexes in fast memory |
innodb_flush_log_at_trx_commit |
2 |
Writes logs to disk once per second; drastic I/O gain |
max_connections |
50β150 |
Limits concurrent connections and prevents OOM crashes |
After the change we restart the service:
# Reload the MariaDB configuration
sudo systemctl restart mariadb
4. Install and tune PHP 8.3-FPM
PHP is provided on Ubuntu 24.04 in version 8.3. The FastCGI Process Manager (php-fpm) runs as a standalone system service and manages worker processes that compile and execute PHP files.
Install PHP-FPM and core modules
Modern web applications (such as WordPress, Nextcloud or Laravel) need a set of specialized PHP extensions besides the FPM service:
# Install PHP 8.3-FPM plus database, image and optimization modules
sudo apt install -y php8.3-fpm php8.3-common php8.3-mysql php8.3-xml php8.3-curl php8.3-gd php8.3-mbstring php8.3-zip php8.3-opcache php8.3-intl
Check service status and modules:
# Check the PHP-FPM status
sudo systemctl status php8.3-fpm --no-pager
# List installed PHP version and active extensions
php -v
php -m | grep -E '(mysqli|pdo_mysql|opcache|curl)'
β οΈ Component dependency: Make sure the
php8.3-fpmpackage is installed. If you accidentally install only thephpmetapackage, Ubuntu pulls Apache as a dependency and starts a colliding web server.
PHP 8.3 directory structure
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PHP 8.3 directory structure β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β /etc/php/8.3/ β
β βββ fpm/ β
β β βββ php-fpm.conf Global master settings β
β β βββ php.ini FPM runtime configuration β
β β βββ pool.d/ Worker pools β
β β βββ www.conf Default pool (www-data socket) β
β βββ cli/ β
β βββ php.ini CLI runtime configuration β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Worker-pool tuning in www.conf
The file /etc/php/8.3/fpm/pool.d/www.conf controls how PHP-FPM spawns processes and accepts requests. By default Ubuntu uses the dynamic mode:
; /etc/php/8.3/fpm/pool.d/www.conf
[www]
user = www-data
group = www-data
; Communication via local Unix-domain socket
listen = /run/php/php8.3-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
; Process management
pm = dynamic
pm.max_children = 25
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 8
pm.max_requests = 500
Rules of thumb for calculating pm.max_children:
# Determine average RAM use of active PHP-FPM workers (in MB)
ps --no-headers -o rss -C php-fpm8.3 | awk '{total+=$1; count++} END {if (count>0) print int(total/count/1024) " MB"; else print "60 MB (default guideline)"}'
# Rule of thumb for pm.max_children:
# max_children = (available RAM for PHP in MB) / (average RAM per worker in MB)
# Calculation example: 2048 MB assigned RAM at about 60 MB per worker
echo $(( 2048 / 60 ))
# Result: 34
| Directive | Typical value | Function |
|---|---|---|
pm.max_children |
20β50 |
Maximum number of concurrent worker processes |
pm.start_servers |
4β8 |
Number of workers created at service start |
pm.min_spare_servers |
2β4 |
Minimum reserve of idle workers at idle |
pm.max_spare_servers |
6β12 |
Maximum reserve of idle workers |
pm.max_requests |
500 |
Recycles workers after 500 requests to prevent memory leaks |
# After a configuration change, reload the FPM service
sudo systemctl reload php8.3-fpm
5. Integrating Nginx and PHP-FPM
After Nginx and PHP-FPM are operational, we configure a virtual host (server block) that forwards PHP requests to the Unix socket /run/php/php8.3-fpm.sock.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Nginx and PHP-FPM request flow β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Client request (HTTP port 80 / HTTPS port 443) β
β βΌ β
β Nginx web server β
β βββ Static files βββΆ Served directly β
β βββ location ~ \.php$ βββΆ FastCGI proxy pass β
β β β
β βΌ β
β Socket: php8.3-fpm.sock β
β β β
β βΌ β
β PHP-FPM worker process β
β β β
β βΌ β
β Client response βββ HTML / JSON response β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Creating the server block
We disable the default configuration and create a clean virtual host under /etc/nginx/sites-available/lemp.conf:
# Create the new server-block configuration
sudo nano /etc/nginx/sites-available/lemp.conf
Configuration content:
server {
listen 80;
listen [::]:80;
server_name your-domain.example www.your-domain.example;
root /var/www/html;
index index.php index.html index.htm;
# Security headers against clickjacking and MIME sniffing
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Static files and directory lookups
location / {
try_files $uri $uri/ =404;
}
# Forward dynamic scripts to the PHP 8.3 FastCGI Unix socket
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
# Buffer tuning for dynamic responses
fastcgi_buffer_size 128k;
fastcgi_buffers 4 256k;
fastcgi_busy_buffers_size 256k;
}
# Strictly block access to hidden files (.git, .env, .htaccess)
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}
Enable the VHost and validate syntax
# Enable the VHost via symlink
sudo ln -s /etc/nginx/sites-available/lemp.conf /etc/nginx/sites-enabled/
# Disable the outdated default VHost
sudo rm -f /etc/nginx/sites-enabled/default
# Check Nginx syntax before reload
sudo nginx -t
# Reload Nginx configuration without downtime
sudo systemctl reload nginx
β οΈ FastCGI pitfall: If the browser shows
502 Bad Gateway, in 95 % of cases the cause is a wrong socket path or permission problems. Check withls -la /run/php/php8.3-fpm.sockwhether the socket exists and belongs towww-data:www-data.
6. End-to-end validation: database and web stack
We verify the interaction of all components with a real test database and a secure PHP script using prepared statements via PDO.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β End-to-end test: web, app and database β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β HTTP GET /db-test.php β
β βΌ β
β Nginx server block β
β βΌ β
β PHP 8.3-FPM (Unix domain socket) β
β βΌ β
β PDO MySQL connection βββββΆ MariaDB (127.0.0.1:3306) β
β βββ UTF8mb4 connection β
β βββ Prepared statement β
β βββ Process test data β
β βΌ β
β Status response ββββββββββΆ 200 OK (text/HTML) β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Creating a dedicated test user in MariaDB
# Log into the MariaDB console
sudo mariadb -u root
Run the SQL commands:
CREATE DATABASE lemp_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'lemp_user'@'localhost' IDENTIFIED BY 'AVerySecretPassword123!';
GRANT ALL PRIVILEGES ON lemp_test.* TO 'lemp_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;
Creating a secure PDO test script
We place the test script at /var/www/html/db-test.php:
sudo nano /var/www/html/db-test.php
<?php
// /var/www/html/db-test.php - For verification purposes only!
declare(strict_types=1);
$dbConfig = [
'host' => '127.0.0.1',
'dbname' => 'lemp_test',
'user' => 'lemp_user',
'pass' => 'AVerySecretPassword123!',
'charset' => 'utf8mb4'
];
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4'
];
try {
$dsn = "mysql:host={$dbConfig['host']};dbname={$dbConfig['dbname']};charset={$dbConfig['charset']}";
$pdo = new PDO($dsn, $dbConfig['user'], $dbConfig['pass'], $options);
// Create the test table
$pdo->exec("CREATE TABLE IF NOT EXISTS health_check (
id INT AUTO_INCREMENT PRIMARY KEY,
checked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)");
// Insert a test row via prepared statement
$stmt = $pdo->prepare("INSERT INTO health_check (checked_at) VALUES (NOW())");
$stmt->execute();
// Fetch the number of rows
$count = (int) $pdo->query("SELECT COUNT(*) FROM health_check")->fetchColumn();
header('Content-Type: text/plain; charset=utf-8');
echo "LEMP-Stack Health Check: OK
";
echo "PHP Version: " . PHP_VERSION . "
";
echo "MariaDB connection: Successful
";
echo "Rows in health_check: {$count}
";
} catch (PDOException $e) {
http_response_code(500);
header('Content-Type: text/plain; charset=utf-8');
echo "LEMP-Stack Health Check: ERROR
";
echo "Reason: " . $e->getMessage() . "
";
}
# Set file permissions for the web-server user
sudo chown www-data:www-data /var/www/html/db-test.php
sudo chmod 640 /var/www/html/db-test.php
# Run the test via the local CLI
curl -s http://localhost/db-test.php
The output confirms correct execution:
LEMP-Stack Health Check: OK
PHP Version: 8.3.x
MariaDB connection: Successful
Rows in health_check: 1
β οΈ Test-script security risk: Delete
db-test.phpimmediately after the function test (sudo rm -f /var/www/html/db-test.php). Public scripts that reveal database structure or connection details are an avoidable entry point for attackers.
7. TLS encryption with Let's Encrypt and Nginx hardening
A production web server today may only be operated over encrypted HTTPS connections. We automate issuance and renewal of TLS certificates with the EFF tool Certbot.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Production HTTPS request flow β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Client browser (HTTPS port 443) β
β βΌ β
β TLS 1.3 handshake and Let's Encrypt certificate check β
β βΌ β
β Nginx web server with HSTS and security headers β
β βΌ β
β FastCGI Unix socket (/run/php/php8.3-fpm.sock) β
β βΌ β
β PHP-FPM worker process βββΆ MariaDB (127.0.0.1:3306) β
β βΌ β
β Gzip compression βββββββββΆ Response to client browser β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Install Certbot and issue a certificate
The official and best-maintained version of Certbot is obtained through the Snap package-management system:
# Ensure snapd and install Certbot classically
sudo snap install core && sudo snap refresh core
sudo snap install --classic certbot
# Create a symlink in the standard path
sudo ln -sf /snap/bin/certbot /usr/bin/certbot
# Request a certificate and configure the VHost for HTTPS automatically
sudo certbot --nginx -d your-domain.example -d www.your-domain.example
Certbot asks for an email address for emergency notifications, verifies the DNS record through a temporary HTTP-01 challenge, automatically adjusts the server block /etc/nginx/sites-available/lemp.conf to port 443 (SSL) and sets up a 301 redirect from HTTP to HTTPS.
Extended TLS hardening and HSTS
For an A+ rating at SSL Labs we add modern TLS protocols and HTTP Strict Transport Security (HSTS) in the SSL server block:
# Addition in the server block (port 443)
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m;
ssl_session_tickets off;
# HSTS (enforces 2 years of HTTPS in the browser including subdomains)
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# Validate the configuration and reload Nginx
sudo nginx -t && sudo systemctl reload nginx
# Test automatic certificate renewal in a dry run
sudo certbot renew --dry-run
π‘ Automatic renewal timer: On Snap installation Certbot automatically sets up a systemd timer (
snap.certbot.renew.timer). It checks twice daily whether certificates expire within the next 30 days and renews them fully automatically.
Command Reference (Cheatsheet)
The most important operational commands for management, monitoring and fault diagnosis of the LEMP stack at a glance:
| Service / task | Command | Explanation |
|---|---|---|
| Packages and system | sudo apt update && sudo apt upgrade -y |
Updates package sources and installed packages |
| Firewall (UFW) | sudo ufw status verbose |
Shows active rules, default policies and ports |
| Firewall (UFW) | sudo ufw allow 'Nginx Full' |
Opens HTTP (port 80) and HTTPS (port 443) |
| Nginx syntax | sudo nginx -t |
Checks configuration files for syntax and path errors |
| Nginx reload | sudo systemctl reload nginx |
Applies configuration changes without interruption |
| MariaDB shell | sudo mariadb -u root -p |
Opens the administrative database console |
| MariaDB status | sudo systemctl status mariadb |
Checks process status and uptime of the database server |
| PHP-FPM status | sudo systemctl status php8.3-fpm |
Checks the status of the PHP process manager |
| PHP-FPM socket | ls -la /run/php/php8.3-fpm.sock |
Verifies existence and permission (www-data) of the socket |
| TLS certificate | sudo certbot --nginx -d your-domain.example |
Obtains and installs Let's Encrypt TLS certificates |
| TLS renewal | sudo certbot renew --dry-run |
Simulates automatic renewal of all certificates |
| Network ports | sudo ss -tuln | grep -E ':(80|443|3306)' |
Checks bound ports of Nginx and MariaDB |
Further Resources
The following internal documentation and references go deeper into safe operation, hardening and system management:
| Resource / documentation | Description |
|---|---|
| Ubuntu upgrade: 22.04 to 24.04 LTS | Fundamentals of the LTS release cycle, in-place upgrades and kernel features |
| LEMP stack on Ubuntu 26.04 LTS | Follow-up guide for Ubuntu 26.04 with Nginx HTTP/3, MariaDB 11.4 and PHP 8.5 |
| Linux server hardening: SSH and firewall | Further hardening of the server with SSH-key enforcement, CrowdSec and Fail2ban |
| Virtual machines for tests | Isolated test environments for staging web servers under KVM and Proxmox VE |
| Docker and container workloads on Linux | Container-based deployment of web applications and microservices |
| Ubuntu server environments | Overview of all guides for administration, maintenance and operation of Linux servers |
| Official Nginx documentation | Reference handbook for all modules, upstream directives and tuning parameters |
| Official MariaDB documentation | Handbook for InnoDB engine, replication and user privileges |
| Official PHP-FPM documentation | Guide to process managers, pool directives and OPcache configuration |
Conclusion
With the LEMP stack built here, Ubuntu 24.04 LTS has a performant, resource-efficient and hardened platform for modern web applications. Through the interplay of Nginx as an asynchronous web server, PHP 8.3-FPM over local Unix domain sockets and a hardened MariaDB 10.11 LTS server, all components are cleanly decoupled. The combination of strict UFW firewall rules, Fail2ban and automated Let's Encrypt encryption provides a stable security foundation in production.
π‘ Practical tip for production: Watch the load of your PHP-FPM workers in
/etc/php/8.3/fpm/pool.d/www.conf. As visitor numbers rise,pm.max_children,pm.start_serversandpm.max_spare_serversshould be adjusted to available memory to prevent worker bottlenecks (server reached max_children setting). Also set OPcache inphp.inito at least 128 MB so that bytecode of compiled PHP scripts stays in RAM and latency drops sharply.
For long-term operation it is advisable to extend the platform step by step with central monitoring and automated log rotation. Next you can further expand your web-server infrastructure with advanced security measures such as CrowdSec or a multi-VHost architecture.