Install a LEMP Stack on an AlmaLinux 9 Linux VPS

2 min
VPS/VDS Hosting

A LEMP stack (Linux, Nginx, MariaDB, PHP-FPM) is a fast and light alternative to Apache for serving PHP sites. On AlmaLinux 9 everything comes from the official AppStream repository, but SELinux and firewalld need a little attention. This guide walks you through a clean setup on your VPS Linux Infrawire; the same steps work on Rocky Linux 9.

📋 Prerequisites

  • A KVM VPS running AlmaLinux 9 (IP and root password are available in your client area)
  • SSH access as root
  • A domain example.com pointing to VPS_IP

1. Update the system and install the tools

Bash
dnf -y update dnf -y install nano policycoreutils-python-utils

policycoreutils-python-utils provides semanage, used in the SELinux section.

2. Install Nginx and open the firewall

Bash
dnf -y install nginx systemctl enable --now nginx firewall-cmd --permanent --add-service=http --add-service=https firewall-cmd --reload firewall-cmd --list-services

3. Install MariaDB

Bash
dnf -y install mariadb-server systemctl enable --now mariadb mysql_secure_installation

Answer the wizard to set a root password, remove anonymous users, forbid remote root login and drop the test database. Then create a database and a dedicated user:

Bash
mysql -u root -p
SQL
1CREATE DATABASE appdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 2CREATE USER 'appuser'@'localhost' IDENTIFIED BY 'STRONG_PASSWORD'; 3GRANT ALL PRIVILEGES ON appdb.* TO 'appuser'@'localhost'; 4FLUSH PRIVILEGES; 5EXIT;

4. Install PHP 8.3 from the AppStream module

AppStream offers several PHP streams. Select the one you want before installing:

Bash
dnf module list php dnf -y module reset php dnf -y module enable php:8.3 dnf -y install php-fpm php-cli php-mysqlnd php-gd php-mbstring php-xml php-intl php-opcache php-pecl-zip php -v

By default the PHP-FPM pool runs as apache. Since Nginx runs as nginx, align the pool and the session directories:

Bash
sed -i 's/^user = apache/user = nginx/; s/^group = apache/group = nginx/' /etc/php-fpm.d/www.conf chown -R root:nginx /var/lib/php/session /var/lib/php/wsdlcache /var/lib/php/opcache grep -E '^(user|group|listen)' /etc/php-fpm.d/www.conf systemctl enable --now php-fpm

The pool listens on the Unix socket /run/php-fpm/www.sock.

5. Create the server block

Bash
mkdir -p /var/www/example.com/public chown -R nginx:nginx /var/www/example.com nano /etc/nginx/conf.d/example.com.conf
Nginx
1server { 2 listen 80; 3 listen [::]:80; 4 server_name example.com www.example.com; 5 root /var/www/example.com/public; 6 index index.php index.html; 7 8 location / { 9 try_files $uri $uri/ /index.php?$query_string; 10 } 11 12 location ~ \.php$ { 13 include fastcgi_params; 14 fastcgi_pass unix:/run/php-fpm/www.sock; 15 fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 16 } 17 18 location ~ /\.(?!well-known) { 19 deny all; 20 } 21}
Bash
nginx -t && systemctl reload nginx

6. Configure SELinux

Keep SELinux in enforcing mode (getenforce). Files created under /var/www already get the httpd_sys_content_t label, but files moved with mv from elsewhere keep their old label. Relabel them, and give write access only to the directories that need it:

Bash
restorecon -Rv /var/www/example.com mkdir -p /var/www/example.com/public/uploads semanage fcontext -a -t httpd_sys_rw_content_t '/var/www/example.com/public/uploads(/.*)?' restorecon -Rv /var/www/example.com/public/uploads

Then enable booleans only if your application needs them:

Bash
# PHP connects to a database over TCP (remote server or 127.0.0.1) setsebool -P httpd_can_network_connect_db 1 # PHP or Nginx makes outgoing connections (APIs, proxy_pass to an app) setsebool -P httpd_can_network_connect 1

A local MariaDB reached through its socket needs neither of them. To see what SELinux blocks, run ausearch -m avc -ts recent. Never "fix" a problem with setenforce 0.

7. Test PHP, then delete the test file

Bash
echo '<?php phpinfo();' > /var/www/example.com/public/info.php curl -sI http://example.com/info.php | head -n 1 rm /var/www/example.com/public/info.php

Expect HTTP/1.1 200 OK. The phpinfo() page reveals your configuration, so remove it right away. Finally, check the services:

Bash
systemctl is-active nginx php-fpm mariadb

🛠️ Troubleshooting

502 Bad Gateway. PHP-FPM is stopped, or the socket path in fastcgi_pass is wrong. Run systemctl status php-fpm, check ls -l /run/php-fpm/www.sock, and read /var/log/nginx/error.log. If the log says Permission denied on the socket, look for an AVC denial with ausearch.

403 Forbidden or Permission denied. Check ownership (nginx:nginx) and the SELinux label with ls -Z /var/www/example.com/public. It should read httpd_sys_content_t; if not, run restorecon -Rv.

"File not found." or a blank page. The root path or the SCRIPT_FILENAME line is wrong. Check both, then nginx -t.

Cannot connect to an external database or API. SELinux blocks outgoing connections by default: enable httpd_can_network_connect_db or httpd_can_network_connect as shown above.


🎉 Nginx, MariaDB and PHP-FPM are running on AlmaLinux 9, with SELinux still enforcing.

Stay up to date with the latest offers, discounts and special promotions.

View promotions

We use cookies

We use analytics cookies to improve your experience on our site. No data is sold to third parties. Learn more in our privacy policy.