# How to install a LEMP stack on Ubuntu 24.04, step by step

> Install a LEMP stack on Ubuntu 24.04 (and Debian 12): firewall, nginx, MariaDB, PHP-FPM, server blocks, Let's Encrypt SSL and per-site isolation done right.

Source: https://zopanel.net/blog/lemp-stack-ubuntu  
Updated: 2026-10-21

## Key takeaways

- Ubuntu 24.04 ships nginx 1.24, PHP 8.3 and MariaDB 10.11 in its official repositories.
- nginx hands PHP files to PHP-FPM through the Unix socket /run/php/php8.3-fpm.sock.
- Each website should have its own Linux user and PHP-FPM pool so one hacked site cannot read another.
- Always allow SSH in UFW before you enable the firewall.

To install a LEMP stack on Ubuntu 24.04, the official repositories are all you need: `apt install nginx mariadb-server php8.3-fpm php8.3-mysql`, run `mariadb-secure-installation`, then create an nginx server block that passes `.php` files to the `/run/php/php8.3-fpm.sock` socket. On a fresh VPS the whole job, including a free SSL certificate, takes about 20 minutes.

This guide walks through each step with commands checked on Ubuntu 24.04 (with notes for Debian 12), explains why each step exists, and covers the parts short tutorials skip: the firewall, file permissions and a separate PHP pool for every site. It ends with an honest look at when to do this by hand and when a control panel makes more sense.

## What is LEMP, and why nginx instead of Apache?

LEMP is **L**inux, **E**ngine-X (nginx), **M**ariaDB (or MySQL) and **P**HP. Unlike LAMP with Apache and `mod_php`, nginx does not run PHP inside the web server. It serves static files very efficiently and hands PHP requests to **PHP-FPM**, a separate service that manages PHP workers.

That split has two practical benefits: nginx stays light on RAM with many concurrent connections, and each website can run PHP as its own Linux user. The trade-off: nginx ignores `.htaccess`, so rewrite rules live in the nginx configuration.

## Which versions do you get when you install a LEMP stack on Ubuntu 24.04?

| Component | Ubuntu 24.04 | Debian 12 |
| --- | --- | --- |
| nginx | 1.24 | 1.22 |
| PHP | 8.3 | 8.2 |
| MariaDB | 10.11 | 10.11 |

