How to Install LAMP on Ubuntu 24.04

In this tutorial you’ll learn how to install a production-ready LAMP stack on Ubuntu 24.04.

LAMP stands for Linux, Apache, MySQL and PHP.

This setup uses PHP-FPM with Apache’s event MPM instead of the older mod_php. It’s faster, it uses far less memory under load, and it’s the configuration you want on a real server. With mod_php, every Apache worker carries a full PHP interpreter (roughly 60 MB each), so a traffic spike can spawn enough workers to exhaust your RAM and freeze the whole box. PHP-FPM keeps PHP in a separate pool you can cap, so a burst queues instead of crashing the server.

First you’ll install all the required packages, then you’ll configure each one.

1. Install the LAMP packages

Update and upgrade everything first.

sudo apt update
sudo apt upgrade -y

Install Apache.

sudo apt install apache2

Install the MySQL server.

sudo apt install mysql-server

Install PHP-FPM and the extensions a typical site (including WordPress) needs.

sudo apt install php-fpm php-mysql php-curl php-gd php-mbstring php-xml php-zip php-intl php-bcmath

On Ubuntu 24.04 this installs PHP 8.3 and the matching php8.3-* extensions. Note that we install php-fpm, not the php package. Installing php alongside Apache would pull in mod_php and switch Apache to the slower prefork MPM, which is exactly what we want to avoid.

2. Configure Apache with PHP-FPM

Enable the two modules that let Apache hand PHP requests to PHP-FPM, then enable the FPM config that ships with the package.

sudo a2enmod proxy_fcgi setenvif
sudo a2enconf php8.3-fpm

Make sure Apache uses the event MPM. On a clean install without mod_php this is already the default, but these commands make it explicit and safe to run either way.

sudo a2dismod mpm_prefork 2>/dev/null
sudo a2enmod mpm_event
sudo systemctl restart apache2

Check that Apache is running and confirm the version.

sudo systemctl status apache2
apache2 -v

Open a browser and visit your server’s IP address.

http://your_server_ip/

If you see the default Apache page, you’re good.

2.1 Configure a virtual host for your domain

This example uses mydomain.com. Replace it with your own domain everywhere it appears.

Create the site config file.

sudo vi /etc/apache2/sites-available/mydomain.com.conf

Add these contents.

<VirtualHost *:80>
    ServerName mydomain.com
    ServerAlias www.mydomain.com
    ServerAdmin webmaster@mydomain.com
    DocumentRoot /var/www/mydomain.com/public_html

    <Directory /var/www/mydomain.com/public_html>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/mydomain.com.error.log
    CustomLog ${APACHE_LOG_DIR}/mydomain.com.access.log combined
</VirtualHost>

Two things to notice here. AllowOverride All is set inside this site’s <Directory> block, so WordPress .htaccess rules work for this site only. This is the production-correct way to do it. Do not enable AllowOverride All globally in apache2.conf, because that makes Apache check for .htaccess files in every directory on every request, which is slower and broader than you need. Options -Indexes also disables directory listing so visitors can’t browse your files.

Create the document root and set the permissions.

sudo mkdir -p /var/www/mydomain.com/public_html
sudo chown -R www-data:www-data /var/www/mydomain.com/public_html
sudo chmod -R 755 /var/www/mydomain.com/public_html

Enable your site, disable the default one, and reload Apache.

sudo a2ensite mydomain.com.conf
sudo a2dissite 000-default.conf
sudo systemctl reload apache2

3. Configure the MySQL database

Secure the installation first.

sudo mysql_secure_installation

Answer Y to these prompts.

Remove anonymous users?
Disallow root login remotely?
Remove test database and access to it?
Reload privilege tables now?

You’ll also be asked whether to set up the password validation component. Enabling it is a good idea on production, but remember that any password you create afterward must then meet the policy.

Now connect and create your database and user.

sudo mysql
CREATE DATABASE webdata CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'webuser'@'localhost' IDENTIFIED BY 'CHANGE_THIS_TO_A_STRONG_PASSWORD';
GRANT ALL PRIVILEGES ON webdata.* TO 'webuser'@'localhost';
FLUSH PRIVILEGES;
EXIT;

A few production details matter here. The user is scoped to 'webuser'@'localhost', so it can only connect from the server itself. If you omit the host, MySQL creates 'webuser'@'%', which allows connections from any host. The utf8mb4 character set supports the full range of Unicode (including emoji) and is the default WordPress expects. And use a real password, not the word “password”.

4. Configure PHP

With PHP-FPM, the file you edit is the FPM php.ini, not the Apache one.

sudo vi /etc/php/8.3/fpm/php.ini

Change these values.

error_log = /var/log/php/error.log
memory_limit = 256M
upload_max_filesize = 200M
post_max_size = 208M

The important detail is that post_max_size must be greater than or equal to upload_max_filesize. post_max_size caps the entire request body, so if you raise upload_max_filesize to 200M but leave post_max_size at its 8M default, any upload over 8M still fails. Set both.

