SSD Nodes Learn Hosting plans →
How to do am Matt ConnorBy Matt Connor

Set up LAMP stack for Ubuntu 24.04 with PHP-FPM

Set up Apache, MariaDB unix_socket auth, PHP 8.3 with PHP-FPM, a name-based vhost, and free Let's Encrypt HTTPS with Certbot on Ubuntu 24.04.

Wetin you dey build

A LAMP stack na four parts wey dey run for one Ubuntu 24.04 server: Linux dey underneath, Apache dey answer HTTP, MariaDB dey hold the data, and PHP 8.3 dey run the code. By the end, you go get a name-based virtual host wey dey serve real application directory, database wey get dedicated least-privilege user, PHP wey connect to Apache through PHP-FPM, and free Let's Encrypt certificate on top.

The installation itself na four apt commands. Almost everything for this guide na how to connect these parts, plus the small mistakes wey fit make brand-new stack serve blank page, give your source code to browser as download, or refuse make you enter the database wey you just install. Each one get sign wey you fit recognise, and we name each one below with the exact text wey you go see.

Prerequisites and the real wahala

Assume say na fresh Ubuntu 24.04 KVM VPS you get, with one sudo user or root, plus public IPv4 address. Minimal stack fit run with 1 GB RAM; give am 2 GB before you put real database-backed application there, because MariaDB default buffers plus some PHP-FPM workers fit finish the first gigabyte sharp sharp.

Two things must dey correct before Certbot for the end fit work, so arrange dem now. You need one domain name with A record wey dey point to the VPS public IP. Let's Encrypt dey validate through HTTP to that name, and bare IP address no fit ever get certificate. Also, ports 80 and 443 must dey reachable from internet. For many providers, this means say you must open dem for network firewall inside control panel and inside ufw for the box. DNS changes fit take up to one hour before dem propagate, so set the A record first. E go don work by the time you need am.

Step 1 - Install Apache and confirm the default page

sudo apt update
sudo apt install -y apache2

apt go start and enable the service for you. Check am:

systemctl status apache2

You suppose see one line wey read active (running). Now open http://YOUR_SERVER_IP/ for browser. The Apache2 Ubuntu Default Page with the big "It works!" banner na the correct result. E prove say Apache dey serve content; e no be mistake. That page dey for /var/www/html/index.html, and the shipped default virtual host 000-default.conf dey serve am. Later, you go disable both of dem. For now, na exactly wetin you suppose see.

If the page no load at all but systemctl show say the process dey run, firewall dey block am. Na that one be the next step.

Step 2 - Open firewall for HTTP and HTTPS

The apache2 package dey register three ufw application profiles. List dem:

sudo ufw app list

You go see Apache, Apache Full, and Apache Secure. Apache na port 80 only, Apache Secure na 443 only, and Apache Full na both. Na that one you want, because you dey add TLS for the end.

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

Allow OpenSSH before you run ufw enable. ufw dey default to deny all incoming traffic, and if you enable am without SSH rule, e go cut your own connection as soon as e activate. You go keep the current session, but you no go fit reconnect again. Confirm with sudo ufw status; you want OpenSSH, Apache Full, and their v6 equivalents all to dey read ALLOW.

Step 3 - Install MariaDB and secure am

sudo apt install -y mariadb-server
systemctl status mariadb

Ubuntu 24.04 release MariaDB 10.11, wey be long-term-support release, so you no need external repository. Once the service dey run, harden am:

sudo mysql_secure_installation

E good make you read the prompts instead of just pressing Enter. When e ask for the current root password, press Enter because password never dey. When e ask "Switch to unix_socket authentication?", answer no change anything because e already enabled for this package, so press n. Answer n to "Change the root password?" because of the reason for the next paragraph. Then answer Y to the remaining prompts: remove anonymous users, disallow remote root login, delete the test database, and reload privilege tables.

Na this part dey confuse everybody. For Ubuntu MariaDB, the root database account dey use unix_socket authentication, no be password. This mean say the database trusts the operating-system user wey you don already authenticate as. So this command go work from root shell:

sudo mysql

...and e go open MariaDB [(none)]> prompt without asking for password. If unprivileged user run the same command, e go refuse am. Na the whole point be that access to database root dey tied to sudo for the machine, and no password dey wey person fit steal, phish, or brute-force. This more secure pass password, so leave am like that. The rule wey follow from this na: never point application to the root account. Create dedicated user for each application (Step 7), because app wey connect over TCP with username and password no fit use socket auth anyway. You also want make each app get access only to its own database.

Step 4 - Install PHP 8.3 with PHP-FPM

Ubuntu 24.04 default PHP na 8.3. Install the FPM process manager and the extensions wey typical app dey need:

