A LEMP stack combines Linux, Nginx, MariaDB or MySQL, and PHP-FPM to run PHP websites and applications. On Ubuntu 24.04, you can build the stack from the standard Ubuntu repositories without adding third-party package sources. This tutorial sets up a production-oriented LEMP stack on a Raff Linux VM, creates a dedicated local database user, connects Nginx to PHP-FPM, verifies PHP execution, adds a basic firewall and HTTPS path, and finishes with end-to-end checks and cleanup instructions.
The current Ubuntu 24.04 package line uses PHP 8.3, and Ubuntu's Noble package repository provides MariaDB 10.11. You can verify those package baselines in the official Ubuntu PHP package listing and Ubuntu MariaDB package listing. This guide uses MariaDB, but the same LEMP architecture can use MySQL when your application requires it.
You need an Ubuntu 24.04 server with at least 2 GB RAM, SSH access, and a non-root user with sudo privileges. A domain is optional for the initial stack test but required for the HTTPS step.
Step 1 — Update Ubuntu and Confirm the Server Baseline
Update package metadata and install available updates:
sudo apt update sudo apt upgrade -y
Check the operating system release:
cat /etc/os-release
If the upgrade reports that a reboot is required, reboot and reconnect before continuing:
sudo reboot
Verify: /etc/os-release should identify Ubuntu 24.04, and sudo apt update should complete without repository errors.
Step 2 — Install and Start Nginx
Install Nginx:
sudo apt install -y nginx
Enable it at boot and start it now:
sudo systemctl enable --now nginx
Check the service and installed version:
systemctl is-active nginx nginx -v
If UFW is already active, allow HTTP and HTTPS traffic only after confirming that your SSH rule is present:
sudo ufw status sudo ufw allow OpenSSH sudo ufw allow 'Nginx Full'
For a deeper Nginx-only setup, see How to Install Nginx on Ubuntu 24.04.
Verify: systemctl is-active nginx should return active, and curl -I http://127.0.0.1 should return an HTTP response from Nginx.
Step 3 — Install and Secure MariaDB
Install MariaDB from the Ubuntu repositories:
sudo apt install -y mariadb-server
Enable and start the service:
sudo systemctl enable --now mariadb
Run MariaDB's security helper:
sudo mariadb-secure-installation
For a single-server LEMP stack, keep local administrative access protected, remove anonymous users and the test database, and avoid unnecessary remote root access. Applications should never connect using the MariaDB root account.
Check the database version:
sudo mariadb -e "SELECT VERSION();"
Do not expose MariaDB port 3306 publicly for this architecture.
Verify: systemctl is-active mariadb should return active, and the version query should return a MariaDB 10.11 release from the Ubuntu 24.04 package line.
Step 4 — Install PHP-FPM and Common PHP Extensions
Install PHP-FPM, the CLI, MySQL/MariaDB support, and common PHP modules:
sudo apt install -y \ php-fpm \ php-cli \ php-mysql \ php-curl \ php-gd \ php-intl \ php-mbstring \ php-xml \ php-zip \ php-bcmath
Ubuntu 24.04's standard PHP branch is 8.3. Check the CLI version and PHP-FPM service:
php -v systemctl is-active php8.3-fpm
Confirm the PHP-FPM Unix socket exists:
ls -l /run/php/php8.3-fpm.sock
If you deliberately install another PHP branch later, update both the service name and Nginx socket path consistently.
Verify: php -v should report PHP 8.3.x on the standard Ubuntu 24.04 package line, php8.3-fpm should be active, and /run/php/php8.3-fpm.sock should exist.
Step 5 — Create a Dedicated Database and Application User
Generate a strong password and save it in a password manager:
openssl rand -hex 24
Open MariaDB:
sudo mariadb
Create an application database and local-only user. Replace use_a_unique_password with the generated password:
CREATE DATABASE example_app CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'example_user'@'localhost' IDENTIFIED BY 'use_a_unique_password'; GRANT ALL PRIVILEGES ON example_app.* TO 'example_user'@'localhost'; FLUSH PRIVILEGES; EXIT;
Test the application account:
mariadb -u example_user -p example_app
Then run:
SELECT DATABASE(), CURRENT_USER(); EXIT;
Verify: The query should show example_app and example_user@localhost. Keep port 3306 closed to the public internet.
Step 6 — Create the Website Directory
This tutorial uses example.com. Replace it with your domain if you have one.
Create the document root:
sudo mkdir -p /var/www/example.com/public sudo chown -R "$USER":www-data /var/www/example.com sudo find /var/www/example.com -type d -exec chmod 750 {} \; sudo find /var/www/example.com -type f -exec chmod 640 {} \;
Create a basic HTML page:
cat <<'EOF' | sudo tee /var/www/example.com/public/index.html >/dev/null <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>LEMP stack ready</title> </head> <body> <h1>Nginx is serving the LEMP site</h1> </body> </html> EOF
Verify: Run ls -ld /var/www/example.com/public and confirm that the directory exists, then confirm index.html is present with ls -l /var/www/example.com/public/index.html.
Step 7 — Configure the Nginx Server Block for PHP-FPM
Create the site configuration:
sudo nano /etc/nginx/sites-available/example.com
Add:
server { listen 80; listen [::]:80; server_name example.com www.example.com; root /var/www/example.com/public; index index.php index.html; access_log /var/log/nginx/example.com.access.log; error_log /var/log/nginx/example.com.error.log; location / { try_files $uri $uri/ =404; } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.3-fpm.sock; } location ~ /\.(?!well-known).* { deny all; } }
Enable the site and remove the default site symlink:
sudo ln -s /etc/nginx/sites-available/example.com \ /etc/nginx/sites-enabled/example.com sudo rm -f /etc/nginx/sites-enabled/default
Test before reloading:
sudo nginx -t sudo systemctl reload nginx
Never reload Nginx after a failed nginx -t check.
Verify: sudo nginx -t should report successful syntax, and this request should return the site response:
curl -I http://127.0.0.1 -H 'Host: example.com'
Step 8 — Verify PHP Processing Without Leaving phpinfo() Exposed
Create a temporary PHP test file:
cat <<'EOF' | sudo tee /var/www/example.com/public/health.php >/dev/null <?php header('Content-Type: text/plain'); echo "php-fpm-ok\n"; EOF
Test it locally through Nginx:
curl http://127.0.0.1/health.php -H 'Host: example.com'
You should see:
php-fpm-ok
Delete the temporary PHP file immediately after verification:
sudo rm -f /var/www/example.com/public/health.php
This avoids leaving a phpinfo() page or other diagnostic endpoint exposed.
Verify: The PHP request should return php-fpm-ok, and test ! -f /var/www/example.com/public/health.php && echo removed should print removed after cleanup.
Step 9 — Verify PHP Can Reach MariaDB
Create a temporary database connectivity test outside the public document root:
cat > /tmp/lemp-db-test.php <<'PHP' <?php $pdo = new PDO( 'mysql:host=localhost;dbname=example_app;charset=utf8mb4', 'example_user', 'use_a_unique_password', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] ); echo "database-ok\n"; PHP
Replace use_a_unique_password with the application database password, then run it from the CLI:
php /tmp/lemp-db-test.php
Remove the temporary file immediately:
rm -f /tmp/lemp-db-test.php
Because the test runs outside the public web root, the database credential is never intentionally exposed over HTTP.
Verify: The command should print database-ok, and the temporary test file should be removed afterward.
Step 10 — Add HTTPS When the Domain Points to the VM
If you have a public domain whose A or AAAA records point to this server, install Certbot and its Nginx plugin:
sudo apt install -y certbot python3-certbot-nginx
Request a certificate and redirect HTTP to HTTPS:
sudo certbot --nginx --redirect \ -d example.com \ -d www.example.com
Check the certificate and renewal process:
sudo certbot certificates sudo certbot renew --dry-run
For certificate troubleshooting, use How to Secure Nginx with Let's Encrypt on Ubuntu 24.04.
Verify: curl -I https://example.com should complete over HTTPS, and curl -I http://example.com should redirect to HTTPS after Certbot updates the Nginx configuration.