How to Install LEMP on Debian 13, 12 and 11

Install LEMP on Debian 13, 12 and 11 with Nginx, MariaDB, and PHP-FPM. Includes SSL setup, firewall rules, and troubleshooting tips.

Last updatedAuthorJoshua JamesRead time10 minGuide typeDebian

A Debian LEMP stack is useful when you want Nginx serving the site, PHP-FPM handling dynamic PHP requests, and MariaDB storing application data without adding third-party package sources. You can install LEMP on Debian from the default APT repositories, then connect the pieces with a version-aware PHP-FPM socket so the same workflow works on Debian 13, 12, and 11.

This setup fits WordPress, phpMyAdmin, phpBB, Laravel, Drupal, and smaller custom PHP applications that need a predictable web-server stack. If you prefer Apache instead of Nginx, use the LAMP stack on Debian workflow because the PHP handler and virtual-host layout are different.

Install LEMP Stack on Debian

The default Debian packages are the right starting point for most servers because Debian maintains security fixes inside each release branch. Use vendor repositories only when a specific feature or application requires a newer branch than Debian provides.

Check Debian LEMP Package Branches

Debian release packages stay on stable branches rather than tracking every upstream point release. The branch names below describe the default package families validated from current Debian APT metadata.

Debian ReleaseNginx BranchMariaDB BranchPHP BranchPHP-FPM Service
Debian 13 (Trixie)1.26.x11.8.x8.4.xphp8.4-fpm
Debian 12 (Bookworm)1.22.x10.11.x8.2.xphp8.2-fpm
Debian 11 (Bullseye)1.18.x10.5.x7.4.xphp7.4-fpm

If an application requires a fixed PHP branch, compare the default path with the dedicated PHP on Debian guide before changing repositories. For upstream Nginx mainline features, use the separate Nginx Mainline on Debian guide instead of mixing package sources in this LEMP workflow.

Update Debian Package Lists

Refresh APT metadata before installing the stack. This checks every enabled APT source on the server, not only Debian’s default repositories.

sudo apt update

These commands use sudo for package installation, service management, and root-owned configuration files. If your user cannot run administrative commands yet, configure sudo access before continuing.

Install Nginx, MariaDB, and PHP-FPM Packages

Install the web server, database server, PHP-FPM service, PHP CLI, and common PHP extensions used by PHP applications. The unversioned PHP package names intentionally follow the default branch for the Debian release.

sudo apt install nginx mariadb-server mariadb-client php-fpm php php-cli php-mysql php-curl php-gd php-mbstring php-xml php-zip

The extension packages cover common application needs: php-mysql loads the MySQL/MariaDB driver, php-curl supports HTTP requests, php-gd handles basic image work, php-mbstring supports multibyte strings, php-xml supports XML parsing, and php-zip handles ZIP archives.

Install optional PHP extensions only when your application needs them. For example, image-heavy CMS sites may need PHP Imagick on Debian, while dependency-managed PHP projects often need Composer on Debian.

Verify LEMP Package Versions

Check the installed branches after APT finishes. On Debian, nginx lives under the administrative path, so use sudo nginx -v when a regular shell cannot find the command.

sudo nginx -v
mariadb --version
php -v

The exact patch versions change with Debian security updates, but the major branches should match the release table. For a deeper standalone web-server setup, see the dedicated Nginx on Debian guide.

Start and Check LEMP Services

Nginx, MariaDB, and PHP-FPM usually start after installation. Use the active PHP branch to build the correct PHP-FPM service name instead of copying release-specific commands.

PHP_FPM_SERVICE="php$(php -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;')-fpm"
sudo systemctl enable --now nginx mariadb "$PHP_FPM_SERVICE"
systemctl is-active nginx mariadb "$PHP_FPM_SERVICE"

A healthy stack returns one active line for each service.

active
active
active

Secure MariaDB on Debian

MariaDB installs with a local administrative account that normally works through Debian’s root socket workflow. Harden the fresh server before creating application databases or exposing any web application that depends on it.

