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'
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. Use mktemp so the file is created with restrictive permissions instead of leaving a database password in a normally world-readable temporary file:
DB_TEST="$(mktemp /tmp/lemp-db-test.XXXXXX.php)" chmod 600 "$DB_TEST" cat > "$DB_TEST" <<'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 verify the file permissions before running it:
stat -c '%a %U:%G %n' "$DB_TEST" php "$DB_TEST"
The file mode should be 600, and PHP should print:
database-ok
Remove the temporary file immediately:
rm -f "$DB_TEST" unset DB_TEST
Because the test runs outside the public web root and the temporary file is restricted to its owner, the database credential is not intentionally exposed over HTTP or to unrelated local users.
Verify: PHP prints database-ok, the temporary file was mode 600 while it existed, and the file is removed after the test.
Step 10 — Add HTTPS When the Domain Points to the VM
If you have a public domain, verify every hostname you plan to include in the certificate before running Certbot:
dig +short A example.com dig +short A www.example.com dig +short AAAA example.com dig +short AAAA www.example.com
Only keep hostnames that resolve to this VM. If you do not use www.example.com, remove it from the Nginx server_name and from the Certbot request.
Install Certbot and its Nginx plugin:
sudo apt install -y certbot python3-certbot-nginx
Request a certificate and redirect HTTP to HTTPS for the hostnames that resolve correctly:
sudo certbot --nginx --redirect \ -d example.com \ -d www.example.com
If you use only the apex hostname, request only -d example.com. Certbot validation must succeed for every hostname in the request.
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.
Step 11 — Run the Final End-to-End LEMP Verification
Check all three services:
systemctl is-active nginx mariadb php8.3-fpm
Test the site locally through the configured hostname:
curl -I http://127.0.0.1 -H 'Host: example.com'
Check recent service and site errors:
sudo journalctl -u nginx -n 30 --no-pager sudo journalctl -u mariadb -n 30 --no-pager sudo journalctl -u php8.3-fpm -n 30 --no-pager sudo tail -n 30 /var/log/nginx/example.com.error.log
Confirm MariaDB is not intentionally exposed through UFW:
sudo ufw status
For production workloads, measure actual PHP-FPM worker memory and database usage before tuning process limits. A fixed pm.max_children value is not portable across WordPress, Laravel, Drupal, custom PHP applications, and different plugin or request profiles.
Verify: Nginx, MariaDB, and PHP-FPM should all be active; the configured site should return an HTTP response; logs should not show a repeating fatal configuration error; and port 3306 should not have a public firewall rule.
Step 12 — Clean Up or Roll Back the LEMP Test Site
If you are keeping the stack, there is nothing else to remove after the temporary PHP and database test files from Steps 8 and 9 are deleted.
If this was only a test and you want to remove the example site, first disable the Nginx site:
sudo rm -f /etc/nginx/sites-enabled/example.com sudo nginx -t sudo systemctl reload nginx
Remove the example files only if they are no longer needed:
sudo rm -rf /var/www/example.com sudo rm -f /etc/nginx/sites-available/example.com
Drop the test database and user:
sudo mariadb
Then run:
DROP DATABASE example_app; DROP USER 'example_user'@'localhost'; EXIT;
If you issued a Let's Encrypt certificate specifically for this retired test domain, inspect certificates before deleting anything:
sudo certbot certificates
Then delete only the certificate that belongs to the retired hostname:
sudo certbot delete --cert-name example.com
Verify: After a full rollback, the Nginx site symlink and document root should be gone, and SHOW DATABASES LIKE 'example_app'; should return no test database.
Conclusion
You now have a complete LEMP stack on Ubuntu 24.04 with Nginx serving the application, PHP-FPM handling PHP requests, and MariaDB using a dedicated local application account. The stack has explicit Nginx, PHP, database, firewall, HTTPS, end-to-end verification, and rollback paths rather than stopping after package installation.
Use the Linux VM product linked in the introduction and current Raff pricing to choose resources for this self-managed stack. A 2 GB VM can be an initial small-test reference; production needs depend on PHP-FPM concurrency, MariaDB memory, background jobs, plugins, and traffic. To decide whether this operating model suits your workload, read VPS for Web Hosting: What Small Teams Should Know. This tutorial runs MariaDB on the VM; managed versus self-hosted databases explains a separate operating model, not an automatically available managed MariaDB SKU. For a complete PHP application example, continue with WordPress on Ubuntu 24.04 with Nginx.