Hardware Recommendations for a Basic LEMP Server
Before installing a single package, you need appropriate hardware. Below are the recommended minimums and comfortable targets for a personal or small-business LEMP server running Ubuntu 24.04 LTS.
CPU
Minimum: 2 cores Recommended: 4+ coresAny modern x86-64 processor. Intel Core i3/i5 or AMD Ryzen 3/5 are excellent choices. Nginx is particularly well-suited to ARM64 including Raspberry Pi.
RAM
Minimum: 1 GB Recommended: 2–4 GBNginx uses significantly less RAM than Apache. A LEMP stack can run comfortably on 1 GB — making it ideal for low-spec hardware and VPS instances.
Storage
Minimum: 20 GB SSD Recommended: 60–120 GB SSDSSDs dramatically improve MariaDB I/O performance. Avoid spinning HDDs for the OS drive if possible.
Network
Minimum: 100 Mbps NIC Recommended: 1 Gbps NICA wired Ethernet connection is strongly preferred over Wi-Fi for server stability and consistent latency.
Power Supply
UPS recommendedAn Uninterruptible Power Supply (UPS) protects against sudden power loss that can corrupt MariaDB databases and the filesystem.
Popular Options
PC / Mini-PC / NUC / PiIntel NUC, Beelink Mini-PC, or any repurposed desktop PC are all practical. Raspberry Pi 5 (4 GB+) is recommended for ARM-based builds — Nginx's low footprint makes it an excellent match for the Pi. The Pi 4 can work but requires an extra bootloader step for USB boot.
Installing Ubuntu 24.04 Server from Scratch
This section covers the complete bare-metal installation of Ubuntu 24.04 LTS Server for both standard x86-64 PCs and ARM64 Raspberry Pi hardware. Follow the path that matches your hardware.
Part A — x86-64 PC Installation
What You Will Need
- A USB flash drive, 4 GB or larger
- The Ubuntu 24.04 LTS Server ISO — download from ubuntu.com/download/server
- A tool to write the ISO to USB: Balena Etcher (Windows/Mac/Linux) or Rufus (Windows only)
- The target PC connected to your router via Ethernet
- A keyboard and monitor connected to the target PC for installation
Step 1 — Write the ISO to USB
- Download the Ubuntu 24.04 LTS Server ISO from ubuntu.com. The filename will look like
ubuntu-24.04-live-server-amd64.iso. - Open Balena Etcher (or Rufus). Select the ISO file, select your USB drive as the target, and click Flash. This will erase everything on the USB drive.
- When flashing is complete, safely eject the USB drive.
Step 2 — Boot From USB
- Insert the USB drive into the target PC and power it on.
- Enter the BIOS/UEFI boot menu — typically by pressing
F2,F10,F12, orDeleteimmediately on startup. - Select your USB drive from the boot device list and press Enter.
- The Ubuntu installer will load. Select Try or Install Ubuntu Server and press Enter.
Step 3 — Walk Through the Ubuntu Server Installer
- Language — Select your language and press Enter.
- Keyboard Layout — Select your keyboard layout.
- Installation Type — Select Ubuntu Server (not the minimized version).
- Network — Leave DHCP as-is for now — you will set the static IP after installation (Section 3). Press Done.
- Proxy — Leave blank. Press Done.
- Ubuntu Archive Mirror — Accept the default. Press Done when the test passes.
- Storage Configuration — Select Use an entire disk. Press Done, then confirm the destructive action warning.
- Profile Setup — Enter your name, a server hostname (e.g.,
lempserver), a username, and a strong password. Write the password down. - Ubuntu Pro — Select Skip for now.
- SSH Setup — Select Install OpenSSH server and press Space to check it, then Enter.
- Featured Server Snaps — Do not select anything. Press Done.
- Installation — Wait 5–15 minutes for the install to complete.
- When complete, select Reboot Now. Remove the USB drive when prompted.
Step 4 — First Login and Update
sudo apt update && sudo apt upgrade -y sudo reboot
Part B — Raspberry Pi ARM64 Installation
What You Will Need
- Raspberry Pi 4 or Pi 5 (4 GB RAM minimum recommended)
- Storage — choose one:
- microSD card — 16 GB or larger, Class 10 or UHS-I. Works on all Pi models.
- USB 3.0 SSD — faster and more reliable. Only the Raspberry Pi 5 and newer support native USB boot out of the box. On a Pi 4, you must first update the bootloader EEPROM firmware.
- The Raspberry Pi Imager tool — free from raspberrypi.com/software
- The Pi connected to your router via Ethernet
Step 1 — Write the Image Using Raspberry Pi Imager
- Download and install Raspberry Pi Imager on your desktop or laptop.
- Open Raspberry Pi Imager. Under Choose Device, select your Pi model.
- Under Choose OS, navigate to: Other general-purpose OS → Ubuntu → Ubuntu Server 24.04 LTS (64-bit).
- Under Choose Storage, select your microSD card.
- Click Next → Edit Settings and configure:
- Hostname:
lempserver - Username and Password — create your admin user
- Enable SSH — check this box, select Use password authentication
- Leave Wi-Fi blank — you will use Ethernet
- Hostname:
- Click Save, then Yes twice to confirm. This erases the card.
- When writing is complete, eject the microSD card safely.
Step 2 — Boot and Connect
# Find the Pi's IP from your router's DHCP client list ssh [email protected] # Accept the SSH fingerprint prompt: yes # Then update immediately sudo apt update && sudo apt upgrade -y sudo reboot
Setting Up a Static IP Address & SSH
A server must have a static local IP address so your router's port forwarding rules always point to the right machine. Ubuntu 24.04 uses Netplan for network configuration.
Step 1 — Identify Your Network Interface
ip link show # Look for eth0, ens3, enp3s0, or similar — ignore lo ip route show # The "default via" line shows your gateway — typically 192.168.1.1
Step 2 — Back Up and Edit Netplan
sudo cp /etc/netplan/00-installer-config.yaml \ /etc/netplan/00-installer-config.yaml.bak
network: version: 2 renderer: networkd ethernets: eth0: # Replace with your interface name dhcp4: no addresses: - 192.168.1.50/24 routes: - to: default via: 192.168.1.1 nameservers: addresses: - 1.1.1.1 - 8.8.8.8
Step 3 — Apply and Verify
sudo netplan try sudo netplan apply ip addr show eth0 ping -c 4 google.com
Raspberry Pi — Disable cloud-init Networking First
sudo bash -c \ 'echo "network: {config: disabled}" > \ /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg' sudo netplan apply sudo reboot
Connect via SSH — Ditch the Server Keyboard
With a confirmed static IP you can now SSH in from your Windows or Linux desktop and copy every command from this webpage and paste it directly into your terminal.
sudo apt install -y openssh-server sudo systemctl enable ssh sudo systemctl start ssh
# Replace yourusername and IP with your actual values ssh [email protected] # Type yes to accept fingerprint on first connection # PowerShell paste: right-click | Linux paste: Ctrl+Shift+V | macOS: Cmd+V
If you prefer a graphical SSH client on Windows, PuTTY (free at putty.org) is the most popular option.
Router Setup & Port Forwarding
For your server to be reachable from the internet, your router must forward incoming traffic on the correct ports to your server's local IP address.
Rule Protocol Ext Port Int Port Destination ───────────────────────────────────────────────────── HTTP Web TCP 80 80 192.168.1.50 HTTPS Web TCP 443 443 192.168.1.50
fail2ban. If you only need SSH from within your home network, do not forward port 22 at all.
Step-by-Step Port Forwarding
- Log into your router admin panel — typically at
http://192.168.1.1orhttp://192.168.0.1. - Navigate to Port Forwarding, Virtual Servers, or NAT.
- Create a rule for HTTP: TCP, external port 80, internal port 80, destination = your server's static IP.
- Create a rule for HTTPS: TCP, external port 443, internal port 443, same destination IP.
- Save and apply. Some routers require a reboot.
- Verify your public IP at whatismyip.com — this is the IP your domain name will point to.
Full LEMP Stack Installation
With networking configured, it's time to install the full stack — Nginx, MariaDB, and PHP-FPM along with all the modules needed to make them work together correctly.
mod_php, Nginx does not embed PHP inside the web server process. Instead, Nginx passes PHP requests to a separate PHP-FPM (FastCGI Process Manager) service over a Unix socket. This means PHP-FPM must be running as its own service, and your Nginx server block configuration must tell Nginx where to find it. This is covered in detail in Section 7.
Step 1 — Update the System
sudo apt update && sudo apt upgrade -y
Step 2 — Install Nginx
sudo apt install -y nginx sudo systemctl enable nginx sudo systemctl start nginx sudo systemctl status nginx
http://192.168.1.50). You should see the Welcome to nginx! default page. This confirms Nginx is running and reachable.
Step 3 — Configure UFW Firewall
UFW must be configured before you enable it — enabling it with no rules will lock you out of SSH immediately.
# Allow HTTP — port 80 sudo ufw allow 80/tcp # Allow HTTPS — port 443 sudo ufw allow 443/tcp # Allow SSH — port 22 (do not skip this) sudo ufw allow 22/tcp # Alternatively use the Nginx profile shortcut for 80 and 443: # sudo ufw allow 'Nginx Full' # Now enable the firewall — rules are already in place sudo ufw enable # Verify sudo ufw status verbose
Status: active Default: deny (incoming), allow (outgoing) To Action From -- ------ ---- 80/tcp ALLOW IN Anywhere 443/tcp ALLOW IN Anywhere 22/tcp ALLOW IN Anywhere
- SSH on custom port:
sudo ufw allow 2222/tcp - App on port 8080:
sudo ufw allow 8080/tcp - MariaDB from LAN only:
sudo ufw allow from 192.168.1.0/24 to any port 3306
Step 4 — Install MariaDB
mysqli and pdo_mysql extensions.
sudo apt install -y mariadb-server sudo systemctl enable mariadb sudo systemctl start mariadb sudo systemctl status mariadb
Step 5 — Secure MariaDB
sudo mysql_secure_installation
- Switch to unix_socket authentication? —
N(keep password auth) - Set root password? —
Y— choose a strong password and write it down - Remove anonymous users? —
Y - Disallow root login remotely? —
Y - Remove test database? —
Y - Reload privilege tables now? —
Y
Step 6 — Create a MariaDB Database and User
-- Log into MariaDB as root sudo mariadb -u root -p CREATE DATABASE mywebsite_db; CREATE USER 'webuser'@'localhost' IDENTIFIED BY 'YourStrongPassword123!'; GRANT ALL PRIVILEGES ON mywebsite_db.* TO 'webuser'@'localhost'; FLUSH PRIVILEGES; SELECT user, host FROM mysql.user; EXIT;
Step 7 — Verify MariaDB Login
mariadb -u webuser -p SHOW DATABASES; # You should see mywebsite_db listed EXIT;
Step 8 — Install PHP-FPM and All Recommended Modules
sudo apt install -y \ php8.3-fpm \ php8.3-mysql \ php8.3-cli \ php8.3-common \ php8.3-curl \ php8.3-gd \ php8.3-intl \ php8.3-mbstring \ php8.3-xml \ php8.3-zip \ php8.3-bcmath \ php8.3-opcache # Enable and start PHP-FPM sudo systemctl enable php8.3-fpm sudo systemctl start php8.3-fpm sudo systemctl status php8.3-fpm # Verify PHP version php --version
/run/php/php8.3-fpm.sock. You will reference this socket path in every Nginx server block that needs to process PHP files. This is configured in Section 7.
Step 9 — Set Correct File Ownership
# Nginx runs as www-data — set ownership of the web root sudo chown -R www-data:www-data /var/www/html # Add your own user to the www-data group sudo usermod -aG www-data $USER # Directories: 755 | Files: 644 sudo find /var/www/html -type d -exec chmod 755 {} \; sudo find /var/www/html -type f -exec chmod 644 {} \; newgrp www-data
Verifying PHP with phpinfo
The phpinfo() function produces a detailed page confirming your LEMP stack is wired together correctly — especially that Nginx is handing PHP requests to PHP-FPM successfully.
phpinfo.php file exposes detailed server configuration to anyone who visits it. Create it, verify your install, then delete it immediately. Never leave this file on a production server.
Step 1 — Create the File
sudo bash -c \ 'echo "<?php phpinfo(); ?>" > /var/www/html/phpinfo.php' sudo chown www-data:www-data /var/www/html/phpinfo.php sudo chmod 644 /var/www/html/phpinfo.php
http://your-server-ip/phpinfo.php right now and see a blank page or a download prompt instead of the PHP info page, it means Nginx is not yet passing .php requests to PHP-FPM. Complete Section 7 first to configure your server block — then come back and test this page. Once Section 7 is done the test will work correctly.
Step 2 — View the Page in a Browser
After completing Section 7, navigate to: http://your-server-ip/phpinfo.php
Verify the following on the PHP info page:
- PHP Version — should show 8.3.x at the top
- Server API — should show
FPM/FastCGI(not Apache Handler or CLI) - mysqli — confirms PHP can talk to MariaDB
- PDO — should show
pdo_mysqlas a driver - Loaded Configuration File — should point to
/etc/php/8.3/fpm/php.ini
Step 3 — Delete the File After Verification
sudo rm /var/www/html/phpinfo.php
Creating Your First Website (HTTP on Port 80)
Nginx uses server blocks — the equivalent of Apache's virtual hosts — to serve websites. Each site gets its own config file in /etc/nginx/sites-available/ and is activated by symlinking it into /etc/nginx/sites-enabled/. For this example, assume your domain is example.com. Replace it with your actual domain throughout.
Step 1 — Create the Website Document Root
sudo mkdir -p /var/www/example.com/public_html sudo chown -R www-data:www-data /var/www/example.com sudo chmod -R 755 /var/www/example.com
Step 2 — Create an Index PHP Page
<?php echo '<h1>Success! Your LEMP Server is Running.</h1>'; echo '<p>Nginx, PHP-FPM, and MariaDB are installed.</p>'; ?>
sudo nano /var/www/example.com/public_html/index.php # Paste the PHP above, then Ctrl+X, Y, Enter to save
Step 3 — Create the Nginx Server Block Configuration
fastcgi_pass directive is what connects Nginx to PHP-FPM. It tells Nginx to forward all .php requests to the PHP-FPM Unix socket. Without this block, Nginx will either serve your PHP files as plain text or return a blank page. Make sure the socket path matches your PHP version — for PHP 8.3 it is /run/php/php8.3-fpm.sock.
server { listen 80; listen [::]:80; server_name example.com www.example.com; root /var/www/example.com/public_html; index index.php index.html index.htm; access_log /var/log/nginx/example.com-access.log; error_log /var/log/nginx/example.com-error.log; location / { try_files $uri $uri/ =404; } # Pass all .php requests to PHP-FPM location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.3-fpm.sock; } # Block access to .htaccess files location ~ /\.ht { deny all; } }
Step 4 — Enable the Site and Test
# Create the symlink to enable the site sudo ln -s /etc/nginx/sites-available/example.com \ /etc/nginx/sites-enabled/ # Remove the default Nginx placeholder site sudo rm /etc/nginx/sites-enabled/default # Test the Nginx config for syntax errors sudo nginx -t # If output shows "test is successful", reload Nginx sudo systemctl reload nginx
sudo nginx -t before reloading. It checks every config file for syntax errors without affecting the running server. If it reports an error, fix it before reloading — a bad config will prevent Nginx from restarting.
http://example.com (or your server's IP). You should see your "Success! Your LEMP Server is Running." page rendered by PHP — not the default Nginx page. Now go back and complete the phpinfo test from Section 6.
Installing Let's Encrypt with Certbot (HTTPS / Port 443)
Let's Encrypt provides free, trusted SSL/TLS certificates. Before proceeding, your domain's DNS A record must already point to your server's public IP address, and port 80 must be reachable from the internet.
Step 1 — Install Certbot for Nginx
sudo apt install -y certbot python3-certbot-nginx certbot --version
Step 2 — Obtain a Certificate for Your Domain
sudo certbot --nginx -d example.com -d www.example.com
- Email address — Enter a valid email for expiry notifications
- Terms of Service — Enter
Ato agree - Redirect HTTP to HTTPS? — Choose
2to enable automatic redirect (recommended)
When Certbot finishes, it automatically modifies your Nginx server block to add SSL configuration and sets up HTTP → HTTPS redirection.
Step 3 — Verify the Updated Server Block
sudo cat /etc/nginx/sites-available/example.com
Certbot will have added SSL certificate paths and an HTTPS server block. It looks similar to this:
server { listen 80; server_name example.com www.example.com; # Certbot adds redirect to HTTPS: return 301 https://$host$request_uri; } server { listen 443 ssl; listen [::]:443 ssl; server_name example.com www.example.com; root /var/www/example.com/public_html; index index.php index.html; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; location / { try_files $uri $uri/ =404; } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.3-fpm.sock; } location ~ /\.ht { deny all; } }
Step 4 — Test Automatic Certificate Renewal
sudo certbot renew --dry-run sudo systemctl status certbot.timer sudo certbot certificates
Nginx Server Blocks — Multiple Domains on One Server
Nginx can serve any number of distinct websites from a single server using server blocks. Each domain gets its own config file, its own document root, and its own SSL certificate. In this example, we'll add a second domain: secondsite.com.
Step 1 — Create the Document Root
sudo mkdir -p /var/www/secondsite.com/public_html sudo chown -R www-data:www-data /var/www/secondsite.com sudo chmod -R 755 /var/www/secondsite.com sudo bash -c 'cat > /var/www/secondsite.com/public_html/index.php << EOF <?php echo "<h1>Welcome to Second Site!</h1>"; ?> EOF'
Step 2 — Create the Server Block Config
server { listen 80; listen [::]:80; server_name secondsite.com www.secondsite.com; root /var/www/secondsite.com/public_html; index index.php index.html; access_log /var/log/nginx/secondsite.com-access.log; error_log /var/log/nginx/secondsite.com-error.log; location / { try_files $uri $uri/ =404; } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.3-fpm.sock; } location ~ /\.ht { deny all; } }
Step 3 — Enable the Site
sudo ln -s /etc/nginx/sites-available/secondsite.com \ /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx
Step 4 — Get SSL Certificate for the Second Domain
sudo certbot --nginx -d secondsite.com -d www.secondsite.com
sudo ln -s /etc/nginx/sites-available/name /etc/nginx/sites-enabled/ — Enable a sitesudo rm /etc/nginx/sites-enabled/name — Disable a site (removes symlink only)ls /etc/nginx/sites-enabled/ — List all active sitessudo nginx -T — Dump the full compiled Nginx config for debugging
Advanced: HTTPS Reverse Proxy Server Blocks
A reverse proxy server block allows your LEMP server to act as a secure front-door for other services running on your local network. External visitors connect to your LEMP server over HTTPS, and Nginx silently forwards their requests to a different machine or port inside your network — passing back the response as if it came from the LEMP server itself.
This is extremely useful for exposing internal services (media servers, dashboards, apps) through a single public IP with proper SSL certificates. Nginx is particularly well-suited to this role — it handles reverse proxying with less overhead than Apache.
Prerequisites
- A valid SSL certificate for the subdomain must exist (from Section 8)
- The internal service must be running and reachable on its local IP and port
- DNS A record for the subdomain must already point to your public IP
Step 1 — Obtain a Certificate for Your Subdomain
sudo certbot --nginx -d app.example.com
Step 2 — Create the Reverse Proxy Server Block
In this example, we'll proxy app.example.com to an internal service running at 192.168.1.42 on port 4533. Replace these with your actual subdomain, internal IP, and port.
# HTTP — redirect all traffic to HTTPS server { listen 80; server_name app.example.com; return 301 https://$host$request_uri; } # HTTPS — reverse proxy to internal service server { listen 443 ssl; listen [::]:443 ssl; server_name app.example.com; ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # Security header add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; # Forward all requests to the internal service location / { proxy_pass http://192.168.1.42:4533; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }
Step 3 — Enable and Test
sudo ln -s /etc/nginx/sites-available/app.example.com \ /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx curl -I https://app.example.com
proxy_set_header lines pass important information to the backend service: the original Host header, the real client IP via X-Real-IP, the full forwarded chain via X-Forwarded-For, and whether the original request was HTTP or HTTPS via X-Forwarded-Proto. Many backend applications need these headers to generate correct links and handle authentication properly.
To expose additional internal services, repeat this process — new subdomain DNS record, new certificate, new server block config file. Each subdomain is completely independent.
Setting Up a Second Physical Server as a Subdomain
Now we'll bring a second physical computer online as a separate web server running its own Nginx instance, and expose it to the internet through your LEMP server as a subdomain — for example, server2.example.com.
Overview of the Architecture
Internet User
│
▼ (HTTPS — server2.example.com)
[Your LEMP Server — Public IP] ← Port 443
│
│ (HTTP — internal LAN)
▼
[Second Server — 192.168.1.51] ← Port 80
On the Second Server — Install Nginx
sudo apt update && sudo apt install -y nginx sudo systemctl enable nginx sudo systemctl start nginx sudo mkdir -p /var/www/website2/public_html sudo chown -R www-data:www-data /var/www/website2 sudo chmod -R 755 /var/www/website2 sudo bash -c 'echo "<?php echo \ \"<h1>Website 2 — Running on the Second Server</h1>\"; ?>" \ > /var/www/website2/public_html/index.php'
On the Second Server — Server Block Config
server { listen 80; listen [::]:80; server_name server2.example.com; root /var/www/website2/public_html; index index.php index.html; access_log /var/log/nginx/website2-access.log; error_log /var/log/nginx/website2-error.log; location / { try_files $uri $uri/ =404; } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.3-fpm.sock; } location ~ /\.ht { deny all; } }
sudo ln -s /etc/nginx/sites-available/website2 \ /etc/nginx/sites-enabled/ sudo rm /etc/nginx/sites-enabled/default sudo nginx -t sudo systemctl reload nginx
On the DNS — Create the Subdomain Record
Type: A Name: server2 (becomes server2.example.com) Value: YOUR.PUBLIC.IP.ADDRESS TTL: Auto (or 3600)
On the LEMP Server — Reverse Proxy for the Subdomain
sudo certbot --nginx -d server2.example.com
server { listen 80; server_name server2.example.com; return 301 https://$host$request_uri; } server { listen 443 ssl; listen [::]:443 ssl; server_name server2.example.com; ssl_certificate /etc/letsencrypt/live/server2.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/server2.example.com/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; location / { proxy_pass http://192.168.1.51:80; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }
sudo ln -s /etc/nginx/sites-available/server2.example.com \ /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx
https://server2.example.com in a browser. You should see the "Website 2" page with a valid padlock — confirming HTTPS is handled by your LEMP server and transparently proxied to the second physical server on your LAN.