These branches stay fixed for the life of the release, with security fixes backported. Check exactly what you get with `apt policy nginx mariadb-server php8.3-fpm`. If you need several PHP versions side by side, you have to add a third-party repository (Ondřej Surý's PPA on Ubuntu, packages.sury.org on Debian); this guide sticks to the official repositories.

On Debian 12, replace every `8.3` in this guide with `8.2`.

## Step 1: Update the system and enable the firewall

Log in as a sudo user and run:

```bash
sudo apt update && sudo apt full-upgrade -y
```

Then enable UFW, **allowing SSH first**:

```bash
sudo apt install ufw
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status
```

If SSH listens on a port other than 22, use that number. Enabling UFW without the SSH rule is the fastest way to lock yourself out; only your provider's console can save you then. For the rest of a new server's hardening, see [how to secure an Ubuntu VPS](/blog/secure-ubuntu-vps).

## Step 2: Install nginx

```bash
sudo apt install nginx
sudo systemctl enable --now nginx
curl -I http://localhost
```

`HTTP/1.1 200 OK` means nginx is running. Opening `http://your-server-ip` in a browser shows the default welcome page.

## Step 3: Install and secure MariaDB

```bash
sudo apt install mariadb-server
sudo mariadb-secure-installation
```

The script asks a few questions. On Ubuntu and Debian, MariaDB's `root` user authenticates with `unix_socket` by default: only the operating system's root (through `sudo mariadb`) can log in, with no password. That is a safe setup; keep it. Answer **Y** to removing anonymous users, disallowing remote root login and removing the test database.

MariaDB listens on `127.0.0.1` by default. Do not open port 3306 to the internet; for remote access, use an SSH tunnel or allow only your own IP.

Create a database and user for the site (one user per site):

```bash
sudo mariadb -e "CREATE DATABASE example_wp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'example_wp'@'localhost' IDENTIFIED BY 'change-to-a-strong-password';
GRANT ALL PRIVILEGES ON example_wp.* TO 'example_wp'@'localhost';"
```

## Step 4: Install PHP-FPM and the usual extensions

```bash
sudo apt install php8.3-fpm php8.3-mysql php8.3-curl php8.3-gd \
  php8.3-mbstring php8.3-xml php8.3-zip php8.3-intl
sudo systemctl status php8.3-fpm
```

These extensions cover WordPress and most popular PHP applications. OPcache comes with PHP-FPM. **Do not** install the bare `php8.3` package: on some systems it pulls in Apache and `libapache2-mod-php`, and Apache then fights nginx for port 80.

Adjust a few values in `/etc/php/8.3/fpm/php.ini` if your application needs them:

```ini
upload_max_filesize = 64M
post_max_size = 64M
memory_limit = 256M
```

Then `sudo systemctl reload php8.3-fpm`.

## Step 5: Create an nginx server block

Create the site's folder and configuration file:

```bash
sudo mkdir -p /var/www/example.com/public
sudo nano /etc/nginx/sites-available/example.com
```

```nginx
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    root /var/www/example.com/public;
    index index.php index.html;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

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

    location ~ /\.(?!well-known) {
        deny all;
    }
}
```

`snippets/fastcgi-php.conf` ships with the Ubuntu and Debian nginx packages and sets up the FastCGI parameters. The last block denies hidden files such as `.env` and `.git` while leaving `.well-known` available for Let's Encrypt.

Enable it and test:

```bash
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 reloading. One missing semicolon can stop nginx from restarting, taking every site on the server with it. The [nginx documentation](https://nginx.org/en/docs/) covers every directive used here.

## Step 6: Check that PHP works

```bash
echo '<?php phpinfo();' | sudo tee /var/www/example.com/public/info.php
curl -s http://localhost/info.php -H 'Host: example.com' | grep -m1 'PHP Version'
sudo rm /var/www/example.com/public/info.php
```

Delete `info.php` right after the test: it exposes your server configuration to anyone.

## Step 7: Free SSL with Let's Encrypt

Point the A records of `example.com` and `www.example.com` at the VPS, then:

```bash
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
sudo certbot renew --dry-run
```

Certbot adds HTTPS to the server block, redirects HTTP to HTTPS and sets up a renewal timer. See [Certbot's instructions](https://certbot.eff.org/) and our [guide to free SSL with Let's Encrypt](/blog/free-ssl-lets-encrypt).

## Step 8: Isolate sites when you host more than one

The configuration above runs PHP for every site as the same `www-data` user. For one site that is fine. For several, it is the biggest weakness of a hand-built LEMP server: a hacked plugin on site A can read site B's `wp-config.php`, because they share a user.

The fix is one Linux user and one PHP-FPM pool per site:

```bash
sudo adduser --system --group --no-create-home --home /var/www/example.com example
sudo chown -R example:example /var/www/example.com/public
sudo cp /etc/php/8.3/fpm/pool.d/www.conf /etc/php/8.3/fpm/pool.d/example.conf
```

In `example.conf`, change `[www]` to `[example]`, set `user = example`, `group = example` and `listen = /run/php/php8.3-fpm-example.sock`, and keep `listen.owner = www-data` and `listen.group = www-data` so nginx can connect. Point the server block's `fastcgi_pass` at the new socket, then reload PHP-FPM and nginx. Set directory permissions to `750` and consider `open_basedir` per pool.

Doing this for ten sites is manageable. Doing it for a hundred, along with CPU and memory limits, disk quotas, backups and certificate renewals, is exactly what a control panel is for.

## Build it yourself or use a control panel?

| Criterion | Hand-built LEMP | Control panel (e.g. ZoPanel) |
| --- | --- | --- |
| Understanding of the system | Excellent | You rarely need to look |
| Time to add a site | 10–20 minutes | About a minute |
| Isolation between sites | Per pool, per user, by hand | Built in for every account |
| Several PHP versions | Add a repository, manage it yourself | Pick from a list |
| Backups, SSL, monitoring | Your own scripts and cron jobs | In the interface |
| Best for | 1–3 sites, people who want full control | Agencies, hosting providers, many sites |

ZoPanel installs this same stack: nginx, MariaDB, and PHP from Ondřej Surý's repositories (PHP 7.4 to 8.4), plus UFW, Fail2ban and chrooted SFTP. MariaDB's buffer pool is set to 20% of RAM. Every hosting account is its own Linux user with its own PHP-FPM pool and `open_basedir`, running in its own systemd slice with CPU, memory and process limits from its package. Pools of idle sites go to sleep to save memory. It installs on Ubuntu 22.04/24.04 or Debian 12/13 with one command:

```bash
curl -fsSL https://get.zopanel.net | sudo bash
```

The installer expects a fresh server with no other web server or panel, so do not run it on the machine you just built by hand. See the [installation guide](/docs/install), [optional components](/docs/components) and [websites and PHP versions](/docs/hosting).

## Common LEMP problems

- **502 Bad Gateway**: nginx cannot reach PHP-FPM. Check that `fastcgi_pass` names the right socket (`ls /run/php/`) and that PHP-FPM is running.
- **The browser downloads .php files**: the `location ~ \.php$` block is missing, or nginx was not reloaded.
- **404 on every page except the home page (WordPress, Laravel)**: `try_files ... /index.php?$args` is missing.
- **Access denied for user**: wrong database password, or the user was created for a host other than `localhost`.
- **Apache holds port 80**: the bare `php8.3` package was installed. Remove Apache with `sudo apt purge apache2` (after checking nothing else uses it).
- **Certbot fails validation**: DNS does not point to the server yet, or your provider's firewall blocks port 80.

## Conclusion

Installing LEMP on Ubuntu 24.04 is straightforward: the official repositories already carry nginx 1.24, PHP 8.3 and MariaDB 10.11, which cover most PHP applications today. The most valuable part of building it yourself is understanding how nginx, PHP-FPM and MariaDB fit together. The most expensive part is keeping many sites isolated, backed up and on valid certificates. When you reach that point, try ZoPanel's Free plan (3 hosting accounts, 10 websites, 10 databases, no time limit, multiple PHP versions included) on a fresh VPS, and see [pricing](/pricing) when you need more.

## Frequently asked questions

### Which PHP version does Ubuntu 24.04 ship?

Ubuntu 24.04's official repositories ship PHP 8.3, as the `php8.3-fpm` package. Debian 12 ships PHP 8.2. For other versions you need a third-party repository such as Ondřej Surý's PPA.

### Should I use MariaDB or MySQL in a LEMP stack?

Both run WordPress and most PHP applications well. MariaDB is in the Ubuntu and Debian repositories and is the default MySQL-compatible server on Debian, so it is simpler to install and update. Choose MySQL only when an application needs a MySQL-specific feature.

### Why does nginx return 502 Bad Gateway after installing PHP?

Usually `fastcgi_pass` points at the wrong socket or PHP-FPM is not running. Run `ls /run/php/` to see the real socket name and `systemctl status php8.3-fpm` to check the service, then fix the server block and reload nginx.

### Do I need to open MariaDB's port 3306?

No, not when the site and database share a server. An open 3306 attracts constant password guessing. For remote access, use an SSH tunnel or allow only your own IP in the firewall.

### Can I install a control panel on top of a hand-built LEMP server?

Most panels, ZoPanel included, expect a fresh server and check that no other web server is present, because the configurations would clash. Install the panel on a new VPS and move your sites over.
