Skip to main contentSkip to navigation
[email protected]
Client AreaSupport
Hosting Mammoth
HostingMammothYour Data, Our Responsibility
Home
Solutions
Hosting Services
Store
Pricing
About
Blog
API
Contact

Stay Ahead of the Curve

Get the latest insights on cybersecurity, AI innovations, and enterprise data solutions delivered to your inbox.

Hosting Mammoth
HostingMammothEnterprise Solutions

Enterprise-grade data solutions. Hosting, recovery, cybersecurity, and AI-powered services for businesses worldwide.

[email protected]
Sun - Fri, 9:00am - 5:00pm

Services

  • Cloud Hosting
  • Data Recovery
  • Cybersecurity
  • Legal Support
  • MSP Services
  • Web Development
  • AI Services
  • Free Server Migration

Hosting

  • VPS Hosting (NVMe SSD)
  • VDS Hosting (NVMe)
  • Storage VPS (High SSD)
  • GPU Servers
  • Managed Services
  • Cloud Firewall
  • Load Balancer
  • One-Click Apps
  • n8n Hosting
  • Object Storage
  • FAQ

Company

  • Store
  • Pricing
  • About Us
  • Locations
  • Blog
  • Testimonials
  • Contact
  • Affiliate Program
  • White-Label
  • Terms of Service
  • Privacy Policy
  • Browser Cookies
  • SLA

Support

  • Client Area
  • Submit Ticket
  • Knowledge Base
  • Server Status
  • API Documentation

© 2026 Hosting Mammoth. All rights reserved.

Knowledge Base
Getting StartedAccount ManagementVPS HostingGPU ServersStorage VPSCloud FirewallLoad BalancerServer ManagementBilling & PaymentsSupport & TicketsAffiliate ProgramReseller ProgramMarketplace & Appsn8n HostingManaged ServicesServer MigrationAPI & DevelopersSecurityTroubleshootingGlossaryInstall Guides
  1. Home
  2. /
  3. Support
  4. /
  5. Install Guides
  6. /
  7. How To Install Lemp Stack Ubuntu
GUIDEInstall Guides

How to Install LEMP Stack on Ubuntu 24.04 — Nginx + MySQL + PHP

16 min read

How to Install LEMP Stack on Ubuntu 24.04 — Nginx + MySQL + PHP

The LEMP stack is the gold standard for running high-performance PHP applications in production. If you are hosting WordPress, Laravel, Magento, Drupal, or any custom PHP app, a properly tuned LEMP stack on Ubuntu 24.04 LTS will outperform the classic LAMP stack on virtually every real-world workload — especially under concurrency.

In this guide you will install the full LEMP stack on Ubuntu 24.04 from a clean VPS, configure Nginx to pass PHP requests to PHP-FPM 8.3 via a Unix socket, harden MySQL, enable HTTPS with Let's Encrypt, and tune the stack for production traffic. Total hands-on time is about 25 minutes.

What Is the LEMP Stack?

LEMP is an open-source software bundle used to serve dynamic websites and web applications:

  • L — Linux (Ubuntu 24.04 LTS in this guide)
  • E — Nginx (pronounced "Engine-X", hence the "E")
  • M — MySQL (or MariaDB as a drop-in replacement)
  • P — PHP (specifically PHP-FPM, the FastCGI Process Manager)
Together these four components form a complete platform: Linux provides the OS and kernel, Nginx handles HTTP(S) requests and static files, PHP-FPM executes PHP code out-of-process, and MySQL stores persistent data. Nginx communicates with PHP-FPM over a fast local socket, while MySQL is reached via TCP on localhost.

LEMP vs LAMP — Why Nginx Instead of Apache?

The only real difference between LAMP and LEMP is the web server. Both can run the same PHP apps, but they handle traffic very differently.

Apache (LAMP) uses a process-per-request or thread-per-request model (prefork/worker/event MPM). Each incoming request consumes a worker slot for its full duration, including slow clients and keep-alive idle time. When concurrency spikes, Apache hits MaxRequestWorkers and starts queuing — or, worse, starts swapping when each worker holds a full PHP interpreter in memory via mod_php.

