Install a LEMP Stack on Ubuntu 24.04: Nginx, MariaDB and PHP-FPM

LEMP stands for Linux, Nginx (pronounced "engine-x"), MariaDB or MySQL, and PHP. It runs most PHP applications: WordPress, Laravel, billing panels and custom code. This guide builds the stack on a clean Ubuntu 24.04 VPS using the packages Ubuntu ships, then covers the errors almost everyone meets on day one.
You need a VPS with root or sudo access, a domain whose A record points to the server's IP address, and about 20 minutes. The commands assume a fresh install. Replace example.com with your own domain inside the config files.
What Ubuntu 24.04 gives you
Distribution packages keep maintenance simple: one apt upgrade patches the whole stack, and security fixes arrive through Ubuntu. On Ubuntu 24.04 the default versions are:
| Component | Package | Version in 24.04 |
|---|---|---|
| Nginx | nginx | 1.24 |
| MariaDB | mariadb-server | 10.11 (LTS) |
| PHP | php-fpm | 8.3 |
If your application needs a different PHP version, third-party repositories exist. Each one is another source you have to trust and track, so start with the defaults unless you have a concrete reason not to.
Step 1: Update the system
sudo apt update && sudo apt upgrade -y
If the upgrade installs a new kernel, reboot before you continue. It is easier to find problems on a clean, current system.
Step 2: Install Nginx and open the firewall
sudo apt install -y nginx
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status
Allow OpenSSH before you enable UFW. If you enable the firewall first, you can lock yourself out of your own server. The Nginx Full profile opens ports 80 and 443.
Open the server's IP address in a browser. The "Welcome to nginx!" page means Nginx is running. If it does not load, check systemctl status nginx and confirm that port 80 is allowed.
Step 3: Install and secure MariaDB
sudo apt install -y mariadb-server
sudo mariadb-secure-installation
On Ubuntu, the MariaDB root account authenticates through the Unix socket. Only the system root user (or sudo) can log in as database root, and no password is involved. That is a safe default, so keep it: answer n to switching to unix_socket authentication (it is already on) and to changing the root password. Answer Y to removing anonymous users, disallowing remote root login, removing the test database and reloading privileges.
Never let your application connect as root. Create a dedicated database and user:
sudo mariadb
CREATE DATABASE appdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'appuser'@'localhost' IDENTIFIED BY 'use-a-long-random-password';
GRANT ALL PRIVILEGES ON appdb.* TO 'appuser'@'localhost';
FLUSH PRIVILEGES;
EXIT;
utf8mb4 stores the full Unicode range, including Arabic text and emoji. The older utf8 character set in MariaDB is a 3-byte subset and can cut some characters. 'localhost' means the user can only connect from the server itself. MariaDB on Ubuntu listens on 127.0.0.1 by default, so port 3306 is not exposed to the internet. Keep it that way unless another server needs access.
Step 4: Install PHP-FPM
sudo apt install -y php-fpm php-mysql php-cli php-curl php-mbstring php-xml php-zip php-gd php-intl
php -v
systemctl status php8.3-fpm
Do not install the bare php meta-package or libapache2-mod-php. On Ubuntu, php can pull in Apache, which then competes with Nginx for port 80.
PHP-FPM runs PHP as a separate pool of worker processes. Nginx does not execute PHP itself; it passes PHP requests to FPM over a Unix socket, by default /run/php/php8.3-fpm.sock. This split is why LEMP handles many visitors with modest memory: Nginx serves images, CSS and JavaScript directly, and only PHP requests use a worker.
Step 5: Create the site and the server block
sudo mkdir -p /var/www/example.com/public
sudo chown -R $USER:www-data /var/www/example.com
sudo nano /etc/nginx/sites-available/example.com
Paste this configuration and adjust the domain and paths:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/example.com/public;
index index.php index.html;
client_max_body_size 64M;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
location ~ /\.(?!well-known) {
deny all;
}
}
What each part does:
try_files serves a file if it exists, otherwise sends the request to
index.php. WordPress and Laravel need this for clean URLs.snippets/fastcgi-php.conf ships with Ubuntu's Nginx. It sets the script path for PHP and returns 404 for PHP files that do not exist.
fastcgi_pass must match the FPM socket exactly. A wrong version number here is the most common cause of 502 errors.
The last block blocks hidden files such as
.envand.git, while still allowing.well-knownfor SSL validation.
Enable the site, remove the default one, test and reload:
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx
Always run nginx -t before a reload. A reload with a broken config is refused, but the test tells you which line is wrong. A reload also keeps existing connections open, unlike a restart.
Step 6: Test PHP
echo '<?php phpinfo();' | sudo tee /var/www/example.com/public/info.php
# after checking it in the browser:
sudo rm /var/www/example.com/public/info.php
Open http://example.com/info.php. You should see the PHP information page with "Server API: FPM/FastCGI". Delete the file right after: it exposes paths, versions and settings that help an attacker.
Fix the common errors
502 Bad Gateway
Nginx could not reach PHP-FPM. Read the error log first; it usually names the cause:
sudo tail -n 20 /var/log/nginx/error.log
# connect() to unix:/run/php/php8.3-fpm.sock failed (2: No such file or directory)
systemctl status php8.3-fpm
ls -l /run/php/
Either FPM is not running, or the socket path in fastcgi_pass does not match the file in /run/php/. This often happens after copying a config written for an older PHP version.
"File not found." or "Primary script unknown"
FPM received the request but could not find the script. The root path is wrong, the file is missing, or the www-data user cannot read it. namei -l /var/www/example.com/public/index.php shows the permissions on every directory in the path.
The browser downloads the PHP file
The request did not match the PHP location block, so Nginx served the file as plain data. Check that your site is enabled and that no other server block is answering for the domain: sudo nginx -T | grep server_name.
413 Request Entity Too Large
Nginx rejects request bodies larger than 1 MB by default. client_max_body_size in the server block raises it. PHP has its own limits: upload_max_filesize and post_max_size in /etc/php/8.3/fpm/php.ini. Set all three to the same value and restart php8.3-fpm after editing php.ini.
Size PHP-FPM workers to your RAM
The default pool allows only 5 PHP workers. Under real traffic you will see "server reached pm.max_children setting (5), consider raising it" in /var/log/php8.3-fpm.log, and visitors wait. Set too high, the workers exhaust RAM and the kernel starts killing processes, often MariaDB first.
A rule of thumb: max_children = RAM you can give PHP ÷ average worker size. Measure the average worker size under normal load:
ps -o rss= -C php-fpm8.3 | awk '{s+=$1; n++} END {print s/n/1024 " MB"}'
Example: on a 2 GB VPS, the OS and MariaDB might keep about 800 MB, which leaves about 1.2 GB. With workers at 60 MB, that is 20 children; leave headroom and start at 15. Then watch memory with free -h during busy hours and adjust.
; /etc/php/8.3/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 15
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500
sudo systemctl restart php8.3-fpm
sudo grep max_children /var/log/php8.3-fpm.log
pm.max_requests recycles each worker after 500 requests, which contains slow memory leaks in plugins. The OPcache extension is enabled by default on Ubuntu and caches compiled PHP code; leave it on.
Next steps
HTTPS: Let's Encrypt certificates are free and Certbot edits the Nginx config for you:
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
Backups: dump the database with
mariadb-dumpon a schedule and copy the dumps off the server.Updates: enable
unattended-upgradesfor security patches, and review larger upgrades yourself.
Run it on IM HOST
A LEMP stack needs full root access and predictable resources. IM HOST Linux VPS plans start from $5/month with full root on KVM, a dedicated IPv4 plus an IPv6 /64, and DDoS protection. If you want dedicated vCPU and RAM with automatic backups every 3 days, look at Cloud VPS. Moving an existing site? Our free migration service moves files, databases and email with no downtime. There are no setup fees, support engineers are available 24/7, and every plan has a 30-day money-back guarantee.
More from our blog
Discover more practical guides and product insights from the IM Host team.
View all articles