SSD Nodes Learn Hosting plans →
How to do am Matt ConnorBy Matt Connor · Updated 2026-08-07

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

If login form dey run for plain HTTP, password dey pass as clear text, and every modern browser go mark the page as "Not secure". Certbot fit fix this with one command. Install am with the 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. Then 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. Since Certbot 2.0, no redirect question dey again; pass --no-redirect if you need continue serving plain HTTP. Because you set real ServerName for Step 5, Certbot go detect the domain automatically. Certificates dey last 90 days, and the package dey install systemd timer wey renew dem. Verify the timer with sudo certbot renew --dry-run; the output 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 how to issue free Let's Encrypt TLS certificates with Certbot for Apache.

Backups, upgrades, and hardening

Back up the two things wey 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. To wrap the whole pipeline inside sudo sh -c 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 provide consistent snapshot of the InnoDB tables without locking dem. Join am with a tar of /var/www and /etc/apache2/sites-available, and you fit rebuild the whole stack for a fresh VPS from those files.

Upgrades na normal sudo apt update && sudo apt upgrade. The one wey fit cause problem na a PHP version bump, when future Ubuntu move the default go PHP 8.4, apt fit install php8.4-fpm alongside 8.3, the socket go become /run/php/php8.4-fpm.sock, and your Apache config still point to the 8.3 socket. Enable the new conf (sudo a2enconf php8.4-fpm) and disable the old one, or your site go start return Primary script unknown after upgrade wey suppose ordinary. Because PHP releases dey move faster than an LTS distro, check the current PHP release notes instead of pinning a patch version.

Two hardening steps dey worth doing on day one. First, put Fail2Ban wey dey monitor SSH for the server. Public VPS dey receive automated login attempts within minutes, and small jail fit turn thousands of tries to just few before ban. 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 this exact stack and dey control the same config files wey you just write. Neither one replace understanding the components, but both reduce day-to-day friction.

Failure modes, plus the strings wey you go see

The default page no wan commot. 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. 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, sudo systemctl reload apache2, then confirm with apache2ctl -S. E go print the vhost map and show which config dey own the default. Clear browser cache too, because cached 200 response from the old page fit continue to show.

A .php file dey download instead of running. You open info.php and browser download file wey contain the raw <?php source, or 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 module load with apache2ctl -M | grep fcgi. E 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 go only accept you when your OS user actually be root. The fix na sudo mysql, no -u root, no password. This message na the expected behaviour of socket auth wey dey work correctly; e no mean say install don 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). So MariaDB dey treat the two 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' for /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 install (a php8.4 socket after upgrade, while na only 8.3 dey run), or the file truly no dey there because DocumentRoot and the real 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 and apache2.

AH00558: apache2: Could not reliably determine the server's fully qualified domain name every time Apache restart. This one na harmless warning, no be error. Apache dey tell you say no global ServerName set. To silence am, write ServerName your.domain inside /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 start. Another web server don already hold port 80, often na stray nginx from previous 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.

#lamp#apache#mariadb#php-fpm#ubuntu