Nginx (LEMP) uses an event-driven, asynchronous architecture. A handful of worker processes (typically one per CPU core) handle thousands of concurrent connections each via epoll. Nginx never embeds PHP — it hands PHP work off to PHP-FPM over a socket and goes straight back to handling other connections. The result:

  • Higher concurrency: Nginx routinely handles 10,000+ concurrent connections per worker with a flat memory footprint, while Apache's memory scales linearly with active workers.
  • Better static file performance: Nginx serves static assets (images, CSS, JS) 2-3x faster than Apache and uses sendfile() for zero-copy delivery.
  • Lower RAM usage: On a 2 GB VPS, Nginx + PHP-FPM typically uses 30-40% less RAM than Apache + mod_php at the same request rate.
  • Cleaner separation: PHP-FPM pools run as a dedicated user, can be tuned independently per site, and can be restarted without touching the web server.
  • Native reverse proxy: Nginx is also a first-class reverse proxy, load balancer, and HTTP cache, which makes it trivial to put in front of Node.js, Python, or Go services later.
For a modern PHP stack, LEMP is almost always the right choice.

Prerequisites

Before you start you need:

  • A fresh Ubuntu 24.04 LTS VPS with at least 1 GB RAM (2 GB recommended for production).
  • Root or sudo access via SSH.
  • A domain name with an A record pointing to your server's IPv4 address (required for Let's Encrypt SSL).
  • Ports 80 and 443 open in your firewall / provider console.
If you do not yet have a server, CloudCore Starter (2 vCPU, 4 GB RAM, 50 GB NVMe) is the plan we recommend for a single-site LEMP deployment. It gives PHP-FPM enough headroom for about 15-20 concurrent PHP workers and MySQL enough RAM for a reasonable InnoDB buffer pool.

Start by updating the system:

bash
sudo apt update && sudo apt upgrade -y

Configure the UFW firewall:

bash
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw --force enable
sudo ufw status

Nginx Full is a profile that opens both 80 and 443. It is registered automatically when you install Nginx in the next step — if UFW complains that the profile is unknown, run the Nginx install first and come back.

Step 1 — Install Nginx

Install Nginx from Ubuntu's default repositories:

bash
sudo apt install nginx -y

Enable and start the service:

bash
sudo systemctl enable --now nginx
sudo systemctl status nginx

You should see active (running). Verify the default page loads by visiting http://YOUR_SERVER_IP in a browser — you should see the "Welcome to nginx!" page.

Confirm Nginx is listening on port 80:

bash
sudo ss -tlnp | grep nginx

Step 2 — Install MySQL and Secure It

Install the MySQL server package:

bash
sudo apt install mysql-server -y

Enable and start MySQL:

bash
sudo systemctl enable --now mysql

Run the interactive hardening script:

bash
sudo mysql_secure_installation

Answer the prompts as follows:

  • VALIDATE PASSWORD component: Y and choose 2 (STRONG) for production.
  • New root password: Set a long, random password and store it in a password manager.
  • Remove anonymous users: Y
  • Disallow root login remotely: Y
  • Remove test database: Y
  • Reload privilege tables: Y
On Ubuntu 24.04, MySQL's root account uses the auth_socket plugin by default — which means you log in via sudo mysql without a password, not by typing the password you just set. To create an application user for your site:

bash
sudo mysql

Then in the MySQL prompt:

sql
CREATE DATABASE app_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'CHANGE_ME_STRONG_PASSWORD';
GRANT ALL PRIVILEGES ON app_db.* TO 'app_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;

MariaDB Alternative

If you prefer MariaDB — a community-maintained fork of MySQL that is fully wire-compatible — install it instead of mysql-server:

bash
sudo apt install mariadb-server -y
sudo mysql_secure_installation

Everything else in this guide (connection strings, PHP extensions, user creation) works identically. MariaDB tends to have slightly better write performance and a more liberal licensing story. Pick one or the other — do not install both.

Step 3 — Install PHP-FPM 8.3 and Extensions

Ubuntu 24.04 ships with PHP 8.3 in its default repositories, so no PPA is needed:

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

These extensions cover the needs of 95% of real-world PHP apps:

  • php8.3-fpm — the FastCGI Process Manager (the PHP runtime itself)
  • php8.3-mysql — PDO and mysqli drivers for MySQL/MariaDB
  • php8.3-curl — outbound HTTP requests (required by most frameworks)
  • php8.3-gd / php8.3-imagick — image processing (resizing, thumbnails)
  • php8.3-mbstring / php8.3-xml / php8.3-intl — Unicode, XML parsing, i18n
  • php8.3-zip — used by Composer and by WordPress plugin uploads
  • php8.3-bcmath — required by many commerce apps for precise math
  • php8.3-opcache — bytecode caching (huge performance win, enabled by default)

Verify PHP-FPM Is Running on a Socket

PHP-FPM starts automatically after install. Verify it:

bash
sudo systemctl status php8.3-fpm

By default the Ubuntu package configures PHP-FPM to listen on a Unix domain socket at /run/php/php8.3-fpm.sock (not a TCP port). Unix sockets are significantly faster than TCP for same-host communication because they skip the networking stack entirely. Confirm the socket file exists:

