LEMP — Linux, Nginx ("engine-x"), MariaDB/MySQL, PHP — is the stack behind most PHP hosting. This is a clean install on Ubuntu 24.04 that ends with a site serving PHP over Nginx with a database ready. About twenty minutes, assuming a server that has been through the hardening checklist.
1. Nginx
sudo apt update
sudo apt install nginx -y
sudo systemctl enable --now nginxOpen http://your-server-ip in a browser: the Nginx welcome page confirms it is serving. If you use ufw:
sudo ufw allow 'Nginx Full'2. MariaDB
sudo apt install mariadb-server -y
sudo systemctl enable --now mariadb
sudo mysql_secure_installationAnswer the prompts: set a root password (or keep unix socket auth — fine for a single server), remove anonymous users, disallow remote root, remove the test database, reload privileges. All yes.
Create a database and a user for the site:
sudo mysqlCREATE DATABASE example_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'example_user'@'localhost' IDENTIFIED BY 'a-long-random-password';
GRANT ALL PRIVILEGES ON example_db.* TO 'example_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;One user per site, with rights on one database only. Never let the application use root.
3. PHP-FPM
sudo apt install php8.3-fpm php8.3-mysql php8.3-curl php8.3-gd php8.3-mbstring \
php8.3-xml php8.3-zip php8.3-intl php8.3-bcmath php8.3-opcache -y
php -vThat extension list covers WordPress, Laravel and most PHP applications. PHP-FPM runs as a pool of worker processes listening on a Unix socket, /run/php/php8.3-fpm.sock; Nginx hands PHP requests to it.
Three settings in /etc/php/8.3/fpm/php.ini worth changing from the defaults:
memory_limit = 256M
upload_max_filesize = 64M
post_max_size = 64MAnd enable OPcache properly in the same file:
opcache.enable=1
opcache.memory_consumption=128
opcache.max_accelerated_files=10000sudo systemctl restart php8.3-fpmTuning the pool sizes for real traffic is a separate topic: configuring PHP-FPM pools.
4. A server block for the site
Create the web root and a config:
sudo mkdir -p /var/www/example.com
sudo chown -R www-data:www-data /var/www/example.com
sudo nano /etc/nginx/sites-available/example.comserver {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/example.com;
index index.php index.html;
access_log /var/log/nginx/example.com.access.log;
error_log /var/log/nginx/example.com.error.log;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
location ~ /\.(?!well-known) {
deny all;
}
location ~* \.(css|js|jpg|jpeg|png|gif|svg|webp|ico|woff2?)$ {
expires 30d;
access_log off;
}
}The try_files line is what makes WordPress permalinks and Laravel routes work; the \. block stops .git and .env being served; the last block caches static files in the browser.
Enable it, remove the default, test and reload:
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx5. Test PHP
echo '<?php phpinfo();' | sudo tee /var/www/example.com/info.phpLoad http://example.com/info.php (or the IP, if DNS is not pointed yet — add server_name _; temporarily). A page of PHP configuration means the whole chain works. Delete the file afterwards; it reveals more than you want public:
sudo rm /var/www/example.com/info.php6. HTTPS
With DNS pointed at the server:
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d example.com -d www.example.comCertbot edits the server block for you, adds the redirect, and installs a renewal timer. Check it with sudo certbot renew --dry-run.
What you have
Nginx serving static files and passing PHP to a FastCGI pool, a database with a least-privilege user, OPcache on, HTTPS with auto-renewal. That is a production stack; from here it is application deployment and tuning.
If you would rather not maintain it: this exact stack, tuned for the workload and patched by us, is what a VPSPioneer managed VPS ships with.