{"id":1013,"date":"2026-11-05T18:00:00","date_gmt":"2026-11-05T18:00:00","guid":{"rendered":"https:\/\/vpslake.com\/blog\/?p=1013"},"modified":"2026-11-05T18:00:00","modified_gmt":"2026-11-05T18:00:00","slug":"install-apache-mariadb-php-centos-stream-9","status":"publish","type":"post","link":"https:\/\/vpslake.com\/blog\/2026\/11\/05\/install-apache-mariadb-php-centos-stream-9\/","title":{"rendered":"How to Install Apache, MariaDB and PHP on CentOS Stream 9"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">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\u201315 minutes.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Prerequisites<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>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.<\/li>\n\n\n<li>SSH access as <code>root<\/code>, or a normal account with working <code>sudo<\/code> privileges.<\/li>\n\n\n<li>A public IPv4 address if the site must be reachable from the internet; use <code>SERVER_IP<\/code> in the examples below.<\/li>\n\n\n<li>A domain such as <code>YOUR_DOMAIN<\/code> is optional for this base installation, but required before you configure a trusted HTTPS certificate.<\/li>\n\n\n<li>If you need the server first, choose a <a href=\"https:\/\/vpslake.com\/buy-vps\">VPSLake Linux VPS<\/a> with CentOS Stream 9 access.<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">This guide uses CentOS Stream 9\u2019s 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.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 1: Update CentOS Stream and install the stack<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo dnf update -y\nsudo dnf install -y httpd firewalld mariadb-server mariadb php php-cli php-fpm php-mysqlnd php-gd php-mbstring php-opcache curl<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl enable --now firewalld httpd mariadb php-fpm\nsudo systemctl is-active firewalld httpd mariadb php-fpm<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Expected output is one <code>active<\/code> line for each service:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>active\nactive\nactive\nactive<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The official <a href=\"https:\/\/httpd.apache.org\/docs\/2.4\/\" target=\"_blank\" rel=\"noopener\">Apache HTTP Server 2.4 documentation<\/a> covers the same <code>httpd<\/code> configuration and service concepts used by CentOS Stream 9.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2: Allow web traffic through firewalld<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo firewall-cmd --permanent --zone=public --add-service=ssh\nsudo firewall-cmd --permanent --zone=public --add-service=http\nsudo firewall-cmd --permanent --zone=public --add-service=https\nsudo firewall-cmd --reload\nsudo firewall-cmd --zone=public --list-services<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The final command should include <code>ssh<\/code>, <code>http<\/code>, and <code>https<\/code>, although it may also list services already enabled by your VPS image:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cockpit dhcpv6-client http https ssh<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 3: Confirm Apache before adding PHP code<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Check Apache\u2019s configuration and local HTTP response before debugging PHP. This separates a web-server or firewall issue from an application-runtime issue.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apachectl configtest\ncurl -I http:\/\/127.0.0.1\/<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You should see <code>Syntax OK<\/code> followed by a successful HTTP response:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Syntax OK\nHTTP\/1.1 200 OK<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The exact <code>Server<\/code> header and date vary. From another computer, open <code>http:\/\/SERVER_IP\/<\/code>; if the default page loads locally but not remotely, recheck the <code>firewalld<\/code> rules and any provider-level network firewall.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 4: Secure and verify MariaDB<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo mariadb-secure-installation<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">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 <code>DB_ROOT_PASSWORD<\/code>, never a reused VPS password.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Test the local client and record the installed server version:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo mariadb -e \"SELECT VERSION();\"\nsudo mariadb -e \"SHOW DATABASES;\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The first command prints one version row, and the second includes system databases such as <code>information_schema<\/code>, <code>mysql<\/code>, and <code>performance_schema<\/code>. The <a href=\"https:\/\/mariadb.com\/docs\/server\/clients-and-utilities\/deployment-tools\/mariadb-secure-installation\" target=\"_blank\" rel=\"noopener\">MariaDB secure-installation documentation<\/a> explains why socket authentication and the cleanup questions can differ from older MySQL tutorials.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 5: Install and test PHP through PHP-FPM<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The PHP packages provide the interpreter and common extensions, while PHP-FPM gives Apache a maintained FastCGI process to execute <code>.php<\/code> files. Restart Apache after the packages are installed so it loads the PHP-FPM configuration.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>php -v\nphp -m | grep -E 'mysqli|mysqlnd|PDO|pdo_mysql'\nsudo systemctl restart php-fpm httpd<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The version is determined by the enabled CentOS Stream 9 repository. The module check should include at least <code>mysqli<\/code>, <code>mysqlnd<\/code>, <code>PDO<\/code>, and <code>pdo_mysql<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Create a short-lived test file that prints a harmless message rather than exposing the full <code>phpinfo()<\/code> configuration page.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>echo '&lt;?php echo \"PHP is working\\n\"; ?&gt;' | sudo tee \/var\/www\/html\/info.php &gt;\/dev\/null\ncurl -fsS http:\/\/127.0.0.1\/info.php\nsudo rm -f \/var\/www\/html\/info.php<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Expected output:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>PHP is working<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Removing <code>info.php<\/code> immediately matters because diagnostic pages can disclose paths, loaded modules, and environment variables. Leave <code>\/var\/www\/html<\/code> with only your real site files.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Verify the LAMP stack<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl is-active httpd php-fpm mariadb\ncurl -fsS http:\/\/127.0.0.1\/\nsudo mariadb -e \"SELECT VERSION() AS mariadb_version;\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The service check should return three <code>active<\/code> lines. The curl command should return the Apache welcome page or your own index content, and the SQL query should return a <code>mariadb_version<\/code> column with a version value. Before deploying a login-capable site, point <code>YOUR_DOMAIN<\/code> to the VPS, configure HTTPS, and keep port 3306 closed unless a separate host genuinely needs database access.<\/p>\n\n\n\n<aside class=\"vl-element vl-cta-banner\" aria-label=\"Launch your LAMP server\">\n\t<div class=\"vl-cta-banner__content\">\n\t\t<p class=\"vl-cta-banner__eyebrow\">CentOS Stream VPS<\/p>\n\t\t<h3 class=\"vl-cta-banner__title\">Launch your LAMP server<\/h3>\n\t\t<p class=\"vl-cta-banner__text\">Run Apache, MariaDB, and PHP on a Linux VPS with root access.<\/p>\n\t<\/div>\n\t<a class=\"vl-cta-banner__button\" href=\"\/buy-vps\" aria-label=\"View Linux VPS plans\">View Linux VPS plans<\/a>\n<\/aside>\n\n\n\n<h2 class=\"wp-block-heading\">Troubleshooting<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><code>Job for httpd.service failed<\/code> or <code>Syntax error<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apachectl configtest\nsudo journalctl -u httpd -n 40 --no-pager\nsudo systemctl restart httpd<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><code>curl<\/code> works locally but the site times out remotely<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The service is listening, but traffic is being blocked by <code>firewalld<\/code> or by the VPS provider\u2019s external firewall. Confirm the public-zone rules and reapply them without opening MariaDB.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo firewall-cmd --zone=public --list-services\nsudo firewall-cmd --permanent --zone=public --add-service=http\nsudo firewall-cmd --permanent --zone=public --add-service=https\nsudo firewall-cmd --reload\nsudo ss -ltnp | grep -E ':(80|443)\\b'<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If the local check succeeds and port 80 is listening, allow TCP 80 and 443 in the VPS control panel\u2019s network firewall as well.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The browser downloads PHP or shows PHP source code<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl status php-fpm --no-pager\nsudo test -f \/etc\/httpd\/conf.d\/php.conf &amp;&amp; echo 'PHP-FPM Apache config found'\nsudo journalctl -u php-fpm -n 40 --no-pager\nsudo systemctl restart php-fpm httpd<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>ERROR 1698 (28000): Access denied for user 'root'@'localhost'<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">That usually means MariaDB is using Unix-socket authentication, which is a normal secure default. Run administrative commands through <code>sudo mariadb<\/code>; use <code>-p<\/code> only if you deliberately configured a password-authenticated root account.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo mariadb -e \"SELECT USER(), CURRENT_USER();\"<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Apache returns <code>403 Forbidden<\/code> for a new document<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo restorecon -Rv \/var\/www\/html\nsudo find \/var\/www\/html -type d -exec chmod 0755 {} \\;\nsudo find \/var\/www\/html -type f -exec chmod 0644 {} \\;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The web root should be readable by Apache and should not be world-writable. If the denial persists, inspect <code>sudo ausearch -m AVC -ts recent<\/code> and correct the named path.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Hardening<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>Install a trusted certificate for <code>YOUR_DOMAIN<\/code> and redirect HTTP to HTTPS before deploying an application that accepts passwords, payments, or personal data.<\/li>\n\n\n<li>Keep MariaDB bound to localhost unless a separate application host requires access; create a limited database user instead of using root in application configuration.<\/li>\n\n\n<li>Replace the default Apache page with a site-specific virtual host and disable directory listing with <code>Options -Indexes<\/code> where appropriate.<\/li>\n\n\n<li>Review enabled ports with <code>sudo firewall-cmd --zone=public --list-all<\/code> after each service installation.<\/li>\n\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">FAQ<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Is MariaDB compatible with MySQL applications?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">MariaDB implements the MySQL client protocol and supports the common SQL features used by many PHP applications. Check the application\u2019s compatibility matrix before relying on a MySQL-specific feature or version requirement.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Do I need to open port 3306 for PHP?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Which PHP version does CentOS Stream 9 install?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The enabled AppStream repository decides the version available to your image, so inspect it with <code>php -v<\/code> after installation. This guide intentionally avoids pinning a version that may change during the Stream 9 lifecycle.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Can I install a website before configuring HTTPS?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<script type=\"application\/ld+json\">{\"@context\": \"https:\/\/schema.org\", \"@type\": \"FAQPage\", \"mainEntity\": [{\"@type\": \"Question\", \"name\": \"Is MariaDB compatible with MySQL applications?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"MariaDB implements the MySQL client protocol and supports the common SQL features used by many PHP applications. Check the application\u2019s compatibility matrix before relying on a MySQL-specific feature or version requirement.\"}}, {\"@type\": \"Question\", \"name\": \"Do I need to open port 3306 for PHP?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"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.\"}}, {\"@type\": \"Question\", \"name\": \"Which PHP version does CentOS Stream 9 install?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"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.\"}}, {\"@type\": \"Question\", \"name\": \"Can I install a website before configuring HTTPS?\", \"acceptedAnswer\": {\"@type\": \"Answer\", \"text\": \"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.\"}}]}<\/script>\n\n","protected":false},"excerpt":{"rendered":"Install apache mariadb and php on centos stream 9 with secure PHP-FPM, firewalld, database security, service checks, and a working PHP test page.","protected":false},"author":1,"featured_media":1014,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"csco_singular_sidebar":"","csco_page_header_type":"","csco_page_load_nextpost":"","footnotes":""},"categories":[5,9],"tags":[71,111,117,73,72],"class_list":["post-1013","post","type-post","status-publish","format-standard","has-post-thumbnail","category-guides","category-vps-hosting","tag-apache","tag-centos-stream-9","tag-lamp-stack","tag-mariadb","tag-php","cs-entry"],"_links":{"self":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1013","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/comments?post=1013"}],"version-history":[{"count":1,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1013\/revisions"}],"predecessor-version":[{"id":3903,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/posts\/1013\/revisions\/3903"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/media\/1014"}],"wp:attachment":[{"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/media?parent=1013"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/categories?post=1013"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/vpslake.com\/blog\/wp-json\/wp\/v2\/tags?post=1013"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}