A LAMP stack on Ubuntu 24.04 is Apache, MariaDB and PHP on one Linux server, and it still runs most PHP applications you’ll host. Every package comes from the Ubuntu archive, so there are no third-party repositories to trust and security fixes arrive through the normal apt updates. That matters right now, because the October 2026 PHP2Shell fix for PHP is one of those updates.
We’ll install and configure each component, check that PHP carries the PHP2Shell fix, host a WordPress site on the stack and put it behind HTTPS. Every command below except the Let’s Encrypt certificate request was run again in October 2026 on a fresh Ubuntu 24.04.5 LTS server with PHP 8.3 from the Ubuntu archive, as a non-root sudo user, so the outputs are what you should see.
Installing LAMP on Ubuntu 24.04 (Noble Numbat)
Refer to the next steps to install and configure Apache, MariaDB and PHP on Ubuntu 24.04 (Noble Numbat). If you are building a new server on Ubuntu 26.04 instead, follow the LAMP stack guide for Ubuntu 26.04, which uses PHP-FPM.
A quick look at what each letter in the LAMP Stack does on this server:
- Linux: This is our Ubuntu 24.04 Linux system.
- Apache: Apache is a web server that sits in between the application and the web browser.
- MariaDB: This is a relational database management system used to store and manage dynamic content of your web app.
- PHP: This is a scripting language that is used in creating dynamic pages on the internet. Its code can be embedded within HTML and is able to connect to database for data storage and retrieval.
The installations performed in this article will only be directly applicable for any PHP based web application. If you’re planning to host any other web application, for example an application written in Django or Node.js skip the PHP part and find the right article in our website. But the Apache and MariaDB parts remain constant if your app depends on them.
Step 1: Install Apache web server
Apache httpd server package is available in Ubuntu APT repositories as apache2. Install the web server on the system using the commands below.
sudo apt update && sudo apt install -y apache2
The package starts and enables the service on its own, so check its status straight away.
systemctl status apache2 --no-pager
It should read enabled on the Loaded line and active (running) on the Active line:
● apache2.service - The Apache HTTP Server
Loaded: loaded (/usr/lib/systemd/system/apache2.service; enabled; preset: enabled)
Active: active (running) since Thu 2026-10-22 15:53:18 UTC; 1s ago
Docs: https://httpd.apache.org/docs/2.4/
Main PID: 999 (apache2)
Tasks: 55 (limit: 1204)
Memory: 5.1M (peak: 5.3M)
CPU: 36ms
If UFW is enabled on your server, or you plan to enable it, open the web ports now. Allow OpenSSH first, because enabling UFW without that rule drops your SSH session and locks you out of a remote server. If that happens anyway, log in through your provider’s web console and run sudo ufw allow OpenSSH there. Cloud servers behind a provider firewall or security group need ports 80 and 443 opened there as well:
sudo ufw allow OpenSSH
sudo ufw allow 'Apache Full'
sudo ufw enable
sudo ufw status
ufw enable asks you to confirm with y, because the change may disrupt existing SSH connections; with the OpenSSH rule already in place it doesn’t. It then reports Firewall is active and enabled on system startup. Apache Full covers ports 80 and 443, and both profiles show up as allowed:
Status: active
To Action From
-- ------ ----
OpenSSH ALLOW Anywhere
Apache Full ALLOW Anywhere
OpenSSH (v6) ALLOW Anywhere (v6)
Apache Full (v6) ALLOW Anywhere (v6)
A default page is located in /var/www/html/index.html and is served on the server IP over HTTP. Hit your server IP address in your browser to check the page.