bash
ls -la /run/php/php8.3-fpm.sock

You should see something like:

text
srw-rw---- 1 www-data www-data 0 Apr 16 14:22 /run/php/php8.3-fpm.sock

Note the owner is www-data — this matches the default Nginx user, so no permission changes are needed.

Step 4 — Configure Nginx to Pass PHP Requests to PHP-FPM

The glue between Nginx and PHP-FPM is a location ~ \.php$ block that uses fastcgi_pass to hand PHP requests to the socket. Create a dedicated Nginx server block for your site.

Create the web root:

bash
sudo mkdir -p /var/www/example.com
sudo chown -R www-data:www-data /var/www/example.com

Create the server block:

bash
sudo nano /etc/nginx/sites-available/example.com

Paste the following complete configuration, replacing example.com with your domain:

nginx
server {
    listen 80;
    listen [::]:80;

server_name example.com www.example.com; root /var/www/example.com;

index index.php index.html index.htm;

client_max_body_size 64M;

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

# Main location block — try static files, then fall back to index.php location / { try_files $uri $uri/ /index.php?$query_string; }

# Hand PHP requests to PHP-FPM over the Unix socket location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.3-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_read_timeout 300; fastcgi_buffers 16 16k; fastcgi_buffer_size 32k; }

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

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

Enable the site and disable the default:

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

Test the configuration and reload:

bash
sudo nginx -t
sudo systemctl reload nginx

Key Directives Explained

  • fastcgi_pass unix:/run/php/php8.3-fpm.sock; — this is the line that makes LEMP work. Nginx opens the socket and forwards the request to PHP-FPM using the FastCGI protocol.
  • include snippets/fastcgi-php.conf; — Ubuntu ships a helper file at /etc/nginx/snippets/fastcgi-php.conf that sets all the standard FastCGI params (SCRIPT_NAME, REQUEST_URI, security: try_files $fastcgi_script_name =404, etc.). Always include it.
  • try_files $uri $uri/ /index.php?$query_string; — required by front-controller frameworks like Laravel, Symfony, and WordPress. Nginx first tries the literal file, then a directory, then falls through to index.php with the query string preserved.
  • client_max_body_size 64M; — default is 1 MB, which is too small for media uploads. See the tuning section for matching PHP settings.

Step 5 — Test PHP with phpinfo

Create a phpinfo test page:

bash
sudo tee /var/www/example.com/info.php > /dev/null <<'EOF'
<?php
phpinfo();
EOF

Visit http://example.com/info.php in your browser. You should see the standard PHP info page showing:

  • Server API: FPM/FastCGI
  • Loaded Configuration File: /etc/php/8.3/fpm/php.ini
  • A mysqli and PDO section confirming the MySQL driver is loaded
Important: Delete this file immediately after testing — it exposes a lot of information about your server:

bash
sudo rm /var/www/example.com/info.php

Step 6 — Enable HTTPS with Let's Encrypt

Install Certbot and the Nginx plugin:

bash
sudo apt install certbot python3-certbot-nginx -y

Request and install a certificate (Certbot will automatically edit your server block to add TLS and a redirect from HTTP to HTTPS):

bash
sudo certbot --nginx -d example.com -d www.example.com

Answer the prompts:

  • Enter your email for renewal notices.
  • Agree to the Terms of Service.
  • Choose 2 to redirect all HTTP traffic to HTTPS.
Certbot installs a systemd timer that auto-renews the certificate twice a day. Verify:

bash
sudo systemctl list-timers | grep certbot
sudo certbot renew --dry-run

Production Tuning

Default packages are tuned for developer convenience, not production traffic. Spend 10 minutes on these changes before you go live.

PHP-FPM Pool Tuning

The PHP-FPM pool config lives at /etc/php/8.3/fpm/pool.d/www.conf. Open it:

bash
sudo nano /etc/php/8.3/fpm/pool.d/www.conf

Adjust these directives based on your VPS RAM. A typical PHP worker uses 40-80 MB RSS (WordPress with a few plugins is around 60 MB). A safe rule of thumb: pm.max_children = (total_RAM_for_PHP_MB) / (avg_worker_MB).

For a 4 GB VPS with ~2 GB available for PHP after Nginx, MySQL, and the OS:

ini
; Use dynamic process management
pm = dynamic

; Max simultaneous PHP workers pm.max_children = 25

; Workers to launch at startup pm.start_servers = 6

; Keep at least this many idle workers ready pm.min_spare_servers = 4

; Do not keep more than this many idle pm.max_spare_servers = 10

; Recycle each worker after this many requests (prevents memory leaks) pm.max_requests = 500

; Write a slow log for any request over 5s slowlog = /var/log/php8.3-fpm.slow.log request_slowlog_timeout = 5s

; Status endpoint (pair with an Nginx location for monitoring) pm.status_path = /fpm-status ping.path = /fpm-ping

For a 1-2 GB VPS, start with pm.max_children = 8, pm.start_servers = 2, pm.min_spare_servers = 1, pm.max_spare_servers = 3. For 8+ GB, you can scale to 50-80 children.

Also bump PHP's own limits in /etc/php/8.3/fpm/php.ini:

ini
memory_limit = 256M
upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 120
max_input_time = 120
date.timezone = UTC

Keep upload_max_filesize and post_max_size in sync with Nginx's client_max_body_size, or uploads will fail silently at the web server layer before PHP ever sees them.

Enable and Tune OPcache

OPcache is installed and enabled by default, but the defaults are conservative. Edit /etc/php/8.3/fpm/php.ini and set:

ini
opcache.enable = 1
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 1
opcache.revalidate_freq = 2
opcache.save_comments = 1
opcache.jit_buffer_size = 128M
opcache.jit = tracing

OPcache typically gives PHP apps a 2-3x throughput improvement by caching compiled bytecode. PHP 8.3's JIT adds another 10-20% on CPU-bound workloads. On production servers where code does not change, set opcache.validate_timestamps = 0 and manually reload PHP-FPM on deploys for maximum performance.

Apply all changes:

bash
sudo systemctl reload php8.3-fpm

Nginx Worker Tuning

Edit /etc/nginx/nginx.conf:

nginx
worker_processes auto;
worker_rlimit_nofile 65535;

events { worker_connections 4096; multi_accept on; use epoll; }

http { sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; server_tokens off;

gzip on; gzip_vary on; gzip_proxied any; gzip_comp_level 6; gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss application/atom+xml image/svg+xml; # ... rest of config }

Test and reload:

bash
sudo nginx -t && sudo systemctl reload nginx

MySQL / InnoDB Tuning

For a VPS with 4 GB RAM, add to /etc/mysql/mysql.conf.d/mysqld.cnf under [mysqld]:

ini
innodb_buffer_pool_size = 1G
innodb_log_file_size = 256M
innodb_flush_log_at_trx_commit = 2
innodb_flush_method = O_DIRECT
max_connections = 100

The buffer pool should be roughly 50-70% of total RAM on a dedicated DB host, or 25-40% on a shared LEMP box. Restart MySQL after changes:

bash
sudo systemctl restart mysql

Security Hardening

  • Server tokens off: Already set above — hides Nginx version in headers and error pages.
  • Block hidden files: The server block denies /\. — make sure .env, .git, and .htaccess cannot be fetched.
  • Fail2ban: sudo apt install fail2ban and enable the nginx-http-auth, nginx-botsearch, and sshd jails.
  • Unattended security upgrades: sudo dpkg-reconfigure unattended-upgrades.
  • PHP expose_php = Off in php.ini — hides the X-Powered-By: PHP/8.3.x header.
  • Disable dangerous PHP functions you do not use: disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_multi_exec,parse_ini_file,show_source (skip this if your app legitimately needs exec/shell_exec, e.g. Laravel queues that shell out).
  • MySQL bind: Ensure bind-address = 127.0.0.1 in mysqld.cnf so the database is never exposed to the public internet.

Backups

At minimum, back up the database and the web root daily. A simple cron:

bash
sudo mkdir -p /var/backups/lemp
sudo tee /usr/local/bin/lemp-backup.sh > /dev/null <<'EOF'
#!/bin/bash
set -e
DATE=$(date +%F)
mysqldump --single-transaction --all-databases | gzip > /var/backups/lemp/db-$DATE.sql.gz
tar -czf /var/backups/lemp/www-$DATE.tar.gz /var/www
find /var/backups/lemp -type f -mtime +14 -delete
EOF
sudo chmod +x /usr/local/bin/lemp-backup.sh
echo "0 3   * root /usr/local/bin/lemp-backup.sh" | sudo tee /etc/cron.d/lemp-backup

For production, push the backup directory to off-site storage (S3, Backblaze B2, or a second VPS via rsync).

Troubleshooting

502 Bad Gateway

Almost always means Nginx cannot reach PHP-FPM. Check in order:

  • Is PHP-FPM running? sudo systemctl status php8.3-fpm
  • Does the socket exist? ls -la /run/php/php8.3-fpm.sock
  • Does the socket path in your Nginx server block match the PHP version? (php8.3-fpm.sock, not php8.2-fpm.sock).
  • Check the PHP-FPM error log: sudo tail -50 /var/log/php8.3-fpm.log
  • Check the Nginx error log: sudo tail -50 /var/log/nginx/example.com.error.log — the exact failed upstream will be logged.
  • If you see connect() to unix:/run/php/php8.3-fpm.sock failed (13: Permission denied), see the socket permission fix below.
  • PHP Files Download Instead of Rendering

    This happens when Nginx has no location ~ \.php$ block and falls through to serving the raw file. Confirm your server block includes the fastcgi_pass block and sudo nginx -t && sudo systemctl reload nginx.

    Socket Permission Denied

    If Nginx runs as a different user than www-data (unusual on Ubuntu, but possible in custom builds), edit /etc/php/8.3/fpm/pool.d/www.conf:

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

    Then sudo systemctl restart php8.3-fpm. Confirm the Nginx user with grep '^user' /etc/nginx/nginx.conf — it should be www-data.

    White Screen / Blank Page

    Turn on error display temporarily in /etc/php/8.3/fpm/php.ini:

    ini
    display_errors = On
    display_startup_errors = On
    error_reporting = E_ALL

    Reload PHP-FPM and reload the page. For production, flip these back off and rely on error_log instead.

    Uploads Fail at ~1 MB

    You forgot to match client_max_body_size (Nginx), upload_max_filesize (PHP), and post_max_size (PHP). All three must be at least as large as your biggest expected upload.

    "Connection refused" from PHP to MySQL

    Confirm MySQL is listening on localhost: sudo ss -tlnp | grep mysql. Confirm the app user exists and has the right password: mysql -u app_user -p -h 127.0.0.1.

    FAQ

    Do I need PHP 8.3 specifically? PHP 8.3 is the current stable branch on Ubuntu 24.04 and has the best JIT and performance characteristics. PHP 8.2 works identically — just swap 8.3 for 8.2 everywhere. Avoid PHP 7.x, which is EOL.

    Can I run multiple PHP versions? Yes. Install additional packages (e.g. php8.2-fpm) — each version gets its own socket at /run/php/php8.2-fpm.sock. Point different Nginx server blocks at different sockets.

    Should I use TCP or Unix socket for PHP-FPM? Unix socket on the same host — it is 20-30% faster and avoids ephemeral port exhaustion. Use TCP only when PHP-FPM runs on a different server than Nginx.

    How many concurrent users can a LEMP VPS handle? Depends on the app. A 4 GB VPS running WordPress with OPcache and a page cache plugin can comfortably serve 200-500 concurrent users. Without caching, expect 20-40.

    Nginx vs OpenLiteSpeed vs Caddy? Nginx has the largest ecosystem and most mature documentation. OpenLiteSpeed has built-in LSCache for WordPress (faster out-of-the-box for WP specifically). Caddy has automatic HTTPS. For a general-purpose PHP stack, Nginx is the safest choice.

    Do I need to tune MySQL if I am only running one small site? Defaults are usable for a site with under ~50 concurrent users. Tune the InnoDB buffer pool before you hit that scale.

    Next Steps

    Your LEMP stack is production-ready. Common next moves:

    • Install WordPress: Create a DB, download WordPress into /var/www/example.com, and walk through the web installer. Add a page cache plugin (W3 Total Cache, LiteSpeed Cache, or WP Super Cache) for another 5-10x throughput boost.
    • Deploy Laravel: Install Composer, clone your repo, point the web root at /var/www/example.com/public, run composer install --no-dev, set up the .env, and run php artisan migrate. Add Redis (sudo apt install redis-server php8.3-redis) for sessions and queues.
    • Add Redis or Memcached: Both live well alongside LEMP and dramatically speed up session storage, object caching, and rate limiting.
    • Monitoring: Install Netdata (curl https://my-netdata.io/kickstart.sh | sh) for a real-time dashboard covering Nginx, PHP-FPM, and MySQL.
    • Horizontal scaling: When you outgrow one box, move MySQL to a dedicated host, put a load balancer in front of two Nginx nodes, and share sessions via Redis.
    If you want a VPS sized and pre-tuned for this exact stack, the CloudCore Starter plan on VPS-Server.host is the entry point — it has enough RAM for a healthy PHP-FPM pool, a 256 MB OPcache, and a 1 GB InnoDB buffer pool with room to spare for OS cache and Nginx.

    Was this article helpful?

    ← Back to Install GuidesBrowse all categories →

    Still have questions?

    Contact Support →Submit a Ticket