How to Set Up a LEMP Stack on Ubuntu 26.04

How to set up a LEMP stack on Ubuntu 26.04

Tested on: Ubuntu 26.04 LTS · Debian 12 · Hetzner CX22 VPS — Last updated: June 2026

A LEMP stack — Linux, Nginx, MariaDB, PHP — is the production-grade foundation for hosting web applications and WordPress sites on your own server. Nginx handles static files and reverse-proxies PHP requests to PHP-FPM efficiently, consuming a fraction of the RAM Apache would under the same load. This guide covers a complete, hardened LEMP installation on Ubuntu 26.04: Nginx tuned for performance, MariaDB with sane InnoDB defaults, PHP 8.3 with OPcache enabled, and a free TLS certificate from Let's Encrypt.

Contents
  1. Prerequisites
  2. Step 1 — Install and Configure Nginx
    1. Tune the Main Nginx Config
  3. Step 2 — Install MariaDB
    1. Create a Database and Application User
    2. Tune MariaDB for a 2 GB VPS
  4. Step 3 — Install PHP 8.3
    1. Configure PHP-FPM and OPcache
    2. Tune the PHP-FPM Worker Pool
  5. Step 4 — Configure the Nginx Virtual Host
  6. Step 5 — Add an SSL Certificate with Let's Encrypt
  7. Step 6 — Install WordPress
  8. Troubleshooting
    1. 502 Bad Gateway
    2. Further Reading

Prerequisites

  • Ubuntu 26.04 LTS server — VPS or bare metal, minimum 1 GB RAM (2 GB recommended for WordPress)
  • A non-root user with sudo privileges
  • A domain name with its DNS A record pointing to your server's IP address (required for SSL)
  • Ports 80 and 443 open in your firewall or cloud security group
  • Basic familiarity with the terminal and a text editor such as nano

Start by updating the system and setting a shell variable for your domain — you'll reuse it throughout:

sudo apt update && sudo apt upgrade -y

# Set once, use everywhere
DOMAIN="yourdomain.com"

Step 1 — Install and Configure Nginx

sudo apt install nginx -y
sudo systemctl enable --now nginx

# Verify it's running
systemctl status nginx

# Quick HTTP check
curl -I http://localhost

You should see HTTP/1.1 200 OK in the curl output. Open the firewall:

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

Tune the Main Nginx Config

Open /etc/nginx/nginx.conf and adjust the http block. The defaults are conservative; these settings suit a 2 GB VPS hosting one or two sites:

sudo nano /etc/nginx/nginx.conf
user www-data;
worker_processes auto;          # matches CPU core count automatically
pid /run/nginx.pid;

events {
    worker_connections 1024;
    multi_accept on;
}

