To install Apache MariaDB and PHP on CentOS Stream 9, use the distribution packages, connect Apache to PHP-FPM, and keep MariaDB off the public network. A fresh VPS should be serving and executing a test PHP page in about 10–15 minutes.
Prerequisites
- A fresh CentOS Stream 9 server with at least 1 GB RAM and 10 GB of free disk space for the web stack and application files.
- SSH access as
root, or a normal account with workingsudoprivileges. - A public IPv4 address if the site must be reachable from the internet; use
SERVER_IPin the examples below. - A domain such as
YOUR_DOMAINis optional for this base installation, but required before you configure a trusted HTTPS certificate. - If you need the server first, choose a VPSLake Linux VPS with CentOS Stream 9 access.
This guide uses CentOS Stream 9’s AppStream packages instead of adding a third-party repository. The enabled stream determines the exact PHP and MariaDB point releases, while the commands and service names remain stable.
Step 1: Update CentOS Stream and install the stack
Updating metadata first prevents an old package cache from producing incomplete dependency or repository errors. This installs Apache, MariaDB, PHP-FPM, the MySQL-compatible PHP driver, and the firewall service in one pass.
sudo dnf update -y
sudo dnf install -y httpd firewalld mariadb-server mariadb php php-cli php-fpm php-mysqlnd php-gd php-mbstring php-opcache curl
Start each daemon and enable it for the next boot. PHP-FPM is the supported Apache integration on the EL9 family; it processes PHP separately from the web server.
sudo systemctl enable --now firewalld httpd mariadb php-fpm
sudo systemctl is-active firewalld httpd mariadb php-fpm
Expected output is one active line for each service:
active
active
active
active
The official Apache HTTP Server 2.4 documentation covers the same httpd configuration and service concepts used by CentOS Stream 9.
Step 2: Allow web traffic through firewalld
Apache cannot be reached from outside the VPS until the host firewall permits HTTP and HTTPS. Keep SSH allowed so a future firewall reload does not remove your remote administration path.
sudo firewall-cmd --permanent --zone=public --add-service=ssh
sudo firewall-cmd --permanent --zone=public --add-service=http
sudo firewall-cmd --permanent --zone=public --add-service=https
sudo firewall-cmd --reload
sudo firewall-cmd --zone=public --list-services
The final command should include ssh, http, and https, although it may also list services already enabled by your VPS image:
cockpit dhcpv6-client http https ssh
Port 3306 is intentionally absent. PHP and MariaDB are on the same VPS, so the application can use the local socket; exposing the database to the internet creates an unnecessary attack surface.
Step 3: Confirm Apache before adding PHP code
Check Apache’s configuration and local HTTP response before debugging PHP. This separates a web-server or firewall issue from an application-runtime issue.
sudo apachectl configtest
curl -I http://127.0.0.1/
You should see Syntax OK followed by a successful HTTP response:
Syntax OK
HTTP/1.1 200 OK
The exact Server header and date vary. From another computer, open http://SERVER_IP/; if the default page loads locally but not remotely, recheck the firewalld rules and any provider-level network firewall.
Step 4: Secure and verify MariaDB
The hardening assistant removes default accounts and test data, and prevents remote root administration. Run it interactively because the available prompts differ slightly between MariaDB package releases.
sudo mariadb-secure-installation
When prompted, keep the secure choices: remove anonymous users, disallow remote root login, remove the test database, and reload the privilege tables. On current MariaDB packages, root may authenticate through the Unix socket instead of a password; do not force a password login if the script offers socket authentication and you do not need remote database administration. If you create a root password, use a long unique value represented by DB_ROOT_PASSWORD, never a reused VPS password.
Test the local client and record the installed server version:
sudo mariadb -e "SELECT VERSION();"
sudo mariadb -e "SHOW DATABASES;"
The first command prints one version row, and the second includes system databases such as information_schema, mysql, and performance_schema. The MariaDB secure-installation documentation explains why socket authentication and the cleanup questions can differ from older MySQL tutorials.
Step 5: Install and test PHP through PHP-FPM
The PHP packages provide the interpreter and common extensions, while PHP-FPM gives Apache a maintained FastCGI process to execute .php files. Restart Apache after the packages are installed so it loads the PHP-FPM configuration.
php -v
php -m | grep -E 'mysqli|mysqlnd|PDO|pdo_mysql'
sudo systemctl restart php-fpm httpd
The version is determined by the enabled CentOS Stream 9 repository. The module check should include at least mysqli, mysqlnd, PDO, and pdo_mysql.
Create a short-lived test file that prints a harmless message rather than exposing the full phpinfo() configuration page.
echo '<?php echo "PHP is working\n"; ?>' | sudo tee /var/www/html/info.php >/dev/null
curl -fsS http://127.0.0.1/info.php
sudo rm -f /var/www/html/info.php
Expected output:
PHP is working
Removing info.php immediately matters because diagnostic pages can disclose paths, loaded modules, and environment variables. Leave /var/www/html with only your real site files.
Verify the LAMP stack
Run the final checks together to prove that all three layers are ready: Apache answers HTTP, PHP-FPM is active and executes code, and MariaDB accepts a local administrative query.
sudo systemctl is-active httpd php-fpm mariadb
curl -fsS http://127.0.0.1/
sudo mariadb -e "SELECT VERSION() AS mariadb_version;"
The service check should return three active lines. The curl command should return the Apache welcome page or your own index content, and the SQL query should return a mariadb_version column with a version value. Before deploying a login-capable site, point YOUR_DOMAIN to the VPS, configure HTTPS, and keep port 3306 closed unless a separate host genuinely needs database access.
Troubleshooting
Job for httpd.service failed or Syntax error
Apache found a malformed configuration file, often a leftover virtual-host file or a typo in a PHP-FPM handler. Read the exact filename from the configuration test, fix that file, and only then restart the service.
sudo apachectl configtest
sudo journalctl -u httpd -n 40 --no-pager
sudo systemctl restart httpd
curl works locally but the site times out remotely
The service is listening, but traffic is being blocked by firewalld or by the VPS provider’s external firewall. Confirm the public-zone rules and reapply them without opening MariaDB.
sudo firewall-cmd --zone=public --list-services
sudo firewall-cmd --permanent --zone=public --add-service=http
sudo firewall-cmd --permanent --zone=public --add-service=https
sudo firewall-cmd --reload
sudo ss -ltnp | grep -E ':(80|443)\b'
If the local check succeeds and port 80 is listening, allow TCP 80 and 443 in the VPS control panel’s network firewall as well.
The browser downloads PHP or shows PHP source code
Apache is serving the file as static text because PHP-FPM is stopped or its Apache handler was not loaded. Check the daemon, PHP configuration, and recent Apache messages, then restart both services.
sudo systemctl status php-fpm --no-pager
sudo test -f /etc/httpd/conf.d/php.conf && echo 'PHP-FPM Apache config found'
sudo journalctl -u php-fpm -n 40 --no-pager
sudo systemctl restart php-fpm httpd
Do not work around this by enabling an old third-party mod_php package; keep the EL9 PHP-FPM path and fix the service or configuration error.
ERROR 1698 (28000): Access denied for user 'root'@'localhost'
That usually means MariaDB is using Unix-socket authentication, which is a normal secure default. Run administrative commands through sudo mariadb; use -p only if you deliberately configured a password-authenticated root account.
sudo mariadb -e "SELECT USER(), CURRENT_USER();"
Apache returns 403 Forbidden for a new document
Files copied from another directory can have an incorrect SELinux context even when their Unix permissions look right. Restore the default web-root labels instead of disabling SELinux.
sudo restorecon -Rv /var/www/html
sudo find /var/www/html -type d -exec chmod 0755 {} \;
sudo find /var/www/html -type f -exec chmod 0644 {} \;
The web root should be readable by Apache and should not be world-writable. If the denial persists, inspect sudo ausearch -m AVC -ts recent and correct the named path.
Hardening
- Install a trusted certificate for
YOUR_DOMAINand redirect HTTP to HTTPS before deploying an application that accepts passwords, payments, or personal data. - Keep MariaDB bound to localhost unless a separate application host requires access; create a limited database user instead of using root in application configuration.
- Replace the default Apache page with a site-specific virtual host and disable directory listing with
Options -Indexeswhere appropriate. - Review enabled ports with
sudo firewall-cmd --zone=public --list-allafter each service installation.
FAQ
Is MariaDB compatible with MySQL applications?
MariaDB implements the MySQL client protocol and supports the common SQL features used by many PHP applications. Check the application’s compatibility matrix before relying on a MySQL-specific feature or version requirement.
Do I need to open port 3306 for PHP?
No. When PHP and MariaDB share this VPS, the application can connect through the local socket or loopback interface. Open 3306 only for a documented remote client, and restrict the source address rather than allowing the whole internet.
Which PHP version does CentOS Stream 9 install?
The enabled AppStream repository decides the version available to your image, so inspect it with php -v after installation. This guide intentionally avoids pinning a version that may change during the Stream 9 lifecycle.
Can I install a website before configuring HTTPS?
You can use HTTP for the local smoke test and initial virtual-host setup. Configure HTTPS before exposing an account, admin panel, checkout page, or any other site that handles credentials or sensitive data.