Nauris
LEMP Setup

Install LEMP Stack on Debian 12

Install Nginx, MariaDB, PHP 8, PHP-FPM, and phpMyAdmin on Debian 12 with optional multi-PHP support and Nginx virtual hosts.

This guide walks through a full LEMP stack setup on Debian 12 using Nginx, MariaDB, PHP 8, PHP-FPM, and phpMyAdmin.

By the end, you will have:

  • a sudo-enabled admin user
  • updated system packages
  • SSH on a custom port
  • UFW enabled
  • systemd-resolved configured
  • Nginx, MariaDB, PHP, and phpMyAdmin installed
  • optional multi-PHP support
  • a dedicated PHP-FPM pool and Nginx virtual host for a site

Create a Sudo User

Skip this step if you already have a working sudo user.

Create a new user:

adduser newuser

Grant sudo access:

sudo usermod -aG sudo newuser

Switch to the new user and verify sudo works:

su - newuser
sudo whoami

If the command returns root, sudo is configured correctly.

Update Packages

Update package metadata and upgrade installed packages:

sudo apt update && sudo apt upgrade

Change the SSH Port

Edit the SSH server config:

sudo vim /etc/ssh/sshd_config

Set a custom port and disable root login:

Port 52225
PermitRootLogin no

Reload the SSH service:

sudo systemctl daemon-reload
sudo systemctl restart ssh

Enable the Firewall

Install UFW:

sudo apt install ufw

Allow HTTP, HTTPS, and your custom SSH port:

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 52225/tcp

Enable the firewall:

sudo ufw enable

Check active rules:

sudo ufw status

Connect over the new SSH port:

ssh -p 52225 user@your_server_ip

Enabling the firewall can drop the current SSH session. Reconnect using the new port if needed.

Configure DNS Resolver

Install and enable systemd-resolved:

sudo apt install systemd-resolved
sudo systemctl enable systemd-resolved
sudo systemctl start systemd-resolved
sudo systemctl status systemd-resolved

Edit the resolver config:

sudo vim /etc/systemd/resolved.conf

Use Cloudflare and Quad9:

[Resolve]
DNS=1.1.1.1 1.0.0.1 2606:4700:4700::1111 2606:4700:4700::1001
FallbackDNS=9.9.9.9 149.112.112.112 2620:fe::fe 2620:fe::9
DNSOverTLS=opportunistic

Point resolv.conf at the stub resolver and restart:

sudo ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
sudo systemctl restart systemd-resolved

Check resolver status:

resolvectl status

Generate an SSL Certificate

Create a self-signed certificate for local testing or internal use:

sudo openssl req -x509 -newkey rsa:2048 -nodes -keyout /etc/ssl/private/default.key -out /etc/ssl/certs/default.crt -days 3650

Install PHP 8 and PHP-FPM

Check available PHP packages:

sudo apt search php | egrep '^php[0-9]'

Install PHP 8.2 and common extensions from the default Debian repository:

sudo apt install php8.2 php8.2-cli php8.2-fpm php8.2-common php8.2-mysql php8.2-zip php8.2-gd php8.2-mbstring php8.2-curl php8.2-xml php8.2-bcmath php8.2-gmp

Those packages cover common dependencies for WordPress and similar PHP applications.

Optional: Install Multiple PHP Versions

If you want to run multiple PHP versions or newer PHP packages, add the Sury PHP repository:

sudo apt install -y apt-transport-https lsb-release ca-certificates gnupg gnupg2
sudo wget -O /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg
echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/php.list
sudo apt update

Install both PHP 7.4 and PHP 8.3:

sudo apt install php7.4 php7.4-cli php7.4-fpm php7.4-common php7.4-mysql php7.4-zip php7.4-gd php7.4-mbstring php7.4-curl php7.4-xml php7.4-bcmath php7.4-gmp
sudo apt install php8.3 php8.3-cli php8.3-fpm php8.3-common php8.3-mysql php8.3-zip php8.3-gd php8.3-mbstring php8.3-curl php8.3-xml php8.3-bcmath php8.3-gmp

Configure PHP-FPM Pools

Edit the PHP 8.3 pool:

sudo vim /etc/php/8.3/fpm/pool.d/www.conf

Use:

[php-fpm-8.3]
user = www-data
group = www-data

listen = /run/php/php8.3-fpm.sock

listen.owner = www-data
listen.group = www-data
listen.mode = 0660

Restart the service:

sudo systemctl restart php8.3-fpm

If you also installed PHP 7.4, edit its pool:

sudo vim /etc/php/7.4/fpm/pool.d/www.conf

Use:

[php-fpm-7.4]
user = www-data
group = www-data

listen = /run/php/php7.4-fpm.sock

listen.owner = www-data
listen.group = www-data
listen.mode = 0660

Restart PHP 7.4 FPM:

sudo systemctl restart php7.4-fpm

Disable commonly abused functions:

sudo vim /etc/php/8.3/fpm/php.ini
sudo vim /etc/php/7.4/fpm/php.ini

Set:

disable_functions = exec,passthru,shell_exec,system,proc_open,popen

Enable OPcache:

[opcache]
opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=10000
opcache.use_cwd=1
opcache.validate_timestamps=1
opcache.revalidate_freq=60
opcache.save_comments=1

Restart both FPM services if you configured both versions:

sudo systemctl restart php8.3-fpm
sudo systemctl restart php7.4-fpm

Install Nginx

You can use either the Debian package or the official Nginx repository.

Install from the Debian repository:

sudo apt install nginx
sudo ufw allow 'Nginx Full'
sudo ufw reload
sudo systemctl enable nginx
sudo systemctl start nginx
sudo systemctl status nginx

If you want the latest Nginx from the official repository, add the signing key:

curl -fsSL https://nginx.org/keys/nginx_signing.key | sudo gpg --dearmor -o /usr/share/keyrings/nginx-archive-keyring.gpg

Add the Debian 12 repository:

sudo vim /etc/apt/sources.list.d/nginx.list

Use:

deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] http://nginx.org/packages/debian bookworm nginx

Then install and start it:

sudo apt update
sudo apt install nginx
sudo systemctl enable nginx
sudo systemctl start nginx
sudo systemctl status nginx

Prepare the site config directories:

sudo mkdir -p /etc/nginx/sites-enabled
sudo mkdir -p /etc/nginx/sites-available

Back up the current config:

sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak

Edit the main config:

sudo vim /etc/nginx/nginx.conf

Replace it with:

user www-data;
worker_processes auto;
pid /run/nginx.pid;
error_log /var/log/nginx/error.log;
include /etc/nginx/modules-enabled/*.conf;

events {
  worker_connections 1024;
}

http {
  sendfile on;
  tcp_nopush on;
  keepalive_timeout 65;
  types_hash_max_size 4096;
  types_hash_bucket_size 128;

  access_log off;

  include /etc/nginx/mime.types;
  default_type application/octet-stream;

  gzip on;
  gzip_vary on;
  gzip_comp_level 5;
  gzip_min_length 512;
  gzip_buffers 8 64k;
  gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml+rss application/x-font-ttf image/svg+xml font/opentype;
  gzip_proxied any;
  gzip_disable "msie6";

  ssl_session_cache shared:SSL:10m;
  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_prefer_server_ciphers on;
  ssl_ciphers HIGH:!aNULL:!MD5;

  proxy_cache_path /var/cache/nginx levels=2 keys_zone=cache:10m inactive=60m max_size=1024m;
  proxy_cache_key "$host$request_uri";
  proxy_temp_path /var/cache/nginx/temp;
  proxy_ignore_headers Expires Cache-Control;
  proxy_cache_use_stale error timeout invalid_header http_502;
  proxy_cache_valid any 1d;

  open_file_cache max=10000 inactive=30s;
  open_file_cache_valid 60s;
  open_file_cache_min_uses 2;
  open_file_cache_errors off;

  map $http_cookie $no_cache {
    default 0;
    ~SESS 1;
    ~wordpress_logged_in 1;
  }

  include /etc/nginx/sites-enabled/*;
}

Create the default virtual host:

sudo vim /etc/nginx/sites-available/default

Use:

server {
  listen 80 default_server;
  listen [::]:80 default_server;
  listen 443 ssl default_server;
  listen [::]:443 ssl default_server;
  server_name _;

  ssl_certificate /etc/ssl/certs/default.crt;
  ssl_certificate_key /etc/ssl/private/default.key;

  root /usr/share/nginx/html;
  index index.html index.htm index.php;

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

    location ~ \.php$ {
      root /usr/share/nginx/html;
      fastcgi_pass unix:/run/php/php8.3-fpm.sock;
      fastcgi_index index.php;
      fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
      include fastcgi_params;
    }
  }

  location ~ /\.ht {
    deny all;
  }

  error_page 500 502 503 504 /50x.html;
  location = /50x.html {
    root /usr/share/nginx/html;
  }
}

Enable the default site:

sudo ln -s /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default

Test and restart Nginx:

sudo nginx -t
sudo systemctl restart nginx

Create a temporary PHP test file:

echo "<?php
phpinfo();
?>" | sudo tee /usr/share/nginx/html/test.php

Open the test page:

http://server_ip/test.php

Remove the test file when finished:

sudo rm /usr/share/nginx/html/test.php

Install MariaDB

Install MariaDB server and client:

sudo apt install mariadb-server mariadb-client

Enable the service:

sudo systemctl enable mariadb
sudo systemctl status mariadb

Verify MariaDB is listening:

sudo netstat -tulnp | grep mariadb

Secure the installation:

sudo mysql_secure_installation

Recommended answers:

  • press Enter to leave the root password empty if you want local-only development first
  • answer Y to switch to Unix socket authentication
  • answer Y to remove anonymous users
  • answer Y to disallow remote root login
  • answer Y to remove the test database
  • answer Y to reload privilege tables

The main MariaDB config file is:

sudo vim /etc/mysql/my.cnf

Install phpMyAdmin

Check the latest upstream release before downloading:

https://www.phpmyadmin.net/downloads

Download the release archive used in this guide:

cd /tmp
curl -O https://files.phpmyadmin.net/phpMyAdmin/5.2.2/phpMyAdmin-5.2.2-english.tar.gz

Extract it:

sudo tar -xvzf phpMyAdmin-5.2.2-english.tar.gz

Move it into place:

sudo mkdir -p /usr/share/phpmyadmin
sudo mv /tmp/phpMyAdmin-5.2.2-english/* /usr/share/phpmyadmin
ls -l /usr/share/phpmyadmin

Create the temp directory:

sudo mkdir -p /usr/share/phpmyadmin/tmp
sudo chown -R www-data:www-data /usr/share/phpmyadmin/tmp

Create the phpMyAdmin database and control user:

sudo mariadb

Run:

CREATE DATABASE phpmyadmin;
CREATE USER 'pma'@'localhost' IDENTIFIED BY 'strong_password_here';
GRANT SELECT, INSERT, UPDATE, DELETE ON phpmyadmin.* TO 'pma'@'localhost';
FLUSH PRIVILEGES;
exit

Generate a random blowfish secret:

openssl rand -base64 24

Copy the sample config and edit it:

sudo cp /usr/share/phpmyadmin/config.sample.inc.php /usr/share/phpmyadmin/config.inc.php
sudo vim /usr/share/phpmyadmin/config.inc.php

Set the generated secret:

$cfg['blowfish_secret'] = 'your_code';

Add the temp directory and hide system databases:

$cfg['TempDir'] = '/usr/share/phpmyadmin/tmp';
$cfg['Servers'][$i]['hide_db'] = '^information_schema|mysql|phpmyadmin|performance_schema|sys$';

Set the control user credentials:

$cfg['Servers'][$i]['controluser'] = 'pma';
$cfg['Servers'][$i]['controlpass'] = 'strong_password_here';

Enable storage tables:

$cfg['Servers'][$i]['pmadb'] = 'phpmyadmin';
$cfg['Servers'][$i]['bookmarktable'] = 'pma__bookmark';
$cfg['Servers'][$i]['relation'] = 'pma__relation';
$cfg['Servers'][$i]['table_info'] = 'pma__table_info';
$cfg['Servers'][$i]['table_coords'] = 'pma__table_coords';
$cfg['Servers'][$i]['pdf_pages'] = 'pma__pdf_pages';
$cfg['Servers'][$i]['column_info'] = 'pma__column_info';
$cfg['Servers'][$i]['history'] = 'pma__history';
$cfg['Servers'][$i]['table_uiprefs'] = 'pma__table_uiprefs';
$cfg['Servers'][$i]['tracking'] = 'pma__tracking';
$cfg['Servers'][$i]['userconfig'] = 'pma__userconfig';
$cfg['Servers'][$i]['recent'] = 'pma__recent';
$cfg['Servers'][$i]['favorite'] = 'pma__favorite';
$cfg['Servers'][$i]['users'] = 'pma__users';
$cfg['Servers'][$i]['usergroups'] = 'pma__usergroups';
$cfg['Servers'][$i]['navigationhiding'] = 'pma__navigationhiding';
$cfg['Servers'][$i]['savedsearches'] = 'pma__savedsearches';
$cfg['Servers'][$i]['central_columns'] = 'pma__central_columns';
$cfg['Servers'][$i]['designer_settings'] = 'pma__designer_settings';
$cfg['Servers'][$i]['export_templates'] = 'pma__export_templates';

Create the phpMyAdmin tables:

sudo mysql -u root -p phpmyadmin < /usr/share/phpmyadmin/sql/create_tables.sql

Create a password hash:

openssl passwd

Create the Nginx basic auth file:

sudo vim /etc/nginx/pma_pass

Use:

pma:password_hash

Create the phpMyAdmin site config:

sudo vim /etc/nginx/sites-available/phpmyadmin

Use:

server {
  listen 80;
  listen [::]:80;
  listen 443 ssl;
  listen [::]:443 ssl;
  server_name pma.example.com;

  ssl_certificate /etc/ssl/certs/default.crt;
  ssl_certificate_key /etc/ssl/private/default.key;

  access_log off;
  error_log /var/log/nginx/pma.example.com.error.log error;

  root /usr/share/phpmyadmin;
  index index.html index.htm index.php;

  location / {
    try_files $uri $uri/ /index.php?$args;
    auth_basic "Admin Login";
    auth_basic_user_file /etc/nginx/pma_pass;

    location ~ \.php$ {
      root /usr/share/phpmyadmin;
      fastcgi_pass unix:/run/php/php8.3-fpm.sock;
      fastcgi_index index.php;
      fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
      include fastcgi_params;
    }
  }

  location ~ /\.ht {
    deny all;
  }

  error_page 500 502 503 504 /50x.html;
  location = /50x.html {
    root /usr/share/nginx/html;
  }
}

Replace pma.example.com with your actual subdomain.

Enable the site:

sudo ln -s /etc/nginx/sites-available/phpmyadmin /etc/nginx/sites-enabled/phpmyadmin

Test and restart Nginx:

sudo nginx -t
sudo systemctl restart nginx

Create a database and user for your application:

sudo mariadb

Run:

CREATE USER 'example_user'@'localhost' IDENTIFIED BY 'password';
CREATE DATABASE example_db;
GRANT ALL PRIVILEGES ON example_db.* TO 'example_user'@'localhost';
FLUSH PRIVILEGES;
exit

You should now be able to sign in to phpMyAdmin at https://pma.example.com.

Create a Dedicated Site User

Create a non-sudo user for the hosted application:

sudo adduser --disabled-password dolphin

Create the document root and fix ownership:

sudo mkdir -p /var/www/dolphin/example.com
sudo chmod 711 /var/www/dolphin
sudo chown root:root /var/www
sudo chown -R dolphin:dolphin /var/www/dolphin
sudo chmod -R 755 /var/www/dolphin/example.com

Copy the PHP 8.3 pool config:

sudo cp /etc/php/8.3/fpm/pool.d/www.conf /etc/php/8.3/fpm/pool.d/dolphin.conf
sudo vim /etc/php/8.3/fpm/pool.d/dolphin.conf

Use:

[php8.3-fpm-dolphin]
user = dolphin
group = dolphin

listen = /run/php/php8.3-fpm-dolphin.sock

listen.owner = www
listen.group = www
listen.mode = 0660

Restart PHP-FPM:

sudo systemctl restart php8.3-fpm

Create the site virtual host:

sudo vim /etc/nginx/sites-available/example.com

Use:

server {
  listen 80;
  listen [::]:80;
  listen 443 ssl;
  listen [::]:443 ssl;
  server_name example.com www.example.com;

  ssl_certificate /etc/ssl/certs/default.crt;
  ssl_certificate_key /etc/ssl/private/default.key;

  access_log off;
  error_log /var/log/nginx/example.com.error.log error;

  root /var/www/dolphin/example.com;
  index index.html index.htm index.php;

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

    location ~ \.php$ {
      root /var/www/dolphin/example.com;
      fastcgi_pass unix:/run/php/php8.3-fpm-dolphin.sock;
      fastcgi_index index.php;
      fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
      include fastcgi_params;
    }
  }

  location ~ /\.ht {
    deny all;
  }

  error_page 500 502 503 504 /50x.html;
  location = /50x.html {
    root /usr/share/nginx/html;
  }
}

Enable the site:

sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/example.com

Test and restart Nginx:

sudo nginx -t
sudo service nginx restart

Install WordPress

Switch to the site user:

sudo su - dolphin

Download and extract WordPress:

cd /var/www/dolphin/example.com
curl -O https://wordpress.org/latest.tar.gz
tar -xvzf latest.tar.gz
mv wordpress/* .
rm latest.tar.gz && rm -fr wordpress

Open https://example.com in your browser to continue the WordPress installer.

On this page