sudo apt install -y php8.3-fpm php8.3-mysql php8.3-cli \
  php8.3-curl php8.3-xml php8.3-mbstring php8.3-zip

Notice wetin no dey that list: libapache2-mod-php. That older package dey put PHP interpreter inside every Apache process. E simple, but every worker carry PHP copy whether e dey serve script or static image. Both dey share the same lifecycle, and e only work with Apache prefork MPM, wey no efficient pass. PHP-FPM instead dey run PHP as im own pool of processes wey Apache dey talk to through socket. Apache fit then use threaded event MPM for static files and send only PHP requests across. You fit tune the pool separately from the web server, and the same FPM setup go still work later if you put nginx in front. Na current default for good reason.

Apache dey reach FPM through proxy_fcgi module. Enable am, enable the config wey FPM package drop, then restart:

sudo a2enmod proxy_fcgi setenvif
sudo a2enconf php8.3-fpm
sudo systemctl restart apache2

a2enconf php8.3-fpm dey activate /etc/apache2/conf-available/php8.3-fpm.conf, wey contain the rule wey route PHP files go FPM socket. The main part match any .php file and forward am to socket for /run/php/php8.3-fpm.sock:

<FilesMatch ".+\.ph(ar|p|tml)$">
    SetHandler "proxy:unix:/run/php/php8.3-fpm.sock|fcgi://localhost"
</FilesMatch>

You no need edit that file; e ship correct. But if you know the socket path, you fit diagnose "PHP downloads instead of running" and "Primary script unknown" failures later. Both happen because Apache and FPM no agree about this socket or the file wey dey behind am.

Step 5 - Virtual host wey use name for your app

Virtual hosting wey use name make one IP fit serve many sites; Apache go pick the site based on the Host: header for the request. Make directory for the app, far from the default /var/www/html:

sudo mkdir -p /var/www/testapp
sudo chown -R www-data:www-data /var/www/testapp
sudo chmod -R 755 /var/www/testapp

Ownership dey important. Apache and PHP-FPM both dey run as the www-data user for Ubuntu, so files wey web server need read, and directories wey app need write to, like uploads folder, suppose belong to www-data. If you go also edit files with your login user, common pattern na to own the files yourself and add your user to the www-data group; for plain deploy, www-data:www-data na the least surprising option.

Create the virtual host for /etc/apache2/sites-available/testapp.conf:

<VirtualHost *:80>
    ServerName app.example.com
    DocumentRoot /var/www/testapp

    <Directory /var/www/testapp>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/testapp-error.log
    CustomLog ${APACHE_LOG_DIR}/testapp-access.log combined
</VirtualHost>

Set ServerName to your real domain. Options -Indexes stop Apache from listing the directory when no index file dey; otherwise visitors fit browse your source tree. AllowOverride All make .htaccess file work, and most PHP applications expect this for pretty URLs; change am to None to get small speed improvement if your app no need am. Enable this site, disable the default, check the config, then reload:

sudo a2ensite testapp
sudo a2dissite 000-default
sudo apache2ctl configtest
sudo systemctl reload apache2

apache2ctl configtest suppose print Syntax OK. Na the a2dissite 000-default line people dey forget, and na why default page later fit still show as if e stuck, as explained for the failures section.

Step 6 - Prove say PHP dey run, then delete the proof

Put one-line PHP file for the app root:

echo "<?php phpinfo();" | sudo tee /var/www/testapp/info.php

Visit http://app.example.com/info.php. Correct result na the long purple-and-grey PHP Version 8.3.x table wey list your loaded modules, with the Server API line showing FPM/FastCGI. That last line confirm say requests dey pass through PHP-FPM, no be mod_php.

Now delete am immediately:

sudo rm /var/www/testapp/info.php

phpinfo() dey expose your exact PHP version, every loaded extension, file paths, and environment details. Anybody wey dey probe the server fit use this information find version wey get known security hole. Na test e be, no be feature. Delete am immediately after you see the page. If your browser offer to download info.php instead of showing the table, PHP no dey connect to Apache properly. Go to the failures section before you do anything else.

Step 7 - Create the app database and a least-privilege user

Open the database as the socket-authenticated root:

sudo mysql

Then create one database and one user wey get permission only for that database:

CREATE DATABASE appdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'appuser'@'localhost' IDENTIFIED BY 'a-long-random-password';
GRANT ALL PRIVILEGES ON appdb.* TO 'appuser'@'localhost';
FLUSH PRIVILEGES;
EXIT;

Three deliberate choices dey here. utf8mb4 na real four-byte UTF-8, but the old utf8 alias dey silently truncate emoji and some CJK characters, so always use utf8mb4. The grant dey on appdb.*, no be *.*: this user fit touch im own database and nothing else, so SQL-injection hole for the app no fit read tables from every other site. And 'appuser'@'localhost' restrict the account to connections wey start from the same box.

Test am as that user:

mysql -u appuser -p appdb

E go ask for the password, then e go put you for a MariaDB [appdb]> prompt. Notice say no -h flag dey there. Leave am out, and the client go connect through the local Unix socket, exactly as MariaDB dey count am as localhost. One thing wey you need know: for MySQL and MariaDB, localhost mean the Unix socket, while 127.0.0.1 mean TCP connection. For stock Ubuntu 24.04 MariaDB, the server still resolve TCP connection from 127.0.0.1 back to localhost, so both go match the account. But for servers wey get skip-name-resolve enabled (a common performance tweak, and the normal setup for many container images), dem dey match the two as different hosts. Then app wey connect to 127.0.0.1 go get refused with ERROR 1045 (28000): Access denied for user 'appuser'@'127.0.0.1' (using password: YES) even when the password correct.

So point your application to host localhost, user appuser, and database appdb. Never point am to root. PHP's mysqli and PDO both switch to the Unix socket when the host na the literal string localhost, so e match the account wey you just create. If framework insist on numeric TCP host, create the user to match how e really dey connect: 'appuser'@'127.0.0.1', or @'%' (together with firewall rule) only if e must reach the database from another machine.

Step 8 - Add HTTPS with Certbot

Serving login form through plain HTTP dey send passwords as clear text, and every modern browser dey mark the page as "Not secure". Certbot fit fix this with one command. Install am with Apache plugin:

sudo apt install -y certbot python3-certbot-apache
sudo certbot --apache

