A LEMP stack combines Linux, Nginx (pronounced "engine-x"), MySQL and PHP. Unlike Apache with mod_php, Nginx does not run PHP itself: it passes PHP requests to PHP-FPM over a Unix socket, which keeps memory use low and scales well under load. In this tutorial you will install Nginx, MySQL 8.0 and PHP-FPM 8.3 on Ubuntu 24.04, configure a server block for your domain and verify that PHP can query the database.
Prerequisites
To follow this tutorial you need:
- A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with at least 1 GB of RAM.
- A non-root user with
sudoprivileges. - Optionally, a domain name with an A record pointing to your server's public IP. This tutorial uses
your_domainas a placeholder. Without a domain, useyour_server_ipinstead.
Step 1 - Installing Nginx and allowing web traffic
Refresh the package index and install Nginx from Ubuntu's repositories:
sudo apt update
sudo apt install nginx
Nginx starts automatically after installation. Check that it is running:
sudo systemctl status nginx --no-pager
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: enabled)
Active: active (running) since ...
If UFW is enabled, allow SSH and the Nginx Full profile, which opens ports 80 and 443:
sudo ufw allow OpenSSH
sudo ufw allow "Nginx Full"
sudo ufw enable
Verify the rules:
sudo ufw status
Status: active
To Action From
-- ------ ----
OpenSSH ALLOW Anywhere
Nginx Full ALLOW Anywhere
...
Open http://your_server_ip in a browser. You should see the "Welcome to nginx!" page. From the server itself:
curl -sI http://localhost | head -n 1
HTTP/1.1 200 OK
Step 2 - Installing MySQL
Install MySQL server. Ubuntu 24.04 ships MySQL 8.0:
sudo apt install mysql-server
Confirm the service is active:
sudo systemctl is-active mysql
active
Run the security script to remove anonymous users, disallow remote root login and drop the test database:
sudo mysql_secure_installation
Answer Y to those questions. You can also enable the password validation component, which rejects weak passwords for new users. On Ubuntu the MySQL root account uses the auth_socket plugin, so the script skips setting a root password: you log in as MySQL root with sudo mysql and no password.
Check that the console opens:
sudo mysql -e "SELECT VERSION();"
+-------------------------+
| VERSION() |
+-------------------------+
| 8.0.x-0ubuntu0.24.04.x |
+-------------------------+
Step 3 - Installing PHP-FPM
Install PHP-FPM and the MySQL extension. Note that you install php-fpm rather than the php meta package: on Ubuntu, php pulls in Apache and mod_php, which you don't want on an Nginx server.
sudo apt install php-fpm php-mysql
Add the extensions that most applications expect:
sudo apt install php-curl php-gd php-mbstring php-xml php-zip php-intl
Ubuntu 24.04 installs PHP 8.3. Check the CLI version and the FPM service:
php -v
sudo systemctl status php8.3-fpm --no-pager
PHP 8.3.6 (cli) (built: ...) (NTS)
...
● php8.3-fpm.service - The PHP 8.3 FastCGI Process Manager
Loaded: loaded (/usr/lib/systemd/system/php8.3-fpm.service; enabled; preset: enabled)
Active: active (running) since ...
PHP-FPM listens on a Unix socket that Nginx will use. Confirm it exists:
ls -l /run/php/
-rw-r--r-- 1 root root ... php8.3-fpm.pid
srw-rw---- 1 www-data www-data 0 ... php8.3-fpm.sock
lrwxrwxrwx 1 root root ... php-fpm.sock -> /etc/alternatives/php-fpm.sock
The socket is owned by www-data, the same user Nginx runs as, so Nginx can connect to it without further changes.
Step 4 - Configuring an Nginx server block
Create a document root for your site and give your user ownership of it:
sudo mkdir -p /var/www/your_domain
sudo chown -R $USER:$USER /var/www/your_domain
Create a new server block file:
sudo nano /etc/nginx/sites-available/your_domain
Add this configuration, replacing your_domain:
server {
listen 80;
listen [::]:80;
server_name your_domain www.your_domain;
root /var/www/your_domain;
index index.php index.html;
access_log /var/log/nginx/your_domain.access.log;
error_log /var/log/nginx/your_domain.error.log;
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 ~ /\.ht {
deny all;
}
}
What each part does:
try_filesserves a file or directory if it exists and otherwise hands the request toindex.php, the front controller pattern used by WordPress, Laravel and Symfony.snippets/fastcgi-php.confships with Ubuntu's Nginx package. It setsSCRIPT_FILENAMEand returns 404 for PHP files that don't exist, so arbitrary paths are never sent to PHP.fastcgi_passpoints to the PHP-FPM socket from Step 3.- The last block hides any
.htaccessfiles left over from Apache-oriented projects.
Enable the site by linking it into sites-enabled, and remove the default site so it does not answer for your domain:
sudo ln -s /etc/nginx/sites-available/your_domain /etc/nginx/sites-enabled/
sudo unlink /etc/nginx/sites-enabled/default
Test the configuration and reload Nginx:
sudo nginx -t
sudo systemctl reload nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Step 5 - Testing PHP with Nginx
Create a test file in the document root:
nano /var/www/your_domain/info.php
Add:
<?php phpinfo();
Open http://your_domain/info.php. The page should show Server API: FPM/FastCGI. From the command line:
curl -s http://your_domain/info.php | grep -o 'FPM/FastCGI' | head -n 1
FPM/FastCGI
Delete the file afterwards, because it reveals details about your server:
rm /var/www/your_domain/info.php
Step 6 - Testing the database connection from PHP
Create a database and a dedicated user. Open the MySQL console:
sudo mysql
Run the following statements, replacing your_strong_password with a strong password of your own:
CREATE DATABASE example_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'example_user'@'localhost' IDENTIFIED BY 'your_strong_password';
GRANT ALL PRIVILEGES ON example_db.* TO 'example_user'@'localhost';
USE example_db;
CREATE TABLE todo_list (id INT AUTO_INCREMENT PRIMARY KEY, content VARCHAR(255));
INSERT INTO todo_list (content) VALUES ('Install Nginx'), ('Install MySQL'), ('Install PHP-FPM');
EXIT;
Create a PHP script that reads the table:
nano /var/www/your_domain/todo.php
Add the following code with the same password:
<?php
$pdo = new PDO(
'mysql:host=localhost;dbname=example_db;charset=utf8mb4',
'example_user',
'your_strong_password',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
foreach ($pdo->query('SELECT id, content FROM todo_list') as $row) {
echo $row['id'] . '. ' . $row['content'] . PHP_EOL;
}
Request the page:
curl http://your_domain/todo.php
1. Install Nginx
2. Install MySQL
3. Install PHP-FPM
The stack is working end to end. Remove the script, which contains a password:
rm /var/www/your_domain/todo.php
Step 7 - Enabling HTTPS with Let's Encrypt (optional)
With your domain pointing to the server, install Certbot and its Nginx plugin:
sudo apt install certbot python3-certbot-nginx
Request a certificate. Certbot updates the server block with the certificate paths and an HTTP to HTTPS redirect:
sudo certbot --nginx -d your_domain -d www.your_domain
Renewal runs automatically from a systemd timer. Confirm it works with a dry run:
sudo certbot renew --dry-run
Troubleshooting
- 502 Bad Gateway on PHP pages. Nginx cannot reach PHP-FPM. Check that
php8.3-fpmis running (sudo systemctl status php8.3-fpm) and that the socket path infastcgi_passmatches the file in/run/php/. The exact error appears in/var/log/nginx/your_domain.error.log. - PHP files are downloaded instead of executed. The request did not match the
location ~ \.php$block. Make sure the block is inside the rightserverand that you reloaded Nginx. nginx -treports a duplicate default server. Two server blocks usedefault_server. Remove the default site link as shown in Step 4.Access denied for userfrom PDO. The database credentials in the script don't match the user you created. Reset the password fromsudo mysqlwithALTER USER.
Conclusion
You installed Nginx, MySQL 8.0 and PHP-FPM 8.3 on Ubuntu 24.04, configured a server block that passes PHP requests to PHP-FPM and confirmed that PHP can read from the database. Next, you can tune PHP-FPM pools for your workload, install an application such as WordPress with WP-CLI, or run several PHP versions side by side for different sites.