http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    server_tokens off;          # hides Nginx version from response headers

    # Gzip compression
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css application/json application/javascript
               text/xml application/xml application/xml+rss text/javascript
               image/svg+xml;

    # FastCGI buffers (tune for PHP-FPM)
    fastcgi_buffers 16 16k;
    fastcgi_buffer_size 32k;
    client_max_body_size 64M;

    # Rate limiting zone — referenced in the WordPress login block later
    limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;

    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    include /etc/nginx/sites-enabled/*;
}
sudo nginx -t && sudo systemctl reload nginx

Step 2 — Install MariaDB

Ubuntu 26.04 ships MariaDB 10.11. It's fully API-compatible with MySQL — your application code doesn't need to change.

sudo apt install mariadb-server -y
sudo systemctl enable --now mariadb

# Lock down the installation interactively
sudo mysql_secure_installation

Answer the prompts as follows: set a strong root password, remove anonymous users, disallow remote root login, remove the test database, and reload privilege tables. All answers should be YES.

Create a Database and Application User

sudo mysql -u root -p
-- Run inside the MariaDB shell
CREATE DATABASE myapp_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'myapp_user'@'localhost' IDENTIFIED BY 'use-a-strong-password-here';
GRANT ALL PRIVILEGES ON myapp_db.* TO 'myapp_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;

Tune MariaDB for a 2 GB VPS

sudo nano /etc/mysql/mariadb.conf.d/50-server.cnf

Add or update these values in the [mysqld] section:

[mysqld]
innodb_buffer_pool_size = 512M    # 50–70% of RAM on a dedicated DB server
innodb_log_file_size    = 128M
innodb_flush_log_at_trx_commit = 2   # safer than 0; slightly faster than 1
query_cache_type        = 0          # deprecated, causes mutex contention
slow_query_log          = 1
slow_query_log_file     = /var/log/mysql/slow.log
long_query_time         = 2
sudo systemctl restart mariadb

Step 3 — Install PHP 8.3

Ubuntu 26.04 ships PHP 8.3 in its standard repositories. Install PHP-FPM and the extensions you'll need for WordPress and most PHP applications:

sudo apt install php8.3-fpm php8.3-mysql php8.3-xml php8.3-mbstring 
  php8.3-curl php8.3-zip php8.3-gd php8.3-intl php8.3-bcmath 
  php8.3-imagick -y

sudo systemctl enable --now php8.3-fpm

# Confirm versions
php8.3 -v
systemctl status php8.3-fpm

Expected output from php8.3 -v:

PHP 8.3.x (cli) (built: ...)
Copyright (c) The PHP Group
Zend Engine v4.3.x, Copyright (c) Zend Technologies
    with Zend OPcache v8.3.x, ...

Configure PHP-FPM and OPcache

sudo nano /etc/php/8.3/fpm/php.ini

Find and update these values (use Ctrl+W in nano to search):

; File uploads
upload_max_filesize = 64M
post_max_size       = 64M

; Memory and execution
memory_limit        = 256M
max_execution_time  = 60
max_input_vars      = 3000

; Security
expose_php     = Off
display_errors = Off
log_errors     = On
allow_url_fopen = Off

; OPcache — bytecode cache, dramatically reduces PHP overhead
opcache.enable                 = 1
opcache.memory_consumption     = 128
opcache.interned_strings_buffer = 8
opcache.max_accelerated_files  = 10000
opcache.revalidate_freq        = 0
opcache.validate_timestamps    = 1    ; set to 0 in production for max speed

Tune the PHP-FPM Worker Pool

The default dynamic pool settings are too conservative for a production site. For a 2 GB VPS running one WordPress site:

sudo nano /etc/php/8.3/fpm/pool.d/www.conf
pm                   = dynamic
pm.max_children      = 20
pm.start_servers     = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 10
pm.max_requests      = 500   ; recycle workers to prevent memory leaks
sudo systemctl restart php8.3-fpm

Step 4 — Configure the Nginx Virtual Host

Create a dedicated site configuration file. This is where Nginx is told to pass PHP requests to the FPM socket:

sudo mkdir -p /var/www/$DOMAIN
sudo chown -R www-data:www-data /var/www/$DOMAIN

sudo nano /etc/nginx/sites-available/$DOMAIN
server {
    listen 80;
    listen [::]:80;
    server_name yourdomain.com www.yourdomain.com;
    root /var/www/yourdomain.com;
    index index.php index.html index.htm;

    # WordPress permalink support
    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    # Pass PHP files to PHP-FPM
    location ~ .php$ {
        include fastcgi_params;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_read_timeout 300;
    }

    # Block access to hidden files (.htaccess, .git, etc.)
    location ~ /. {
        deny all;
        access_log off;
        log_not_found off;
    }

    # Cache static assets aggressively
    location ~* .(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
        expires 30d;
        add_header Cache-Control "public, no-transform";
        access_log off;
    }

    # WordPress login rate limiting (defined in nginx.conf http block)
    location = /wp-login.php {
        limit_req zone=login burst=5 nodelay;
        include fastcgi_params;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";
    add_header X-XSS-Protection "1; mode=block";
    add_header Referrer-Policy "no-referrer-when-downgrade";

    # Disable directory listing
    autoindex off;
}
# Enable the site
sudo ln -s /etc/nginx/sites-available/$DOMAIN /etc/nginx/sites-enabled/

# Remove the default site if it exists
sudo rm -f /etc/nginx/sites-enabled/default

# Test and reload
sudo nginx -t && sudo systemctl reload nginx

Step 5 — Add an SSL Certificate with Let's Encrypt

Certbot handles certificate issuance and Nginx config modification automatically. Your DNS A record must already point to this server before running this step — Let's Encrypt performs an HTTP challenge to verify domain ownership.

sudo apt install certbot python3-certbot-nginx -y

sudo certbot --nginx -d $DOMAIN -d www.$DOMAIN

Certbot will ask for your email address, prompt you to accept the terms of service, and then issue the certificate and rewrite your Nginx config to add HTTPS listeners. After it completes, verify the configuration and test renewal:

# Confirm certificates were issued
sudo certbot certificates

# Test automatic renewal (does not actually renew)
sudo certbot renew --dry-run

# Confirm HTTPS is working
curl -I https://$DOMAIN

Certbot installs a systemd timer that renews certificates automatically. Check it with:

systemctl status certbot.timer

Step 6 — Install WordPress

cd /tmp
wget https://wordpress.org/latest.tar.gz
tar -xzf latest.tar.gz

# Move files into the web root
sudo rsync -av wordpress/ /var/www/$DOMAIN/
sudo chown -R www-data:www-data /var/www/$DOMAIN/
sudo find /var/www/$DOMAIN -type d -exec chmod 755 {} ;
sudo find /var/www/$DOMAIN -type f -exec chmod 644 {} ;

# Create wp-config.php from the sample
sudo cp /var/www/$DOMAIN/wp-config-sample.php /var/www/$DOMAIN/wp-config.php
sudo nano /var/www/$DOMAIN/wp-config.php

Set these four database constants to match what you created in MariaDB earlier:

define( 'DB_NAME',     'myapp_db' );
define( 'DB_USER',     'myapp_user' );
define( 'DB_PASSWORD', 'use-a-strong-password-here' );
define( 'DB_HOST',     'localhost' );

Replace the placeholder salts with fresh values from the WordPress API:

curl -s https://api.wordpress.org/secret-key/1.1/salt/

Paste the output into wp-config.php, replacing the existing define('AUTH_KEY', ...); block. Then lock down the config file:

sudo chmod 600 /var/www/$DOMAIN/wp-config.php

Visit https://yourdomain.com in your browser to complete the WordPress setup wizard.

Troubleshooting

502 Bad Gateway

This means Nginx cannot reach the PHP-FPM socket. The most common causes are FPM not running or a socket path mismatch.

# Check FPM status
systemctl status php8.3-fpm

# Confirm the socket exists
ls -la /run/php/php8.3-fpm.sock

# Compare with the path in your Nginx site


Go up