Run the MariaDB Hardening Script

Debian’s current MariaDB packages provide mariadb-secure-installation. The older MySQL-named command is common in old examples, but the MariaDB-named command is the package-provided path on Debian’s supported releases.

sudo mariadb-secure-installation

The script is interactive. On a new local server, the safest general choices are to keep or enable socket-based root authentication, remove anonymous users, disallow remote root login, remove the test database, and reload privileges. If the server already hosts databases, read each prompt before accepting defaults.

For branch-specific database administration, repository choices, remote access, backups, and removal details, use the dedicated MariaDB on Debian guide and the official MariaDB documentation.

Confirm Local MariaDB Access

Verify that the local administrative connection still works after hardening.

sudo mariadb -e "SELECT VERSION();"

The command should print the MariaDB server version. If it fails with an authentication error, jump to the MariaDB troubleshooting section before creating application users.

Configure Nginx for PHP-FPM on Debian

Nginx serves static files by default. PHP execution starts only after the server block passes PHP requests to the active PHP-FPM socket.

Detect the PHP-FPM Socket

Build the PHP-FPM socket path from the installed PHP branch and confirm that the socket exists. Keep this terminal open because the server-block command later reuses $PHP_FPM_SOCKET.

PHP_VERSION=$(php -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;')
PHP_FPM_SOCKET="/run/php/php${PHP_VERSION}-fpm.sock"
test -S "$PHP_FPM_SOCKET" && printf 'Using PHP-FPM socket: %s\n' "$PHP_FPM_SOCKET"

Example output on Debian 13 shows the detected PHP 8.4 socket. Debian 12 and 11 show their matching PHP-FPM socket paths instead.

Using PHP-FPM socket: /run/php/php8.4-fpm.sock

Create the Web Root

Create a document root for the site and give your user account ownership so you can add application files without editing as root. Replace example.com with your real domain in every path and server-block value if you use a different site name.

sudo mkdir -p /var/www/html/example.com
sudo chown -R "$USER":www-data /var/www/html/example.com
sudo chmod -R 755 /var/www/html/example.com

The group ownership lets the web server read files while your user remains the normal owner. For CMS platforms that need writable upload or cache directories, grant write access only to those application-specific paths.

Create the Nginx Server Block

Create a Debian-style server block under /etc/nginx/sites-available/. The quoted heredoc preserves Nginx variables such as $uri, then the final sed command inserts the detected PHP-FPM socket.

sudo tee /etc/nginx/sites-available/example.com.conf > /dev/null <<'EOF'
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    root /var/www/html/example.com;
    index index.php index.html;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:@@PHP_FPM_SOCKET@@;
    }

    location ~ /\.ht {
        deny all;
    }
}
EOF

sudo sed -i "s|@@PHP_FPM_SOCKET@@|${PHP_FPM_SOCKET}|g" /etc/nginx/sites-available/example.com.conf

This example uses the Debian package layout with sites-available, sites-enabled, and the packaged snippets/fastcgi-php.conf file. Those paths are specific to Debian-family Nginx packages, not source-built Nginx or every Linux distribution.

Enable and Reload the Server Block

Enable the site, test the Nginx configuration, then reload the service only after the syntax check passes.

sudo ln -sfn /etc/nginx/sites-available/example.com.conf /etc/nginx/sites-enabled/example.com.conf
sudo nginx -t
sudo systemctl reload nginx

The Nginx syntax test should report success before the reload.

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

For directive behavior, module context, and advanced server-block design, use the official Nginx documentation.

Test the LEMP Stack on Debian

Package installation is only part of the job. Test one PHP request through Nginx and one PHP connection to MariaDB so you know the stack works beyond service status.

Test PHP Through Nginx

Create a temporary PHP health file and request it through Nginx. The --resolve option lets you test the server block locally before public DNS points to the server.

