A LEMP stack — Linux, Nginx, MySQL, PHP — is the modern workhorse behind millions of PHP sites, from WordPress blogs to custom Laravel apps. This guide installs the full stack on a fresh Ubuntu 24.04 server, configures Nginx to serve PHP through php-fpm, and gets you a working PHP page in about 20 minutes. It is written for beginners, but every command is explained so you know exactly what is happening on your server.

What you will build

By the end you will have:

  • Nginx 1.24 serving traffic on ports 80/443
  • MySQL 8.0 running with a dedicated application user (not root)
  • PHP 8.3 running as php-fpm, wired into Nginx
  • A server block for your domain that renders PHP correctly

Prerequisites

  • A server (VPS or local VM) running Ubuntu 24.04 LTS
  • A non-root user with sudo access (see our Ubuntu initial server setup guide if you have not done this yet)
  • A domain or subdomain pointed at your server's IP (optional for local testing — localhost works too)
  • About 20 minutes and a terminal

Step 1 — Update the package index

Always start with fresh package lists so you pull the newest available versions from the Ubuntu repositories.

sudo apt update && sudo apt upgrade -y

apt update refreshes the list of available packages, and apt upgrade -y installs pending security updates. On a brand-new VPS this can take a couple of minutes.

Step 2 — Install Nginx

Nginx is the web server that will accept HTTP requests and pass PHP requests on to php-fpm.

sudo apt install nginx -y

This installs Nginx 1.24 from the Ubuntu 24.04 repositories. Once installed, start it and check it is running:

sudo systemctl start nginx
sudo systemctl enable nginx
sudo systemctl status nginx --no-pager

enable makes Nginx start automatically after a reboot — something you want on any server. If UFW is active, open the web ports now:

sudo ufw allow 'Nginx Full'

This opens both port 80 (HTTP) and 443 (HTTPS). Visit http://your-server-ip in a browser — you should see the Nginx welcome page.

Step 3 — Install MySQL 8.0

Next, the database layer.

sudo apt install mysql-server -y

Ubuntu 24.04 ships MySQL 8.0. Confirm the service is up:

sudo systemctl status mysql --no-pager

Now harden the installation. On Ubuntu, the MySQL root user authenticates via the auth_socket plugin, which means you log in as root with sudo mysql — no password prompt:

sudo mysql_secure_installation

Answer y to the VALIDATE PASSWORD COMPONENT prompt (it enforces strong passwords), then remove anonymous users, disallow remote root login, and reload privilege tables. When it asks for the root password, note that with auth_socket this changes nothing for local root — password login for root is only relevant if you switch root to mysql_native_password, which you do not need to do.

Create a database and a dedicated app user

Never let your application connect as root. Create a database and a user with privileges scoped to just that database:

sudo mysql

Then inside the MySQL shell:

CREATE DATABASE myapp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'myapp'@'localhost' IDENTIFIED BY 'change-this-to-a-strong-password';
GRANT ALL PRIVILEGES ON myapp.* TO 'myapp'@'localhost';
FLUSH PRIVILEGES;
EXIT;

utf8mb4 is the correct character set for full Unicode (emoji included) — utf8 in MySQL is a partial implementation and should be avoided. FLUSH PRIVILEGES reloads the grant tables so the new user works immediately.

Step 4 — Install PHP 8.3 with php-fpm

Ubuntu 24.04 ships PHP 8.3, which is the current stable branch for new deployments. Unlike Apache, Nginx does not run PHP itself — it hands PHP requests to the php-fpm process manager over a Unix socket.

sudo apt install php-fpm php-mysql php-cli php-curl php-gd php-mbstring php-xml php-zip -y

What each package is for: php-fpm is the FastCGI process manager that executes PHP; php-mysql lets PHP talk to MySQL; the rest (php-curl, php-gd, php-mbstring, php-xml, php-zip) are the extensions most CMSs and frameworks expect. Start and enable it:

sudo systemctl start php8.3-fpm
sudo systemctl enable php8.3-fpm
sudo systemctl status php8.3-fpm --no-pager

Note the exact socket path php-fpm listens on — you will need it for the Nginx config:

ls /run/php/

You should see php8.3-fpm.sock. If your PHP version differs, adjust the socket path in the next step accordingly.

Step 5 — Configure an Nginx server block for PHP

Create a server block for your site. Replace example.com with your domain (or use an IP-based test):

sudo nano /etc/nginx/sites-available/example.com

Paste this configuration:

