How to Set Up a LEMP Stack
Install and configure Nginx, MariaDB, and PHP-FPM on a fresh Linux server. Covers Debian, Ubuntu, Fedora, RHEL, Rocky, and Arch with systemd service management.
Before you start
- ▸A fresh server with a non-root sudo user
- ▸SSH access and a terminal
- ▸A domain name pointed at the server IP (optional but recommended for TLS later)
- ▸Basic familiarity with a terminal text editor such as nano or vim
A LEMP stack — Linux, Nginx, MariaDB (or MySQL), and PHP — is a proven foundation for hosting web applications, WordPress sites, and custom PHP projects. This guide walks through installing and wiring together each component on a fresh server, then verifies the stack works end-to-end before you deploy anything real.
Install Nginx
Debian / Ubuntu
sudo apt update
sudo apt install -y nginx
Fedora / RHEL / Rocky
sudo dnf install -y nginx
Arch
sudo pacman -S --noconfirm nginx
Enable and start Nginx with systemd:
sudo systemctl enable --now nginx
If a firewall is active, open HTTP and HTTPS traffic:
# ufw (Debian/Ubuntu)
sudo ufw allow 'Nginx Full'
# firewalld (Fedora/RHEL/Rocky)
sudo firewall-cmd --permanent --add-service=http --add-service=https
sudo firewall-cmd --reload
Confirm Nginx is serving by visiting http://YOUR_SERVER_IP in a browser. You should see the default welcome page.
Install MariaDB
MariaDB is the recommended drop-in replacement for MySQL on most distros. If your application specifically requires Oracle MySQL, substitute mysql-server in the commands below — the configuration steps are identical.
Debian / Ubuntu
sudo apt install -y mariadb-server
Fedora / RHEL / Rocky
sudo dnf install -y mariadb-server
Arch
sudo pacman -S --noconfirm mariadb
sudo mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql
Enable and start the service, then run the security script:
sudo systemctl enable --now mariadb
sudo mariadb-secure-installation
The interactive script asks you to set a root password, remove anonymous users, disallow remote root login, and drop the test database. Accept all recommendations unless you have a specific reason not to.
Create an application database and user
Never let your application connect as the MariaDB root user. Create a dedicated account:
sudo mariadb -u root -p
CREATE DATABASE myapp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'myapp_user'@'localhost' IDENTIFIED BY 'StrongPassw0rd!';
GRANT ALL PRIVILEGES ON myapp.* TO 'myapp_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;
Install PHP-FPM
Nginx does not execute PHP natively; it passes requests to PHP-FPM (FastCGI Process Manager) over a Unix socket. Install PHP-FPM and the extensions most applications need:
Debian / Ubuntu
sudo apt install -y php-fpm php-mysql php-mbstring php-xml php-curl php-zip
On Ubuntu/Debian the service name includes the version, e.g. php8.3-fpm. Check with:
systemctl list-units 'php*fpm*'
Fedora / RHEL / Rocky
sudo dnf install -y php-fpm php-mysqlnd php-mbstring php-xml php-curl php-zip
Arch
sudo pacman -S --noconfirm php-fpm php-gd
Enable and start PHP-FPM:
sudo systemctl enable --now php-fpm
On Debian/Ubuntu use the versioned unit name, for example:
sudo systemctl enable --now php8.3-fpm
Confirm the socket path
You need the Unix socket path to configure Nginx. The default locations are:
- Debian/Ubuntu:
/run/php/php8.x-fpm.sock - Fedora/RHEL/Rocky:
/run/php-fpm/www.sock - Arch:
/run/php-fpm/php-fpm.sock
Verify the socket exists and is active:
ls -l /run/php-fpm/www.sock # adjust path for your distro
Configure Nginx to Use PHP-FPM
Create a server block for your site. Replace example.com and the socket path as needed.
sudo nano /etc/nginx/sites-available/example.com
On Fedora/RHEL/Rocky, the equivalent directory is /etc/nginx/conf.d/; create example.com.conf there instead and skip the symlink step.
server {
listen 80;
server_name example.com www.example.com;
root /var/www/example.com/public;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf; # Debian/Ubuntu
# On RHEL/Arch replace the line above with:
# fastcgi_split_path_info ^(.+\.php)(/.+)$;
# include fastcgi_params;
# fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/php8.3-fpm.sock; # adjust socket path
}
location ~ /\.ht {
deny all;
}
}
On Debian/Ubuntu, enable the site and remove the default:
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
Test the configuration syntax before reloading:
sudo nginx -t
Expected output (will vary slightly): nginx: configuration file /etc/nginx/nginx.conf test is successful
sudo systemctl reload nginx
Create the Web Root and a Test Page
sudo mkdir -p /var/www/example.com/public
sudo chown -R www-data:www-data /var/www/example.com # use 'nginx' on RHEL/Arch
echo '
Visit http://YOUR_SERVER_IP/info.php (or your domain). You should see the PHP information page, confirming Nginx is passing PHP requests to PHP-FPM successfully. The page should show the Server API as FPM/FastCGI.
Remove the info page once you have confirmed it works — it exposes server internals:
sudo rm /var/www/example.com/public/info.php
Verify the Full Stack
Check that all three services are running and enabled at boot:
systemctl is-active nginx mariadb php-fpm # or php8.3-fpm on Debian/Ubuntu
Output should show active for each. Verify MariaDB accepts connections with the application user:
mariadb -u myapp_user -p -e "SELECT VERSION();" myapp
Troubleshooting
502 Bad Gateway
This almost always means Nginx cannot reach PHP-FPM. Check that the socket path in your Nginx config exactly matches the path shown by ls /run/php*/. Also confirm PHP-FPM is running: sudo systemctl status php-fpm.
Permission denied on the socket
The Nginx worker process user (www-data on Debian/Ubuntu, nginx on RHEL) must be able to read the PHP-FPM socket. Open /etc/php-fpm.d/www.conf (or the distro equivalent) and set:
listen.owner = nginx
listen.group = nginx
listen.mode = 0660
Then restart PHP-FPM: sudo systemctl restart php-fpm.
SELinux blocking connections (RHEL/Rocky/Fedora)
If you see AVC denials in sudo ausearch -m avc -ts recent, allow Nginx to connect to PHP-FPM:
sudo setsebool -P httpd_can_network_connect 1
Nginx config changes have no effect
Always run sudo nginx -t before reloading. A syntax error will prevent the reload from applying and Nginx continues with the old config — but the command exit code will be non-zero and the error will be printed to the terminal.
Frequently asked questions
- Can I use MySQL instead of MariaDB?
- Yes. Substitute mysql-server (Debian/Ubuntu) or mysql-community-server (RHEL/Fedora via the MySQL repo) in the install step. The configuration, SQL commands, and systemd unit name (mysqld on RHEL, mysql on Debian) differ slightly, but the PHP-FPM and Nginx setup is identical.
- How do I add HTTPS / TLS to Nginx?
- Install Certbot and the nginx plugin (certbot-nginx), then run sudo certbot --nginx -d example.com. Certbot rewrites your server block and sets up auto-renewal via a systemd timer.
- Should I run multiple PHP versions on one server?
- You can install multiple php-fpm packages simultaneously on Debian/Ubuntu (each gets its own versioned socket and systemd unit). Point different Nginx server blocks at different sockets. On RHEL you need the Remi repository to access non-default PHP versions.
- What user should own files in the web root?
- Files should be owned by your deploy user or root, not the Nginx/PHP worker user. Set group ownership to www-data (or nginx) and use 644/755 permissions so the worker can read but not write. Only upload directories need group write permission, and ideally those live outside the web root.
- Why use a Unix socket instead of a TCP port for PHP-FPM?
- A Unix socket avoids the TCP stack overhead for local communication, giving slightly lower latency and eliminating the need to bind a port. For single-server setups it is always preferred. Use TCP (127.0.0.1:9000) only when Nginx and PHP-FPM run on separate hosts.
Related guides
Configure Prometheus Alertmanager
Configure Prometheus Alertmanager with routing trees, receivers, inhibition rules, grouping, Go templates, and PagerDuty/Slack on-call integrations.
Build an Intranet Server on Linux
Set up a complete small-office intranet on one Linux box: Nginx web server, dnsmasq local DNS, Samba file sharing, and a Wiki.js team wiki.
Build an nftables Firewall Script
Build a complete nftables firewall from scratch: tables, chains, sets, default-deny input policy, service allowlisting, and persistent systemd configuration.
Caddy as a Reverse Proxy
Set up Caddy as a reverse proxy with automatic HTTPS, load balancing, WebSocket passthrough, reusable snippets, and header control — no certbot required.