Certbot dey use two plugins here. The apache authenticator dey prove say you control the domain by briefly serving challenge file through your running Apache, while the apache installer dey rewrite your virtual host to add the 443 block, point am to the new certificate, and redirect all HTTP traffic go HTTPS by default. Certbot never ask redirect question since version 1.4.0 (May 2020), so pass --no-redirect if you need keep serving plain HTTP. Because you set real ServerName for Step 5, Certbot go detect the domain automatically. Certificates wey issue today last 90 days (Let's Encrypt go reduce its default lifetime to 64 days on 10 February 2027 and to 45 days on 16 February 2028), and the package go install systemd timer wey renew dem; verify the timer with sudo certbot renew --dry-run, wey suppose end with Congratulations, all simulated renewals succeeded.

For the full walk-through of the challenge, renewal timer, and DNS and firewall requirements, see the companion guide on issuing free Let's Encrypt TLS certificates with Certbot on Apache.

Backup, upgrade, and hardening

Back up the two things wey dey hold your state: the databases and the web root. Nightly logical dump na the simplest reliable approach, sudo sh -c 'mysqldump --all-databases --single-transaction | gzip > /root/db-$(date +%F).sql.gz', then copy am comot from the server. Wrapping the whole pipeline inside sudo sh -c dey important: without am, shell go run the > /root/... redirect as your own user and fail with Permission denied, because na only mysqldump inherit the sudo. --single-transaction dey give consistent snapshot of the InnoDB tables without locking dem. Pair am with a tar of /var/www and /etc/apache2/sites-available, and you fit rebuild the whole stack on fresh VPS from those files.

Upgrades na normal sudo apt update && sudo apt upgrade. The one wey dey cause problem na PHP version bump during release upgrade. Ubuntu 26.04 LTS ships PHP 8.5 as the default, and the php8.3-* packages wey you install here no dey for that release. So after the upgrade, you dey run php8.5-fpm, the socket don become /run/php/php8.5-fpm.sock, and your Apache config still dey point to the 8.3 socket. Enable the new conf (sudo a2enconf php8.5-fpm) and disable the old one, or your site go start return Primary script unknown after upgrade wey otherwise normal. Because PHP releases dey move faster than LTS distro, check the current PHP release notes instead of pinning patch version.

Two hardening steps worth doing on day one. First, put Fail2Ban wey dey monitor SSH for the server. Public VPS go receive automated login attempts within minutes, and small jail fit reduce thousands of attempts to only a few before e ban dem. Second, if you prefer manage Apache virtual hosts, MariaDB databases, and users through browser instead of editing files by hand, the Webmin web-based control panel dey on top of this exact stack and dey control the same config files wey you just write. Neither one replace understanding the components, but both reduce everyday work.

Wetin fit fail, and the messages wey you go see

The default page no go comot. You edit your virtual host, reload am, but browser still dey show "Apache2 Ubuntu Default Page" and the "It works!" banner. Apache dey serve the first virtual host wey match, and when no ServerName match the request, the config wey come first alphabetically go win; 000-default.conf dey sort before testapp.conf. Either the hostname for the request no match your ServerName, or you never run sudo a2dissite 000-default. Disable the default with sudo systemctl reload apache2, then confirm with apache2ctl -S. E go print the vhost map and show which config dey control the default. Clear the browser cache too; cached 200 response from the old page fit continue dey show.

A .php file downloads instead of running. You open info.php and browser downloads file wey contain the raw <?php source, or e show am as plain text instead of running am. Apache dey serve the file as static asset because PHP handler no attach, you skip sudo a2enmod proxy_fcgi or sudo a2enconf php8.3-fpm, or you no restart Apache afterwards. Run all three (Step 4) and reload. Confirm say the module load with apache2ctl -M | grep fcgi. The output suppose list proxy_fcgi_module. This one na source-code leak, no be cosmetic bug, so fix am before you put anything real for the server.

ERROR 1698 (28000): Access denied for user 'root'@'localhost'. You run mysql -u root or mariadb -u root without sudo. The root account dey use unix_socket auth, so e only go accept you when your OS user really be root. The fix na sudo mysql, no -u root, no password. This message na the expected behaviour when socket auth dey work correctly; e no mean say installation spoil.

ERROR 1045 (28000): Access denied for user 'appuser'@'127.0.0.1' from the application, even though the password correct. The account dey exist as 'appuser'@'localhost', but your app dey connect through TCP to 127.0.0.1 for server wey hostname resolution disable (skip-name-resolve). Because of this, MariaDB dey treat both as different hosts: localhost na Unix socket, while 127.0.0.1 na TCP. Point the app to host localhost so e go use the socket and match the account, or create another account 'appuser'@'127.0.0.1' if the framework fit only use TCP.

AH01071: Got error 'Primary script unknown' inside /var/log/apache2/testapp-error.log, while browser dey show File not found.. Apache hand the request to PHP-FPM, but FPM no fit find the script for the path wey Apache give am. Two common causes dey: the FPM socket for your config point to PHP version wey no dey installed, like when 8.3 socket remain configured after release upgrade move you to PHP 8.5; or the file really no dey there because DocumentRoot and the actual directory no agree. Check whether the socket dey exist with ls -l /run/php/. Confirm say DocumentRoot match where the file dey. Then restart both php8.3-fpm (or whichever PHP-FPM version actually dey installed) and apache2.

AH00558: apache2: Could not reliably determine the server's fully qualified domain name on every restart. This na harmless warning, no be error. Apache dey tell you say no global ServerName set. Silence am by writing ServerName your.domain into /etc/apache2/conf-available/servername.conf, then run sudo a2enconf servername.

(98)Address already in use: AH00072: make_sock: could not bind to address 0.0.0.0:80 when Apache dey start. Another web server already dey hold port 80, often na stray nginx from old experiment. Find am with sudo ss -ltnp | grep :80, then stop and disable the other service before you start Apache.

FAQ

mod_php or PHP-FPM - which one I suppose use?

Use PHP-FPM. mod_php dey embed interpreter for every Apache process and force the slow prefork MPM, so Apache still carry PHP overhead even when e dey serve static image. PHP-FPM dey run PHP as separate pool wey you fit tune independently, and Apache dey reach am through socket. E work with the faster threaded event MPM, and you fit move am unchanged go nginx later. Na the modern default; mod_php only make sense for legacy app wey depend on some in-process behaviour.

Why my browser dey download PHP file instead of running am?

Apache dey treat the .php file like static download because no PHP handler attach to am. For Ubuntu 24.04 with FPM, e mean say you miss one of sudo a2enmod proxy_fcgi, sudo a2enconf php8.3-fpm, or the Apache restart afterwards. Run all three and reload, then use apache2ctl -M | grep fcgi verify say proxy_fcgi_module dey listed. Until you fix am, the server dey leak source code, so treat am as urgent.

Why root access dey denied for MariaDB even with the correct password?

Because no password dey set, Ubuntu MariaDB dey authenticate the root account through unix_socket, and tie am to the operating-system root user. mysql -u root from normal shell dey return ERROR 1698 (28000): Access denied for user 'root'@'localhost' by design. Connect with sudo mysql instead, and create separate password-authenticated user for any application instead of reusing root.

How I fit add HTTPS to my LAMP site?

Install certbot and python3-certbot-apache, point a domain A record to the server, then run sudo certbot --apache. The apache authenticator dey prove domain control through your running Apache, and the installer dey rewrite the virtual host for port 443 and set up automatic renewal. The full Certbot and Apache walk-through dey explain the challenge, the renewal timer, and the common failure modes.