SSD Nodes Learn
How to do am Matt ConnorBy Matt Connor · Updated 2026-07-24

how to install LAMP stack for Ubuntu 24.04

Learn how to set up Apache, MariaDB, and PHP 8.3 with PHP-FPM on Ubuntu 24.04. We show you how to fix blank page errors and set up Certbot for HTTPS.

Wetin you dey build

LAMP stack na four things wey dey work together for one Ubuntu 24.04 server: Linux for base, Apache for answer HTTP, MariaDB for hold data, and PHP 8.3 for run code. When you finish, you go get name-based virtual host wey dey serve real application directory, database with special user wey get small permission only, PHP wey connect to Apache through PHP-FPM, and free Let's Encrypt certificate for top.

The install itself na four apt commands. Almost everything for this guide na how to connect all these pieces, and the small mistakes wey dey make brand-new stack show blank page, download your source code for browser, or lock you out for the database wey you just install. Every one of dem get signature wey you fit recognize, and we don list dem below with the exact text wey you go see.

Prerequisites and the honest gotchas

Assume say you get fresh Ubuntu 24.04 KVM VPS with sudo user or root, and public IPv4 address. Minimal stack dey run well for 1 GB of RAM; but e better make you give am 2 GB before you put real database-backed application for am. This one na because MariaDB default buffers plus some PHP-FPM workers go chop the first gigabyte fast-fast.

Two things must dey correct before Certbot go work for end, so arrange dem now. You need domain name wey get A record wey point to VPS public IP — Let's Encrypt dey use HTTP take validate that name, and you no fit get certificate with only IP address. Also, ports 80 and 443 must dey open for internet. For many providers, this one mean say you must open dem for network firewall for control panel plus for ufw for inside the box. DNS changes fit take up to one hour to propagate, so set the A record first and e go ready 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 like dis:

systemctl status apache2

You must see one line wey get active (running). Now open http://YOUR_SERVER_IP/ for browser. The Apache2 Ubuntu Default Page wey get big "It works!" banner na di correct result — e mean say Apache dey work well, e no be mistake. Dat page dey for /var/www/html/index.html and 000-default.conf dey serve am. You go disable dem both later; for now, wetin you see na wetin you suppose see.

If di page no load at all but systemctl show say di process dey run, firewall dey block am. Dat na di next step.

Step 2 - Open the firewall for HTTP and HTTPS

apache2 package 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 dat one you need, because you dey add TLS for end.

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

Allow OpenSSH before you run ufw enable. ufw default na to block all incoming traffic, and if you enable am without SSH rule, you go lock yourself out as soon as e start working — you go fit stay for current session but you no go fit reconnect again. Confirm am with sudo ufw status; you want make OpenSSH, Apache Full, and dem v6 equivalents all show ALLOW.

Step 3 - Install MariaDB and secure am

sudo apt install -y mariadb-server
systemctl status mariadb

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

sudo mysql_secure_installation

E better make you read the prompts instead of just to dey mash Enter. When e ask for the current root password, press Enter — e no get any password yet. When e ask "Switch to unix_socket authentication?", the answer no go change anything because dem don already enable am for this package, so press n. Say n to "Change the root password?" because of the reason wey dey next paragraph, then answer Y to the rest: remove anonymous users, disallow remote root login, drop the test database, and reload privilege tables.

Dis part na wetin dey confuse everybody. For Ubuntu's MariaDB, the root database account dey use unix_socket authentication, no be password. Dat mean say the database dey trust de operating-system user wey you don already authenticate as. So, dis one go work from a root shell:

sudo mysql

...and e go drop you for a MariaDB [(none)]> prompt without asking for password. If you run de same command as unprivileged user, e go refuse am, and dat na de whole point: access to de database root dey tied to sudo on de box, and no password to steal, phish, or brute-force. Dis one secure pass password, so leave am alone. De rule wey follow from dis one na: never point any application to de root account. Create one dedicated user for every application (Step 7), because any app wey dey connect over TCP with username and password no fit use socket auth anyway, and you want make each app limit 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 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

Look wetin no dey for that list: libapache2-mod-php. That old package dey put PHP interpreter inside every Apache process. E simple, but every worker dey carry PHP copy whether e dey serve script or static image. The two dey share one lifecycle, and e only work with Apache prefork MPM — wey be the least efficient one. PHP-FPM instead dey run PHP as its own pool of processes wey Apache dey talk to through socket. Apache fit use threaded event MPM for static files and only send PHP requests go there. The pool dey run independent of the web server, and the same FPM setup go work if you use nginx for front later. Na why e be the current default.