printf '<?php echo "PHP route OK\n"; ?>\n' | sudo tee /var/www/html/example.com/health.php > /dev/null
curl -fsS --resolve example.com:80:127.0.0.1 http://example.com/health.php
PHP route OK

Remove the health file after the test so it does not become a stray public endpoint.

sudo rm -f /var/www/html/example.com/health.php

Test PHP Connection to MariaDB

Create a temporary database and user, test the connection from PHP CLI with the same mysqli extension web applications use, then remove the test objects. This avoids leaving a browser-accessible database test file in the web root.

DB_TEST_PASSWORD="lc-$(date +%s)-$RANDOM"

sudo mariadb <<EOF
CREATE DATABASE lc_lemp_test;
CREATE USER 'lc_lemp_test'@'localhost' IDENTIFIED BY '${DB_TEST_PASSWORD}';
GRANT SELECT ON lc_lemp_test.* TO 'lc_lemp_test'@'localhost';
FLUSH PRIVILEGES;
EOF

DB_TEST_PASSWORD="$DB_TEST_PASSWORD" php -r '$db = new mysqli("localhost", "lc_lemp_test", getenv("DB_TEST_PASSWORD"), "lc_lemp_test"); if ($db->connect_errno) { fwrite(STDERR, $db->connect_error . PHP_EOL); exit(1); } echo "MariaDB route OK\n"; $db->close();'
MariaDB route OK

Clean up the temporary user, database, and shell variable after the smoke test.

sudo mariadb <<'EOF'
DROP USER IF EXISTS 'lc_lemp_test'@'localhost';
DROP DATABASE IF EXISTS lc_lemp_test;
EOF

unset DB_TEST_PASSWORD

Do not use the MariaDB root account from PHP applications. Create a dedicated database and least-privilege application user for each site so one compromised application cannot administer every database on the server.

Allow Web Traffic with UFW on Debian

A firewall is optional only when another control plane already handles ingress, such as a cloud firewall or hosting panel. On a normal Debian server using UFW, allow SSH first, then allow HTTP and HTTPS for Nginx.

Install UFW and Preserve SSH Access

Install UFW when it is not already present. If you are connected over SSH, allow SSH before enabling the firewall.

sudo apt install ufw
sudo ufw allow ssh

Allow Nginx HTTP and HTTPS Profiles

The Debian Nginx package registers UFW application profiles. Nginx Full allows HTTP and HTTPS over TCP, which is the usual profile before issuing a Let’s Encrypt certificate.

sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status verbose

If your server uses custom SSH ports, source-restricted access, HTTP/3, or other advanced rules, follow the full UFW on Debian workflow instead of relying only on this stack-level example.

Secure Nginx with Let’s Encrypt on Debian

Issue the TLS certificate only after the HTTP site works and port 80 is reachable from the internet. Certbot needs to prove control of the domain before it can install the certificate into the Nginx server block.

Install Certbot for Nginx

Install Certbot and the packaged Nginx plugin from Debian’s repositories.

sudo apt install python3-certbot-nginx

Request the Let’s Encrypt Certificate

Replace the email address and domain names before running Certbot. The --redirect flag lets Certbot add an HTTP-to-HTTPS redirect after the certificate is issued.

sudo certbot --nginx --redirect --agree-tos --email admin@example.com -d example.com -d www.example.com

After Certbot succeeds, check the certificate timer and test renewal behavior.

systemctl list-timers 'certbot*' --all
sudo certbot renew --dry-run

Use the dedicated Let’s Encrypt and Nginx on Debian guide for wildcard certificates, OCSP stapling, HSTS decisions, renewal troubleshooting, and DNS validation.

Manage LEMP Services on Debian

Once the stack is running, most daily administration is service checks, package updates, log inspection, and application deployment into the web root or a dedicated application directory.

Restart or Reload Services

Reload Nginx after safe configuration changes. Restart PHP-FPM after PHP pool or extension configuration changes, and restart MariaDB only during database maintenance windows.