server {
    listen 80;
    server_name example.com www.example.com;
    root /var/www/example.com;
    index index.php index.html index.htm;

    location / {
        try_files $uri $uri/ =404;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    location ~ /\.ht {
        deny all;
    }
}

How it works: root sets the document root; index makes Nginx look for index.php first; the \.php$ location hands every PHP request to the php-fpm socket via FastCGI. The snippets/fastcgi-php.conf file ships with Ubuntu's Nginx and sets the standard FastCGI parameters.

Create the document root and a test page:

sudo mkdir -p /var/www/example.com
sudo chown -R www-data:www-data /var/www/example.com
echo '<?php phpinfo(); ?>' | sudo tee /var/www/example.com/index.php

The www-data ownership is important — Nginx and php-fpm run as www-data, and files owned by root with restrictive permissions are a classic cause of 403 errors. Enable the site:

sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t

nginx -t tests the configuration without reloading. It must print syntax is ok and test is successful. If it does, reload Nginx:

sudo systemctl reload nginx

A reload applies the new config with zero downtime — existing connections are not dropped.

Step 6 — Verify it worked

Run these checks top to bottom. Every one should pass.

  1. Browser check: visit http://example.com (or your server IP). You should see the purple PHP info page — that proves Nginx and php-fpm are talking.
  2. Services check:
    sudo systemctl is-active nginx mysql php8.3-fpm

    All three should print active.

  3. Version check:
    nginx -v
    mysql --version
    php -v

    Expect Nginx 1.24, MySQL 8.0.x, and PHP 8.3.x.

  4. Database connection check — confirm your app user can log in:
    mysql -u myapp -p -e "SELECT 1;"

    Type the password you set. A 1 in the output means credentials work.

  5. OPcache check — PHP 8.3 ships with OPcache enabled by default; it caches compiled scripts in memory and is free performance:
    php -i | grep -i "opcache.enable "

    It should show On.

Step 7 — Remove the phpinfo page and add a real test

phpinfo() exposes server details to the world — useful for testing, dangerous to leave online. Replace it with a database-connection test instead:

sudo rm /var/www/example.com/index.php
sudo tee /var/www/example.com/index.php > /dev/null <<'EOF'
<?php
$conn = new mysqli('localhost', 'myapp', 'change-this-to-a-strong-password', 'myapp');
if ($conn->connect_error) {
    die('Database connection failed: ' . $conn->connect_error);
}
echo 'LEMP stack is working — connected to MySQL ' . $conn->server_info;
$conn->close();
EOF
sudo chown www-data:www-data /var/www/example.com/index.php

Reload the page: you should see "LEMP stack is working — connected to MySQL 8.0.x". That single line proves all four layers are cooperating.

Common issues & fixes

1. 502 Bad Gateway on PHP pages

The most common LEMP error. It almost always means the socket path in your Nginx config does not match the socket php-fpm actually created. Check what exists:

ls /run/php/
sudo systemctl status php8.3-fpm --no-pager

If php-fpm is not running, start it. If the socket name differs (e.g. after a PHP upgrade), update the fastcgi_pass line and run sudo systemctl reload nginx.

2. Browser downloads the PHP file instead of executing it

Nginx is serving the file as static content, which means the location ~ \.php$ block is not matching. Causes: the fastcgi_pass line is commented out, the site config was not enabled in sites-enabled, or you edited a file but forgot sudo nginx -t && sudo systemctl reload nginx.

3. "Access denied for user 'myapp'@'localhost'"

You typed the password wrong, or the grant did not apply. Re-run the GRANT and FLUSH PRIVILEGES statements from Step 3 as sudo mysql. If your app runs on a different host, the user needs a 'myapp'@'your-app-host' entry — see our guide on enabling remote MySQL access safely.

4. 403 Forbidden on the document root

Permissions. Fix ownership and make sure Nginx can traverse the path:

sudo chown -R www-data:www-data /var/www/example.com
sudo chmod 755 /var/www/example.com

5. Site works on localhost but not from the internet

Almost always the firewall or the cloud provider's security group. Verify UFW:

sudo ufw status

Nginx Full (or ports 80/443) must be ALLOW. If you are on AWS/DigitalOcean/Hetzner, also check the cloud firewall attached to the instance.

What to do next

A bare LEMP stack is just the foundation. The usual next steps, in order:

  • Add HTTPS — install a free Let's Encrypt certificate with Certbot (see our Nginx + Let's Encrypt guide).
  • Install WordPress or Laravel — both run natively on this exact stack.
  • Tune for production — adjust pm.max_children in /etc/php/8.3/fpm/pool.d/www.conf based on your RAM, and set up cron jobs for backups.
  • Lock down MySQL — restrict the app user further if it only needs SELECT/INSERT/UPDATE/DELETE.

You now have a production-capable LEMP stack on Ubuntu 24.04. If you would rather have an expert set up and harden your server — Nginx tuning, MySQL replication, automated backups — XpertBees does exactly that for clients every week. Get in touch and we will take it from here.