Apache dey reach FPM through the proxy_fcgi module. Enable am, enable the config wey the FPM package drop, and 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 get the rule wey dey route PHP files to the FPM socket. The main part dey match any .php file and send am to the 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>

No edit that file; e come correct. But if you know the socket path, e go help you fix "PHP downloads instead of running" and "Primary script unknown" errors later — both of dem dey happen because Apache and FPM no agree about that socket or the file behind am.

Step 5 - Name-based virtual host for your app

Name-based virtual hosting make one IP serve many sites; Apache dey pick the site by the Host: header inside the request. Create directory for the app, far away 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 important. Apache and PHP-FPM both dey run as www-data user for Ubuntu, so files wey web server must read — and directories wey app must write to, like uploads folder — suppose be owned by www-data. If you wan edit files as your login user, one common way na to own the files yourself and add your user to the www-data group; for plain deploy, www-data:www-data na the easiest way.

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 dey stop Apache from listing the directory when no index file dey — if you no do this, visitors go fit browse your source tree. AllowOverride All dey make .htaccess file work, wey most PHP applications dey expect for pretty URLs; drop am for None if your app no need am, so the speed go increase small. Enable this site, disable the default, check the config, and reload:

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

apache2ctl configtest suppose print Syntax OK. The a2dissite 000-default line na the one people dey forget, and na why the default page dey appear to be stuck later — we go explain am for failures section.

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

Drop one single-line PHP file inside the app root:

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

Visit http://app.example.com/info.php. If everything correct, you go see one long purple-and-grey PHP Version 8.3.x table wey list all your loaded modules, and the Server API line go show FPM/FastCGI. That last line dey 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 — na big gift for anybody wey dey probe server for version wey get known hole. Na test be this, no be feature. Delete am the moment you don see the page. If instead of the table, your browser ask you to download info.php, e mean say PHP no connect to Apache; jump 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 scoped to exactly 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 — the old utf8 alias dey silently truncate emoji and some CJK characters, so always use utf8mb4. The grant na for appdb.*, no be *.*: this user fit touch its own database and nothing else, so if SQL-injection happen for the app, the attacker no fit read tables for other sites. And 'appuser'@'localhost' restrict the account to connections wey dey come from the box itself.

Test am as that user:

mysql -u appuser -p appdb

E go ask for the password and drop you for MariaDB [appdb]> prompt. Notice say there is no -h flag — leave am off and the client go connect over the local Unix socket, wey na wetin MariaDB count as localhost. One thing wey you must know: for MySQL and MariaDB, localhost mean the Unix socket and 127.0.0.1 mean a TCP connection. For stock Ubuntu 24.04 MariaDB, the server still resolve TCP connection from 127.0.0.1 back to localhost, so both match the account — but for servers wey skip-name-resolve dey enabled (wey many people dey use for performance, and na how many container images dey), the two na different hosts, so if app dey dial 127.0.0.1, the server go refuse am with ERROR 1045 (28000): Access denied for user 'appuser'@'127.0.0.1' (using password: YES) even if password correct.

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

Step 8 - Add HTTPS with Certbot

If you dey serve login form for plain HTTP, password go dey travel for clear text, and every modern browser go show "Not secure" for the page. Certbot fit fix that one 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 for here. The apache authenticator dey prove say you control the domain by serving one challenge file through your running Apache, and the apache installer go rewrite your virtual host to add the 443 block, point am to the new certificate, and redirect all HTTP traffic to HTTPS by default — since Certbot 2.0, dem no dey ask about redirect again; pass --no-redirect if you want make plain HTTP still dey work. Because you don set real ServerName for Step 5, Certbot go detect the domain automatically. Certificates dey last 90 days and the package dey install one systemd timer wey go renew dem; check the timer with sudo certbot renew --dry-run, wey suppose end with Congratulations, all simulated renewals succeeded.

For full explanation about the challenge, the renewal timer, and the DNS and firewall requirements, see the companion guide for issuing free Let's Encrypt TLS certificates with Certbot on Apache.

Backups, upgrades, and hardening

Back up the two things wey dey hold your state: the databases and the web root. To do nightly logical dump na the simplest way to get reliable result — use sudo sh -c 'mysqldump --all-databases --single-transaction | gzip > /root/db-$(date +%F).sql.gz', then copy am off the box. E important make you wrap the whole pipeline inside sudo sh -c: if you no do am, the shell go run the > /root/... redirect as your own user and e go fail with Permission denied, because na only mysqldump inherit the sudo. --single-transaction dey give you consistent snapshot of the InnoDB tables without lock dem. Pair am with tar of /var/www and /etc/apache2/sites-available, and you fit rebuild the whole stack for new VPS from those files.