PHP_FPM_SERVICE="php$(php -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;')-fpm"
sudo nginx -t
sudo systemctl reload nginx
sudo systemctl restart "$PHP_FPM_SERVICE"

View LEMP Logs

Use logs to separate web-server, PHP, and database failures. The PHP-FPM log path includes the active PHP branch.

PHP_VERSION=$(php -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;')
sudo tail -50 /var/log/nginx/error.log
sudo tail -50 "/var/log/php${PHP_VERSION}-fpm.log"
sudo journalctl -u mariadb --no-pager -n 50

Update LEMP Packages

Debian updates the stack through APT. Apply normal system updates during a maintenance window, especially on production servers with active PHP applications and databases.

sudo apt update
sudo apt upgrade

Back up application files and databases before major application upgrades, schema changes, or PHP branch changes. Package updates and application updates are separate jobs: APT updates Nginx, PHP, MariaDB, Certbot, and system libraries, while WordPress, Composer projects, and other applications have their own update workflows.

Troubleshoot LEMP Stack Issues

Most LEMP failures fall into one layer: Nginx cannot bind or route the request, PHP-FPM is inactive or using a different socket, MariaDB is not reachable, Certbot cannot validate the domain, or the firewall blocks the expected port.

Nginx Cannot Start on Port 80

A port conflict means another service already owns the HTTP listener. Check the current listener before stopping anything.

sudo ss -H -ltnp 'sport = :80'

If Apache owns the port and you no longer need the Apache site, stop Apache and start Nginx. If Apache still serves another site, move one service to another port or migrate the virtual host intentionally.

sudo systemctl stop apache2
sudo systemctl start nginx

PHP Files Download Instead of Executing

Downloaded PHP files usually mean Nginx is serving the file as static content instead of passing it to PHP-FPM. Check that the enabled server block contains a PHP location and that the socket path exists.

PHP_VERSION=$(php -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;')
PHP_FPM_SOCKET="/run/php/php${PHP_VERSION}-fpm.sock"
test -S "$PHP_FPM_SOCKET" && ls -l "$PHP_FPM_SOCKET"
sudo grep -R --line-number "fastcgi_pass" /etc/nginx/sites-enabled/

If the socket is missing, restart the matching PHP-FPM service. If the socket exists but Nginx points somewhere else, correct the fastcgi_pass value, run sudo nginx -t, and reload Nginx.

502 Bad Gateway from PHP-FPM

A 502 response commonly means Nginx reached the PHP location but could not connect to PHP-FPM. Confirm the active service and socket path together.

PHP_FPM_SERVICE="php$(php -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;')-fpm"
systemctl is-active "$PHP_FPM_SERVICE"
sudo systemctl status "$PHP_FPM_SERVICE" --no-pager

Start the service if it is inactive, then retest Nginx and the PHP health file.

sudo systemctl start "$PHP_FPM_SERVICE"
sudo nginx -t
sudo systemctl reload nginx

MariaDB Connection Refused or Access Denied

Separate service failure from credential failure. First confirm that MariaDB is active and that local administrative access works.

systemctl is-active mariadb
sudo mariadb -e "SELECT VERSION();"

If root access works but PHP receives Access denied, check the application username, password, database name, and host value. A user created for 'app'@'localhost' is not the same account as 'app'@'%' or 'app'@'127.0.0.1'.

An error about caching_sha2_password usually means the client is connecting to a MySQL 8 account that uses MySQL’s authentication plugin, not the local MariaDB server installed in this stack. Treat that as a remote MySQL compatibility problem instead of reinstalling MariaDB.

Certbot Cannot Validate the Domain

Certbot’s Nginx plugin still depends on public HTTP validation unless you use a DNS challenge. Confirm DNS points to the server and that HTTP reaches the correct server block before requesting the certificate.

curl -I http://example.com
sudo nginx -t
sudo ufw status verbose

If the request reaches a different host, fix DNS or the upstream firewall first. If the request reaches Nginx but not the right site, check the server_name values and enabled server-block symlink.

Remove LEMP Stack Components

Remove LEMP packages only after moving or backing up the sites and databases they serve. Package removal, site-file cleanup, and database-data deletion are separate actions with different risk levels.

Stop LEMP Services

Stop the services before removing packages. Build the PHP-FPM unit name while PHP is still installed.

PHP_FPM_SERVICE="php$(php -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;')-fpm"
sudo systemctl stop nginx mariadb "$PHP_FPM_SERVICE"

Remove Installed LEMP Packages

Remove the package set installed for the stack. Leave UFW installed if it protects SSH or other services, and leave Certbot installed if other Nginx sites still use Let’s Encrypt certificates.

sudo apt remove nginx mariadb-server mariadb-client php php-fpm php-cli php-mysql php-curl php-gd php-mbstring php-xml php-zip python3-certbot-nginx

Review autoremovable dependencies separately so APT does not remove unrelated packages left over from earlier work.

apt-get -s autoremove

If the simulation lists only packages you intend to remove, run the real cleanup.

sudo apt autoremove

Remove the Example Site Files

List the example site paths before deleting anything. Change the domain in these commands if you used a different server-block filename or document root.

sudo find /etc/nginx/sites-available /etc/nginx/sites-enabled /var/www/html -maxdepth 1 -name 'example.com*' -print

The next command permanently deletes the example Nginx server block and web root. Back up application files, uploads, certificates, and custom configuration before deleting site paths.

sudo rm -f /etc/nginx/sites-enabled/example.com.conf /etc/nginx/sites-available/example.com.conf
sudo rm -rf /var/www/html/example.com

Optionally Remove MariaDB Data

APT package removal does not automatically mean every database should be deleted. Export any database you need before removing MariaDB data directories.

sudo find /var/lib/mysql /etc/mysql /var/log/mysql -maxdepth 0 -print 2>/dev/null

The next command permanently deletes MariaDB databases, local configuration, and database logs. Do not run it on a server that has databases you may need later.

sudo rm -rf /var/lib/mysql /etc/mysql /var/log/mysql

Verify LEMP Removal

Check for remaining installed package rows and open HTTP or HTTPS listeners.

dpkg-query -W -f='${db:Status-Abbrev} ${binary:Package}\n' nginx mariadb-server php php-fpm php-cli php-mysql 2>/dev/null | grep '^ii' || true
ss -H -ltn 'sport = :80'
ss -H -ltn 'sport = :443'

No ii package rows and no listener output mean the reviewed stack packages and web ports are no longer active. If another web server still listens on port 80 or 443, inspect that service before deleting unrelated files.

Conclusion

Nginx is serving the Debian site through a PHP-FPM socket, MariaDB accepts local PHP connections, and the stack has clear checks for firewall and certificate readiness. From here, deploy an application such as WordPress with Nginx on Debian or add phpMyAdmin with Nginx on Debian for database administration.

Share this guide

Help another Linux user troubleshoot faster

Share this guide with someone troubleshooting Linux systems or saving it for later.

Follow LinuxCapable

Want more LinuxCapable guides in Google?

Add LinuxCapable as a preferred source so Google can show our tutorials more often in Top Stories and mark them as preferred in AI Mode and AI Overviews when relevant.

Add LinuxCapable as a preferred source on Google
Search LinuxCapable

Need another guide?

Search LinuxCapable for package installs, commands, troubleshooting, and follow-up guides related to what you just read.

Found this guide useful?

Support LinuxCapable to keep tutorials free and up to date.

Buy me a coffeeBuy me a coffee
Before commenting, please review our Comments Policy.
Formatting tips for your comment

You can use basic HTML to format your comment. Useful tags currently allowed in published comments:

You type Result
<code>command</code> command
<strong>bold</strong> bold
<em>italic</em> italic
<a href="https://example.com">link</a> link
<blockquote>quote</blockquote> quote block

Add to the discussion

Questions, fixes, command output, and version notes help keep this guide current.

Verify before posting: