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.compointing toVPS_IP
1. Update the system and install the tools
Bashdnf -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
Bashdnf -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
Bashdnf -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:
Bashmysql -u root -p
SQL1CREATE 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:
Bashdnf 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:
Bashsed -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
Bashmkdir -p /var/www/example.com/public chown -R nginx:nginx /var/www/example.com nano /etc/nginx/conf.d/example.com.conf
Nginx1server { 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}
Bashnginx -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:
Bashrestorecon -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
Bashecho '<?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:
Bashsystemctl 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.