That page only proves Apache answers on port 80. PHP comes next.
Step 2: Install PHP and Extensions
PHP works on the server-side in executing the scripts and giving response to the web server. We can install the default version of PHP available in the OS upstream repositories with the following command:
sudo apt install -y php
Check the PHP version after installation, because the CLI and the Apache module come from the same build and this is the quickest way to see which branch you got.
php --version
Ubuntu 24.04 ships PHP 8.3. The build date moves with every Ubuntu security update, but it doesn’t prove a build is patched, and Step 3 shows the check that does:
PHP 8.3.6 (cli) (built: Oct 19 2026 15:16:58) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.3.6, Copyright (c) Zend Technologies
with Zend OPcache v8.3.6, Copyright (c), by Zend Technologies
Apache runs PHP through the libapache2-mod-php module. On a server where apache2 is already installed, the php package pulls in libapache2-mod-php8.3 by itself, switches Apache to the prefork MPM and enables the module, so this line only makes it explicit. That also means PHP runs inside every Apache worker process, which matters later: a PHP security update only takes effect once Apache restarts, and that is the restart Step 3 checks for.
sudo apt install -y libapache2-mod-php
apt reports libapache2-mod-php as newly installed, but it is only the metapackage pointing at the 8.3 module that is already in place.
Installing PHP extensions
PHP extensions are libraries created to enhance PHP functionalities. A PHP extension is loaded at runtime and it provides classes, functions, and other features usable in your PHP scripts.
Install the modules most PHP applications expect. WordPress, for example, refuses to run without mysqli from php-mysql and complains about image uploads without gd, so install them before the application rather than after its first error.
sudo apt install -y php-{cli,curl,json,mysql,zip,pear,gd,mbstring,xml,bcmath}
To check loaded PHP modules use:
php -m
The list includes curl, gd, mbstring, mysqli and pdo_mysql, which WordPress needs later.
[PHP Modules]
bcmath
calendar
Core
ctype
curl
date
dom
exif
FFI
fileinfo
filter
ftp
gd
gettext
hash
iconv
json
libxml
mbstring
mysqli
mysqlnd
openssl
pcntl
pcre
PDO
pdo_mysql
Phar
posix
random
...
Any module missing from this list means its package didn’t install, so rerun the apt install line and read its errors before you go further.
Step 3: Check PHP for the PHP2Shell fix
PHP2Shell is the name for two critical remote code execution bugs in PHP itself, CVE-2026-109618 and CVE-2026-109619, both rated CVSS v4.0 9.2 in php.net’s 20 October pre-announcement and fixed upstream on 21 October 2026 in PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35, according to the PHP security advisory. Ubuntu 24.04 already ships the fix for its PHP 8.3, but as a backport in 8.3.6-0ubuntu0.24.04.13, so the version stays 8.3.6 and php -v can’t tell a patched server from a vulnerable one. PHP confirmed full code execution against Apache with mod_php, which is exactly how this stack runs PHP.
The package changelog is the reliable check, so search it for both CVE IDs:
zgrep -E 'CVE-2026-10961[89]' /usr/share/doc/php8.3-common/changelog.Debian.gz
A patched server prints the patch line and both CVE IDs. A fresh install from Step 2 pulls the fixed build, so if you see this you can move on to Step 4:
- debian/patches/CVE-2026-109618_109619.patch: Avoid state corruption via
- CVE-2026-109618
- CVE-2026-109619
Minimized Ubuntu images strip /usr/share/doc, so the changelog isn’t there to search. The installed package version works everywhere, because dpkg can compare it with the fixed build directly:
dpkg --compare-versions "$(dpkg-query -W -f='${Version}' php8.3-common)" ge 8.3.6-0ubuntu0.24.04.13 && echo patched || echo VULNERABLE
It prints patched on a current server and VULNERABLE on our rolled-back one below, which is the answer you need without reading any changelog.
If the zgrep printed nothing, your installed build is older than 8.3.6-0ubuntu0.24.04.13, which is the case on an existing server that hasn’t been updated since the fix landed. To test that path, we rolled our server back to the 8.3.6-0maysync1 build from the release pocket, and the same zgrep printed nothing. The upgrade glob covers the php8.3 metapackage and every installed php8.3 extension. libapache2-mod-php8.3 is named on its own because it is the copy of PHP that Apache actually loads, and libphp8.3-embed because the glob doesn’t match it; apt skips either one if it isn’t installed:
sudo apt update
sudo apt install --only-upgrade -y 'php8.3*' libapache2-mod-php8.3 libphp8.3-embed
apt prints a “Skipping” line for each matching package that isn’t installed, then upgrades the rest. On our server that was 13 packages:
The following packages will be upgraded:
libapache2-mod-php8.3 php8.3 php8.3-bcmath php8.3-cli php8.3-common
php8.3-curl php8.3-gd php8.3-mbstring php8.3-mysql php8.3-opcache
php8.3-readline php8.3-xml php8.3-zip
13 upgraded, 0 newly installed, 0 to remove and 1 not upgraded.
Note when the packages changed, because the restart check below compares against that time. dpkg logs every package change and rotates the log, so search the old copies too:
zgrep -hE ' (install|upgrade) (php8.3-common|libapache2-mod-php8.3):' /var/log/dpkg.log* | sort | tail -2
Both packages moved from the old build to the fixed one.
2026-10-22 16:11:56 upgrade libapache2-mod-php8.3:amd64 8.3.6-0maysync1 8.3.6-0ubuntu0.24.04.13
2026-10-22 16:11:56 upgrade php8.3-common:amd64 8.3.6-0maysync1 8.3.6-0ubuntu0.24.04.13
With mod_php, PHP lives inside the Apache processes, so Apache is the service to restart. The package restarted Apache by itself in our test, but restart it anyway and confirm, because an Apache process that started before the upgrade still has the vulnerable code in memory:
sudo systemctl restart apache2
systemctl show -p ActiveEnterTimestamp apache2
The timestamp must be later than the upgrade time from dpkg.log. If it is earlier, Apache never restarted and still serves pages with the old PHP, so run the restart again and check that systemctl status apache2 reports active. dpkg logged ours at 16:11:56 UTC, and Apache came back after it:
ActiveEnterTimestamp=Thu 2026-10-22 16:12:14 UTC
Run the zgrep again and it lists both CVE IDs. If you later move this site to PHP-FPM, restart php8.3-fpm after PHP updates too, and restart any long-running PHP CLI workers (queue workers under supervisor or systemd), since they keep the old code until they restart.
Other Ubuntu releases differ, and the free archive is not patched on all of them. These are the native PHP builds each release served on 22 October 2026:
- Ubuntu 26.04: php8.5
8.5.4-0ubuntu1.5, patched - Ubuntu 22.04: php8.1
8.1.2-1ubuntu2.28, patched - Ubuntu 20.04: php7.4
7.4.3-4ubuntu2.29, not patched (fix in Ubuntu Pro only)
A LAMP box on 20.04 without Pro has no free patched PHP, and the Ondrej PPA no longer publishes PHP for 20.04 either, so rebuild it on 24.04 with this guide. If you run PHP from the Ondrej PPA on 24.04 instead of the Ubuntu archive, its noble builds of PHP 7.4 through 8.5 carry the fix, and the PHP 8.2 on Ubuntu guide shows the same changelog check for PPA packages.
Step 4: Install MariaDB Database Server
MariaDB is a RDMS forked from MySQL but has matured to be one of the most databases used in the world. Install MariaDB with the following commands:
sudo apt install -y mariadb-server
Secure MariaDB database server after installation by running the mariadb-secure-installation script.
sudo mariadb-secure-installation
The script asks a short series of questions about your MariaDB server. Press Enter for the current root password (there is none yet), answer n to switching to unix_socket authentication because root already uses it, then set a root password, remove anonymous users, disallow remote root login, remove the test database and reload the privilege tables. Root ends up accepting either the password or a local sudo login over the socket, which is why sudo mariadb without a password still works later in this guide.
Connect with the mariadb client and enter the root password when it asks:
sudo mariadb -u root -p
The banner shows the MariaDB 10.11 build from the Ubuntu archive. Type quit to leave the prompt:
Enter password:
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 40
Server version: 10.11.14-MariaDB-0ubuntu0.24.04.1 Ubuntu 24.04
Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
MariaDB [(none)]>
If you forget that password, root still accepts the socket login, so sudo mariadb without -p gets you in to reset it.
Step 5: Hosting PHP app using MariaDB and Apache
You can use Apache virtual host feature to host more than one websites on a single server. We can test our setup with the installation of WordPress CMS.
Open the MariaDB console as root over the local socket.
sudo mariadb
Create the database and a user that can only reach it. Replace StrongDBPassw0rd with your own password:
CREATE DATABASE wordpress;
GRANT ALL ON wordpress.* TO wordpress@localhost IDENTIFIED BY "StrongDBPassw0rd";
FLUSH PRIVILEGES;
QUIT
Download and extract the latest WordPress archive. The latest.tar.gz URL always points at the current release, so there is no version to edit in the command.
wget https://wordpress.org/latest.tar.gz
tar xvf latest.tar.gz
Move the folder into the /var/www directory.
sudo mv wordpress /var/www
Create the WordPress configuration file from the sample that ships in the archive. The installer can write this file for you, but doing it here keeps the database password out of the browser session and lets you check it before the site goes live.
sudo mv /var/www/wordpress/wp-config-sample.php /var/www/wordpress/wp-config.php
Edit the file and set the database connection details.
sudo vim /var/www/wordpress/wp-config.php
Change the three placeholder values in the database section to the name, user and password you created. Leave DB_HOST and the utf8mb4 charset as they are:
// ** Database settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
define( 'DB_NAME', 'wordpress' );
/** Database username */
define( 'DB_USER', 'wordpress' );
/** Database password */
define( 'DB_PASSWORD', 'StrongDBPassw0rd' );
/** Database hostname */
define( 'DB_HOST', 'localhost' );
/** Database charset to use in creating database tables. */
define( 'DB_CHARSET', 'utf8mb4' );
The files still belong to your login user after the move. Hand them to www-data, the user Apache runs as, so WordPress can write uploads and install plugins and updates:
sudo chown -R www-data:www-data /var/www/wordpress
Disable the default site. With it gone, the WordPress virtual host becomes the first one Apache loads, so requests by IP address land on WordPress instead of the Ubuntu test page.
sudo a2dissite 000-default.conf
Create VirtualHost file for wordpress.
sudo vim /etc/apache2/sites-available/wordpress.conf
Paste this virtual host, replacing wordpress.example.com and the admin email with your own. The Directory block lets the .htaccess file WordPress writes for pretty permalinks take effect:
<VirtualHost *:80>
ServerAdmin [email protected]
ServerName wordpress.example.com
DocumentRoot /var/www/wordpress
<Directory /var/www/wordpress>
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/wordpress_error.log
CustomLog ${APACHE_LOG_DIR}/wordpress_access.log combined
</VirtualHost>
Enable the rewrite module that permalinks need, enable the site and check the configuration before restarting:
sudo a2enmod rewrite
sudo a2ensite wordpress.conf
sudo apache2ctl configtest
The test must end with Syntax OK. An AH00558 line about the server’s fully qualified domain name above it is only a warning and doesn’t stop Apache. Restart apache2 web server:
sudo systemctl restart apache2
Until DNS points at the server, map the domain on your own workstation. Open the hosts file there, not on the server:
- Linux:
/etc/hosts - Windows:
C:\Windows\System32\drivers\etc\hosts
Add a line with the server’s IP address and the domain name.
10.0.1.50 wordpress.example.com
Load the web page in your browser. If successful you should get a page similar to below.