Create the directory for the PHP error log. PHP-FPM runs as www-data, so it needs to own it.

sudo mkdir /var/log/php
sudo chown www-data:www-data /var/log/php

Now size the PHP-FPM pool. This is the single most important production setting, because pm.max_children is the hard ceiling on how much memory PHP can ever use.

sudo vi /etc/php/8.3/fpm/pool.d/www.conf

Set these values.

pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 8
pm.max_requests = 500

How to choose pm.max_children: each PHP-FPM process uses roughly 40 to 60 MB for a typical app. Take the RAM you want to give PHP, then divide by the per-process size. For example, on a 4 GB server, leave about 1 GB for MySQL and 0.5 GB for the system, which leaves around 2.5 GB for PHP. At 60 MB per process that’s about 40 children, so a conservative starting point is 20. Start low and raise it only if you see all children busy without memory pressure. pm.max_requests = 500 recycles each process after 500 requests, which releases any memory leaked by plugins over time.

Restart PHP-FPM and reload Apache to apply everything.

sudo systemctl restart php8.3-fpm
sudo systemctl reload apache2

5. Secure the server for production

Set up the firewall. Allow SSH first so you don’t lock yourself out, then allow Apache, then enable it.

sudo ufw allow OpenSSH
sudo ufw allow "Apache Full"
sudo ufw enable

Reduce the information Apache leaks about itself. Edit the security config.

sudo vi /etc/apache2/conf-available/security.conf

Set these two directives.

ServerTokens Prod
ServerSignature Off

Then reload Apache.

sudo systemctl reload apache2

Serve your site over HTTPS. A production server should not run on plain HTTP. Install a free Let’s Encrypt certificate by following how to install an SSL certificate on Ubuntu with Apache2.

Protect against brute-force attacks. Bots constantly hammer SSH and login pages. Install fail2ban by following how to install fail2ban on Ubuntu 24.04.

Make sure all services start automatically after a reboot.

sudo systemctl enable apache2 mysql php8.3-fpm

6. Test the LAMP installation

Create a temporary test file.

sudo vi /var/www/mydomain.com/public_html/test.php

Add these contents. Use the same database password you set earlier.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Database Connection</title>
</head>
<body>
    <h1>Database Connection Status</h1>
    <p>
        <?php
            $servername = "localhost";
            $username = "webuser";
            $password = "CHANGE_THIS_TO_A_STRONG_PASSWORD";
            $dbname = "webdata";

            $conn = mysqli_connect($servername, $username, $password, $dbname);

            if (!$conn) {
                echo "Not Connected: " . mysqli_connect_error();
            } else {
                echo "Connected Successfully to database: " . $dbname;
            }

            mysqli_close($conn);
        ?>
    </p>
</body>
</html>

Set the owner.

sudo chown www-data:www-data /var/www/mydomain.com/public_html/test.php

Open your browser and go to http://mydomain.com/test.php. If everything works you’ll see:

Connected Successfully to database: webdata

Now delete the test file. This step is not optional.

sudo rm /var/www/mydomain.com/public_html/test.php

This file contains your database credentials in plain text. Never leave it on a production server. Once you’ve confirmed the connection works, remove it.

Your LAMP stack is now installed, tuned, and hardened for production. You can drop a WordPress install or your own application into /var/www/mydomain.com/public_html and go.

FAQ

Should I use mod_php or PHP-FPM on a production server?

Use PHP-FPM with Apache’s event MPM. With mod_php, every Apache worker loads a full PHP interpreter, so each one needs around 60 MB and a traffic spike can exhaust your RAM. PHP-FPM runs PHP in a separate pool you can cap with pm.max_children, so the server stays stable under load.

Why does my large file upload still fail after raising upload_max_filesize?

Because post_max_size is too low. It caps the whole request body, so it must be greater than or equal to upload_max_filesize. If upload_max_filesize is 200M but post_max_size is the 8M default, uploads over 8M fail. Raise both.

How do I choose pm.max_children for PHP-FPM?

Divide the RAM you want to give PHP by the average process size (about 40 to 60 MB). On a 4 GB server, after leaving room for MySQL and the system, around 20 to 40 is sensible. Start low and raise it only if all children are busy and you still have free memory.

Do I need to edit apache2.conf to enable AllowOverride for WordPress?

No. Set AllowOverride All inside the <Directory> block of your virtual host instead. That enables .htaccess for your site only, which is faster and safer than turning it on globally in apache2.conf.

Is this LAMP stack ready for WordPress?

Yes. The PHP extensions installed here cover WordPress requirements, the database uses the utf8mb4 charset WordPress expects, and AllowOverride All lets its permalinks work. Add HTTPS and fail2ban as shown, and you’re production-ready.

Leave a Reply

Your email address will not be published. Required fields are marked *