
I once spent four hours at 2 a.m. troubleshooting a “database connection refused” error on a client’s fresh Ubuntu box, only to discover MySQL had bound to ::1 instead of 127.0.0.1 because of a default config change nobody warned me about. The fix took ninety seconds once I found it. The four hours before that were pure ego damage. That kind of thing is exactly why I don’t trust “just run apt install” tutorials anymore, and it’s why this guide spends extra time on the parts that actually break in production rather than padding out the easy stuff.
Ubuntu 26.04 LTS (“Resolute Raccoon”), released in April 2026, ships with meaningfully different defaults than 24.04 — MySQL jumped from the 8.0 branch to 8.4 LTS, PHP defaults to 8.5.2, and the kernel moved to the 7.0 line. If you’re used to the 24.04 LAMP workflow, a few things will trip you up, particularly around MySQL’s authentication plugin behavior and PHP-FPM socket permissions under systemd 259. This guide walks through installing Apache, MySQL, and PHP on Ubuntu 26.04 LTS with an eye toward what actually goes wrong on a live server, not just a lab VM you’ll tear down tomorrow. By the end you’ll have a hardened, tuned LAMP stack, know how to verify each layer independently, and have a troubleshooting reference for the errors you’ll actually see in your logs.
Prerequisites
Before touching a single package, get these right. Skipping this section is how people end up debugging permission errors three steps later that have nothing to do with permissions.
- A fresh Ubuntu 26.04 LTS server (minimum 1GB RAM for light traffic, 4GB+ recommended if you’re running WordPress or Laravel with any real concurrency)
- A non-root user with sudo privileges — don’t SSH in as root, ever, and if you’re inheriting a server that does, fix that first
- SSH access with key-based auth already configured
- A domain name pointed at the server (optional for setup, required later for SSL)
- Basic comfort with
nanoorvim, whichever you actually prefer — I won’t pretend one is objectively correct here, that fight is not worth having
Run a full update first. This isn’t optional ceremony:
sudo apt update && sudo apt upgrade -y
The reason order matters here: apt update refreshes the package index against the repos, apt upgrade then installs newer versions of what’s already there. Run upgrade without update first and you’re upgrading against a stale index, which on a fresh cloud image can leave you with mismatched library versions for anything you install next.
Step 1: Install Apache
sudo apt install apache2 -y
Ubuntu 26.04 pulls Apache from the 2.4.x branch by default, same major version as 24.04 but with security backports applied. Once installed, check that the service is actually running rather than just assuming the install script started it correctly:
sudo systemctl status apache2
You want to see active (running). If it’s not, check sudo journalctl -xeu apache2 before you do anything else — don’t just restart it blindly and hope.
Enable it to survive reboots:
sudo systemctl enable apache2
Now open the firewall. Ubuntu ships with UFW (uncomplicated firewall), and it’s not enabled by default on most cloud images, which honestly bugs me. Enable it and allow the Apache profile before you allow SSH gets locked out — I’ve seen a junior admin enable UFW, forget to allow port 22, and get locked out of a remote box with no console access. Don’t be that person.
sudo ufw allow OpenSSH
sudo ufw allow 'Apache Full'
sudo ufw enable
Apache Full opens both 80 and 443, which is fine even before you have SSL configured — Apache just won’t have anything listening on 443 yet. Verify by hitting the server’s public IP in a browser; you should get the default Apache splash page.
Step 2: Install MySQL 8.4
This is where 26.04 diverges from what you might remember. Ubuntu 26.04 defaults to MySQL 8.4 LTS instead of 8.0, and 8.4 changed some defaults around replication and authentication that matter if you’re migrating an existing app.
sudo apt install mysql-server -y
Check the version to confirm what you actually got, since repo contents can shift between point releases:
mysql --version
You should see something in the 8.4.x range. Now run the security script. Don’t skip this. I don’t care how many times you’ve done this before, running mysql_secure_installation on every fresh install is non-negotiable and one of the few absolutes in this whole guide.
sudo mysql_secure_installation
It will walk you through setting a validate-password policy, removing anonymous users, disabling remote root login, and removing the test database. Say yes to all of it on a production box. The only time I’d argue for skipping the password validation component is a throwaway dev VM that never touches the internet, and even then I usually don’t bother arguing.
Here’s the gotcha that ate my evening once: on fresh MySQL 8.4 installs, bind-address in /etc/mysql/mysql.conf.d/mysqld.cnf sometimes defaults to 127.0.0.1 for IPv4-only binding, but if your app or ORM is configured to connect via ::1 (IPv6 localhost) because of how /etc/hosts resolves localhost, you’ll get connection refused even though MySQL is clearly running. Check both:
sudo ss -tlnp | grep mysql
If you only see 127.0.0.1:3306 and your app insists on connecting to ::1, either fix the app’s connection string to use 127.0.0.1 explicitly, or add a bind-address = 0.0.0.0 line if remote connections are actually intended and firewalled appropriately. Do not casually open MySQL to 0.0.0.0 on a public-facing box without also restricting it via UFW to specific source IPs — that’s how databases end up in Shodan scans.
Log in and set the root authentication method explicitly. MySQL 8.4 defaults to caching_sha2_password, which is more secure than the old mysql_native_password but occasionally causes compatibility headaches with older PHP MySQL drivers or GUI tools like older versions of HeidiSQL:
sudo mysql
ALTER USER 'root'@'localhost' IDENTIFIED WITH caching_sha2_password BY 'your_strong_password_here';
FLUSH PRIVILEGES;
EXIT;
Create a dedicated application database and user instead of running your app as root against MySQL. This is a permissions best practice, not paranoia:
CREATE DATABASE app_production;
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'a_different_strong_password';
GRANT ALL PRIVILEGES ON app_production.* TO 'app_user'@'localhost';
FLUSH PRIVILEGES;
Step 3: Install PHP 8.5 and Required Modules
Ubuntu 26.04’s default repos ship PHP 8.5.2. If your application specifically requires an older PHP version for compatibility reasons — some legacy WordPress plugins still choke on 8.5’s stricter type coercion — you’ll need Ondřej Surý’s PPA, but for a fresh install assume you want current PHP unless you have a documented reason not to.
sudo apt install php libapache2-mod-php php-mysql -y
Install the common extension set most real applications need. Skipping this step is the single most common cause of “white screen of death” errors later:
sudo apt install php-cli php-curl php-gd php-mbstring php-xml php-zip php-bcmath php-intl -y
Verify the module loaded correctly:
apache2ctl -M | grep php
You should see php_module in the output. If Apache is still serving .php files as plain text downloads instead of executing them, it usually means libapache2-mod-php didn’t register correctly with Apache’s module config, or you’re running mpm_event instead of mpm_prefork.
And this is worth pausing on, because it trips people up constantly. Apache’s mpm_event (the default multi-processing module on fresh installs) is not compatible with mod_php running as a traditional Apache module — it’s designed for PHP-FPM’s process-based model instead. If you installed libapache2-mod-php, Apache should have automatically switched you to mpm_prefork. Confirm it:
apache2ctl -V | grep MPM
If it still says event, switch manually:
sudo a2dismod mpm_event
sudo a2enmod mpm_prefork
sudo a2enmod php8.5
sudo systemctl restart apache2
Personally, I don’t run mod_php on anything serving real traffic anymore. PHP-FPM with mpm_event gives you better concurrency handling and separates PHP process management from the web server itself, which matters a lot once you’re past a few hundred concurrent connections. If you’re building this for a hobby project, mod_php is fine and simpler. For anything client-facing, I’d set up PHP-FPM instead — happy to cover that as a separate build if there’s interest, but this guide sticks with mod_php since that’s the more common starting point.
Step 4: Configure Apache Virtual Hosts
Never run your site out of /var/www/html directly on anything meant to last. Set up a proper virtual host structure from day one:
sudo mkdir -p /var/www/yourdomain.com/public_html
sudo chown -R $USER:$USER /var/www/yourdomain.com/public_html
sudo chmod -R 755 /var/www/yourdomain.com
Create the virtual host config:
sudo nano /etc/apache2/sites-available/yourdomain.com.conf
<VirtualHost *:80>
ServerAdmin admin@yourdomain.com
ServerName yourdomain.com
ServerAlias www.yourdomain.com
DocumentRoot /var/www/yourdomain.com/public_html
ErrorLog ${APACHE_LOG_DIR}/yourdomain.com-error.log
CustomLog ${APACHE_LOG_DIR}/yourdomain.com-access.log combined
</VirtualHost>
Enable the site and disable the default one, then reload:
sudo a2ensite yourdomain.com.conf
sudo a2dissite 000-default.conf
sudo apache2ctl configtest
sudo systemctl reload apache2
Always run configtest before reloading. It catches syntax errors before they take your live site down, which is a lesson I learned by taking a live site down.
Verifying the Stack End to End
Drop a PHP info file into the docroot temporarily:
echo "<?php phpinfo(); ?>" | sudo tee /var/www/yourdomain.com/public_html/info.php
Visit yourdomain.com/info.php in a browser. You should see the full PHP configuration page with version 8.5.2 listed near the top. Confirm the MySQL extension is loaded by searching the page for “mysqli” or “pdo_mysql” — if it’s missing, you skipped the php-mysql package.
Delete this file immediately after testing. It leaks server configuration details that attackers actively scan for.
sudo rm /var/www/yourdomain.com/public_html/info.php
Troubleshooting Real Errors
Error: AH00558: apache2: Could not reliably determine the server's fully qualified domain name
This is cosmetic and won’t break functionality, but it’s annoying in logs. Fix it by setting ServerName globally:
echo "ServerName localhost" | sudo tee /etc/apache2/conf-available/servername.conf
sudo a2enconf servername
sudo systemctl reload apache2
Error: ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/run/mysqld/mysqld.sock'
This almost always means MySQL isn’t running, or the socket path in my.cnf doesn’t match what the client expects. Check systemctl status mysql first. If it’s crash-looping, check sudo journalctl -u mysql -n 50 — I’ve seen this caused by a full disk on /var/lib/mysql more than once on budget VPS instances with tiny default partitions, so check df -h before assuming a config issue.
Error: PHP Fatal error: Uncaught mysqli_sql_exception: Access denied for user 'app_user'@'localhost'
Almost always a password mismatch or the user was created with a host restriction that doesn’t match how the app connects. Double-check with:
SELECT user, host FROM mysql.user WHERE user = 'app_user';
If it shows app_user@% but your app connects from localhost, MySQL treats those as different grants entirely. This bites people constantly because it looks like it should just work.
Error: 403 Forbidden on a page that clearly exists
Check both file permissions and Apache’s Require all granted directive inside the virtual host’s <Directory> block. On 26.04, AppArmor is also active by default (Ubuntu doesn’t use SELinux), and while it rarely interferes with basic Apache/PHP operation, if you’ve moved your docroot to an unusual path like /opt/webapps, AppArmor’s Apache profile might not have permission to traverse it. Check sudo journalctl -k | grep -i apparmor if permissions look correct but access is still denied — this is the one that gets missed most often because admins assume AppArmor is a desktop-only concern.
Performance and Security Hardening
Don’t treat this as optional bolted-on advice at the end. Bake it in now.
For traffic spikes — and I’ve seen this take down a mid-traffic e-commerce box more than once — tune Apache’s MaxRequestWorkers in /etc/apache2/mods-available/mpm_prefork.conf based on available RAM, not the default. Each Apache process with mod_php loaded can eat 30-50MB depending on your app’s memory footprint. On a 4GB box, a default MaxRequestWorkers of 256 will happily let Apache eat all your RAM and trigger the OOM killer, which then kills MySQL because Linux doesn’t know MySQL is more important. Calculate it: available RAM minus MySQL’s innodb_buffer_pool_size minus OS overhead, divided by average process size.
Set MySQL’s InnoDB buffer pool to roughly 50-70% of available RAM on a dedicated database server, or a much smaller fraction if MySQL shares the box with Apache and PHP:
[mysqld]
innodb_buffer_pool_size = 1G
Enable OPcache for PHP, which caches compiled bytecode and cuts execution time meaningfully on repeat requests:
sudo apt install php-opcache -y
sudo systemctl restart apache2
For firewall rules beyond the basics already covered, restrict MySQL’s port entirely from external access unless you have a specific replication or remote-admin need:
sudo ufw deny 3306
Set up automatic security updates so you’re not manually patching every CVE announcement:
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure --priority=low unattended-upgrades
Finally, install Certbot and get real SSL running, not the “I’ll do it later” version of SSL:
sudo apt install certbot python3-certbot-apache -y
sudo certbot --apache -d yourdomain.com -d www.yourdomain.com
Frequently Asked Questions
Does Ubuntu 26.04 LTS support LAMP the same way as 24.04?
Mostly yes, but package versions differ meaningfully — MySQL moved from 8.0 to 8.4 LTS and PHP defaults to 8.5.2 instead of 8.3. The installation commands are identical; the version-specific behavior around authentication plugins and PHP type strictness is what changes.
Should I use MySQL or MariaDB on Ubuntu 26.04?
Both are available in the default repos — MySQL 8.4 or MariaDB 11.8.x. I lean MySQL for anything using tools built primarily around Oracle’s ecosystem, and MariaDB when I want a fully open-source stack with slightly more permissive licensing terms for redistribution. For a typical WordPress or Laravel deployment, either works fine.
Why does my PHP file download instead of executing in the browser?
This almost always means libapache2-mod-php isn’t loaded or Apache is running mpm_event instead of mpm_prefork. Run apache2ctl -M | grep php to confirm the module is active, and check the MPM in use with apache2ctl -V.
Is LAMP still relevant in 2026, or should I use containers instead?
For a lot of small-to-mid production workloads, a properly tuned bare-metal or VPS LAMP stack is still simpler to operate and debug than a container orchestration layer you don’t need yet. Docker makes more sense once you’re managing multiple environments or need reproducible deploys across a team — for a single-server WordPress or PHP app, it’s often unnecessary overhead.
How do I secure MySQL after installation on Ubuntu 26.04?
Run mysql_secure_installation immediately, create dedicated per-application database users instead of using root, and restrict remote access to port 3306 via UFW unless external connections are explicitly required.
What’s the minimum server spec for LAMP on Ubuntu 26.04?
1GB RAM will technically run it but leaves almost no headroom for traffic or PHP’s memory needs under load. 2-4GB RAM with at least 2 vCPUs is a more realistic floor for anything beyond a personal test site.
If you’re deploying this on a box that’ll actually see production traffic, run sudo apt install fail2ban -y before you walk away from the terminal. It’s a five-minute step that will quietly block the brute-force SSH and login attempts that start hitting a new public IP within hours of it going live — I’ve watched it happen on test servers with no DNS pointed at them yet.