Install LEMP Stack on Ubuntu 24.04
Install Nginx, MariaDB, PHP 8, PHP-FPM, and phpMyAdmin on Ubuntu 24.04 with optional multi-PHP support and Nginx virtual hosts.
This guide walks through a full LEMP stack setup on Ubuntu 24.04 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-resolvedconfigured- 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 newuserGrant sudo access:
sudo usermod -aG sudo newuserSwitch to the new user and verify sudo works:
su - newuser
sudo whoamiIf the command returns root, sudo is configured correctly.
Update Packages
Update package metadata and upgrade installed packages:
sudo apt update && sudo apt upgradeChange the SSH Port
Edit the SSH server config:
sudo vim /etc/ssh/sshd_configSet a custom port and disable root login:
Port 52225
PermitRootLogin noReload the SSH service:
sudo systemctl daemon-reload
sudo systemctl restart sshEnable the Firewall
Install UFW:
sudo apt install ufwAllow HTTP, HTTPS, and your custom SSH port:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 52225/tcpEnable the firewall:
sudo ufw enableCheck active rules:
sudo ufw statusConnect over the new SSH port:
ssh -p 52225 user@your_server_ipEnabling 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-resolvedEdit the resolver config:
sudo vim /etc/systemd/resolved.confUse 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=opportunisticPoint resolv.conf at the stub resolver and restart:
sudo ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
sudo systemctl restart systemd-resolvedCheck resolver status:
resolvectl statusGenerate 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 3650Install PHP 8 and PHP-FPM
Check the PHP packages available in the Ubuntu repositories:
sudo apt search php | egrep '^php[0-9]'Install PHP 8.3 and common extensions:
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-gmpThose packages cover common dependencies for WordPress and similar PHP applications.
Optional: Install Multiple PHP Versions
If you want to run multiple PHP versions, add the Ondrej Sury PHP repository:
sudo apt install software-properties-common
sudo add-apt-repository ppa:ondrej/php
sudo apt updateInstall 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-gmpConfigure PHP-FPM Pools
Edit the PHP 8.3 pool:
sudo vim /etc/php/8.3/fpm/pool.d/www.confUse:
[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 = 0660Restart the service:
sudo systemctl restart php8.3-fpmIf you also installed PHP 7.4, edit its pool:
sudo vim /etc/php/7.4/fpm/pool.d/www.confUse:
[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 = 0660Restart PHP 7.4 FPM:
sudo systemctl restart php7.4-fpmDisable commonly abused functions:
sudo vim /etc/php/8.3/fpm/php.ini
sudo vim /etc/php/7.4/fpm/php.iniSet:
disable_functions = exec,passthru,shell_exec,system,proc_open,popenEnable 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=1Restart both FPM services if you configured both versions:
sudo systemctl restart php8.3-fpm
sudo systemctl restart php7.4-fpmInstall Nginx
You can use either the Ubuntu package or the official Nginx repository.
Install from the Ubuntu repository:
sudo apt install nginx
sudo ufw allow 'Nginx Full'
sudo ufw reload
sudo systemctl enable nginx
sudo systemctl start nginx
sudo systemctl status nginxIf you want the latest Nginx from the official repository, add the signing key:
curl -fsSL https://nginx.org/keys/nginx_signing.key | gpg --dearmor | sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/nullAdd the stable repo:
echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] http://nginx.org/packages/ubuntu `lsb_release -cs` nginx" | sudo tee /etc/apt/sources.list.d/nginx.listOr add the mainline repo:
echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] http://nginx.org/packages/mainline/ubuntu `lsb_release -cs` nginx" | sudo tee /etc/apt/sources.list.d/nginx.listThen install and start it:
sudo apt update
sudo apt install nginx
sudo systemctl enable nginx
sudo systemctl start nginx
sudo systemctl status nginxPrepare the site config directories:
sudo mkdir -p /etc/nginx/sites-enabled
sudo mkdir -p /etc/nginx/sites-availableBack up the current config:
sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bakEdit the main config:
sudo vim /etc/nginx/nginx.confReplace 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/defaultUse:
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/defaultTest and restart Nginx:
sudo nginx -t
sudo systemctl restart nginxCreate a temporary PHP test file:
echo "<?php
phpinfo();
?>" | sudo tee /usr/share/nginx/html/test.phpOpen the test page:
http://server_ip/test.phpRemove the test file when finished:
sudo rm /usr/share/nginx/html/test.phpInstall MariaDB
Install MariaDB server and client:
sudo apt install mariadb-server mariadb-clientEnable the service:
sudo systemctl enable mariadb
sudo systemctl status mariadbVerify MariaDB is listening:
sudo netstat -tulnp | grep mariadbSecure the installation:
sudo mysql_secure_installationRecommended answers:
- press
Enterto leave the root password empty if you want local-only development first - answer
Yto switch to Unix socket authentication - answer
Yto remove anonymous users - answer
Yto disallow remote root login - answer
Yto remove the test database - answer
Yto reload privilege tables
The main MariaDB config file is:
sudo vim /etc/mysql/my.cnfInstall phpMyAdmin
Check the latest upstream release before downloading:
https://www.phpmyadmin.net/downloadsDownload 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.gzExtract it:
sudo tar -xvzf phpMyAdmin-5.2.2-english.tar.gzMove it into place:
sudo mkdir -p /usr/share/phpmyadmin
sudo mv /tmp/phpMyAdmin-5.2.2-english/* /usr/share/phpmyadmin
ls -l /usr/share/phpmyadminCreate the temp directory:
sudo mkdir -p /usr/share/phpmyadmin/tmp
sudo chown -R www-data:www-data /usr/share/phpmyadmin/tmpCreate the phpMyAdmin database and control user:
sudo mariadbRun:
CREATE DATABASE phpmyadmin;
CREATE USER 'pma'@'localhost' IDENTIFIED BY 'strong_password_here';
GRANT SELECT, INSERT, UPDATE, DELETE ON phpmyadmin.* TO 'pma'@'localhost';
FLUSH PRIVILEGES;
exitGenerate a random blowfish secret:
openssl rand -base64 24Copy 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.phpSet 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.sqlCreate a password hash:
openssl passwdCreate the Nginx basic auth file:
sudo vim /etc/nginx/pma_passUse:
pma:password_hashCreate the phpMyAdmin site config:
sudo vim /etc/nginx/sites-available/phpmyadminUse:
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/phpmyadminTest and restart Nginx:
sudo nginx -t
sudo systemctl restart nginxCreate a database and user for your application:
sudo mariadbRun:
CREATE USER 'example_user'@'localhost' IDENTIFIED BY 'password';
CREATE DATABASE example_db;
GRANT ALL PRIVILEGES ON example_db.* TO 'example_user'@'localhost';
FLUSH PRIVILEGES;
exitYou 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 dolphinCreate 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.comCopy 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.confUse:
[php8.3-fpm-dolphin]
user = dolphin
group = dolphin
listen = /run/php/php8.3-fpm-dolphin.sock
listen.owner = www
listen.group = www
listen.mode = 0660Restart PHP-FPM:
sudo systemctl restart php8.3-fpmCreate the site virtual host:
sudo vim /etc/nginx/sites-available/example.comUse:
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.comTest and restart Nginx:
sudo nginx -t
sudo service nginx restartInstall WordPress
Switch to the site user:
sudo su - dolphinDownload 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 wordpressOpen https://example.com in your browser to continue the WordPress installer.