Pick the language, then fill in the site title and the admin account on the next screen:

Our run finished the installer with a Success! page and 12 tables in the wordpress database, which proves Apache, PHP and MariaDB are talking to each other.
Step 6: Enabling SSL / TLS on Apache
A public site needs HTTPS, and Let’s Encrypt with certbot is the simplest way to get it. Certbot proves you own the domain over plain HTTP, so two things must be in place first: an A record for wordpress.example.com pointing at the server’s public IP at your DNS provider, and port 80 reachable from the internet. The ServerName in the virtual host must match that domain, because certbot finds the right site by it.
sudo apt install -y certbot python3-certbot-apache
sudo certbot --apache -d wordpress.example.com
Certbot asks for an email address and agreement to its terms, then obtains the certificate and writes an HTTPS virtual host for the site next to wordpress.conf. We installed certbot and its Apache plugin on the test server but did not request a certificate, because a lab box has no public DNS name.
If you already have a certificate from another CA, configure it by hand instead. Enable the SSL module:
sudo a2enmod ssl
Then open the same site file again.
sudo vim /etc/apache2/sites-available/wordpress.conf
Replace its contents with an HTTP virtual host that only redirects, plus the HTTPS one. Put your certificate chain and key at the two paths shown, or change the paths to where yours live:
<VirtualHost *:80>
ServerName wordpress.example.com
Redirect permanent / https://wordpress.example.com/
</VirtualHost>
<VirtualHost *:443>
ServerAdmin [email protected]
ServerName wordpress.example.com
DocumentRoot /var/www/wordpress
<Directory /var/www/wordpress>
AllowOverride All
Require all granted
</Directory>
SSLEngine on
SSLCertificateFile /etc/ssl/certs/wordpress.example.com.crt
SSLCertificateKeyFile /etc/ssl/private/wordpress.example.com.key
ErrorLog ${APACHE_LOG_DIR}/wordpress_error.log
CustomLog ${APACHE_LOG_DIR}/wordpress_access.log combined
</VirtualHost>
Check the syntax and restart Apache.
sudo apache2ctl configtest
sudo systemctl restart apache2
We tested this file with a self-signed certificate at those paths: port 80 answered with a 301 to the HTTPS URL and the login page loaded over HTTPS with a 200. If you finished the WordPress installer over HTTP, change both addresses under Settings, General to https:// so WordPress stops generating plain HTTP links.
Conclusion
You now have Apache serving a WordPress site from MariaDB, with PHP 8.3 from the Ubuntu archive running inside Apache through mod_php and HTTPS in front of it. Keep the PHP packages and libapache2-mod-php8.3 on the regular apt upgrade cycle and restart Apache after each PHP update, because that is the only way the PHP2Shell fix, and the next one, reaches the running server. Schedule a nightly mariadb-dump of the wordpress database before the site holds anything you care about.