Upgrades na normal sudo apt update && sudo apt upgrade. The one wey dey cause wahala na PHP version bump — when future Ubuntu move the default to 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 dey point at the 8.3 socket. Enable the new conf (sudo a2enconf php8.4-fpm) and disable the old one, or your site go start to return Primary script unknown even after routine upgrade. Because PHP release dey move faster than LTS distro, check the current PHP release notes instead of to pin one patch version.

Two hardening steps worth doing on day one. First, put Fail2Ban wey dey watch SSH for the box — public VPS dey get automated login attempts within minutes, and small jail fit turn thousands of tries into just handful before ban happen. Second, if you prefer to manage Apache virtual hosts, MariaDB databases, and users through browser instead of to hand-edit files, the Webmin web-based control panel dey sit on top of this exact stack and dey drive the same config files wey you just write. Neither one dey replace understanding of the pieces, but both dey reduce day-to-day friction.

Failure modes, with the strings you will see

The default page no go commot. You edit your virtual host, reload, but browser still dey show "Apache2 Ubuntu Default Page" and that "It works!" banner. Apache dey serve the first virtual host wey match, and if no ServerName match the request, the one wey first for alphabet go win — 000-default.conf dey come before testapp.conf. Either the host name for request no match your ServerName, or you never run sudo a2dissite 000-default. Disable the default, sudo systemctl reload apache2, and confirm with apache2ctl -S, wey go print the vhost map and show you which config dey carry the default. Clear your browser cache too; old 200 cache fit dey stay for there.

.php file go download instead of to run. You open info.php and browser go download file wey get raw <?php source, or e go show am as plain text, instead of to run 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 after. Run all three (Step 4) and reload. Confirm say module load with apache2ctl -M | grep fcgi, wey suppose list proxy_fcgi_module. This na source-code leak, no be just cosmetic bug, so fix am before you put anything real for 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 if your OS user na root. The fix na sudo mysql — no -u root, no password. This message na how socket auth dey behave when e dey work well, no be say install don spoil.

ERROR 1045 (28000): Access denied for user 'appuser'@'127.0.0.1' from the application, with the right password. The account dey exist as 'appuser'@'localhost', but your app dey connect over TCP to 127.0.0.1 for server wey host-name resolution don shut (skip-name-resolve), so MariaDB dey see dem as two different hosts — localhost na the Unix socket, 127.0.0.1 na TCP. Point the app to host localhost so e go use the socket and match the account, or create second account 'appuser'@'127.0.0.1' if the framework must use TCP.

AH01071: Got error 'Primary script unknown' in /var/log/apache2/testapp-error.log, with the browser showing File not found.. Apache don hand the request to PHP-FPM, but FPM no fit find the script for the path wey Apache give am. Two common reasons: the FPM socket for your config dey point to PHP version wey no dey install (php8.4 socket after upgrade when only 8.3 dey run), or the file no dey there because DocumentRoot and the real directory no match. Check if socket dey with ls -l /run/php/, confirm say DocumentRoot match where the file dey, and restart both php8.3-fpm and apache2.

AH00558: apache2: Could not reliably determine the server's fully qualified domain name every time you restart. This na harmless warning, no be error — Apache dey tell you say no global ServerName set. Silence am by write ServerName your.domain inside /etc/apache2/conf-available/servername.conf and 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 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 put interpreter inside every Apache process and e dey force the slow prefork MPM, so Apache go carry PHP overhead even when e dey serve static image. PHP-FPM dey run PHP as separate pool wey you fit tune independently, Apache go reach am through socket, e dey work with the faster threaded event MPM, and e go easy to move to nginx later. Na the modern default; mod_php only make sense if you get old legacy app wey depend on some in-process behaviour.

Why my browser dey download PHP file instead of to run am?

Apache dey treat the .php file as static download because no PHP handler dey attached 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 you no restart Apache. Run all three and reload, then use apache2ctl -M | grep fcgi check if proxy_fcgi_module dey list. If you no fix am, server dey leak source code, so treat am as urgent matter.

Why root access dey denied for MariaDB even when I get correct password?

Because no password dey — Ubuntu's MariaDB dey authenticate the root account by unix_socket, e dey tie am to the operating-system root user. mysql -u root from normal shell go return ERROR 1698 (28000): Access denied for user 'root'@'localhost' by design. Use sudo mysql connect instead, and create separate user wey get password for any application instead of to dey use root.

How I go add HTTPS to my LAMP site?

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

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