LEMP Stack on Ubuntu 24.04 on Azure User Guide
Overview
The LEMP stack is the nginx counterpart of the classic LAMP stack: Linux, nginx (pronounced "engine x", the E), MariaDB and PHP. Where Apache traditionally embeds a PHP interpreter in the web server process, nginx keeps the two separate: it serves static files itself and hands PHP requests to a dedicated PHP FPM process pool over a unix socket. That event driven model is why LEMP handles large numbers of simultaneous connections on modest hardware, which makes it a popular choice for content management systems, custom PHP sites and API backends.
This cloudimg image wires the whole stack together and hardens it, on a fully patched Ubuntu 24.04 LTS base. nginx 1.24 serves port 80, PHP 8.3 runs as an FPM pool, and MariaDB 10.11 listens only on the loopback interface. Every password is generated uniquely on the first boot of each VM, with no default login anywhere in the image, and an application database is created and ready for your code before you first sign in. phpMyAdmin 5.2.3 is included for browser based database administration. The database and the web document root each live on their own dedicated Azure data disk. Backed by 24/7 cloudimg support.
What is included:
- nginx 1.24 serving port 80, with PHP dispatched to PHP FPM over a unix socket
- PHP 8.3 FPM with the extension set modern applications expect, including mysqli, pdo_mysql, mbstring, gd, intl, bcmath, zip, curl, xml and opcache
- MariaDB 10.11, bound to
127.0.0.1only and never exposed to the network - A ready to use application database and a least privilege database user, created on first boot
- A live
/status.phpstack status page that proves nginx, PHP FPM and MariaDB are all working together - phpMyAdmin 5.2.3 at
/phpmyadminfor browser based database administration - Per VM passwords for the database root account, the phpMyAdmin control user and the application user, recorded in a root only file
- Two dedicated Azure data disks: one for the database at
/var/lib/mysql, one for the document root at/var/www nginx.service,php8.3-fpm.serviceandmariadb.serviceas enabled systemd units- An unauthenticated
/healthzendpoint for Azure Load Balancer health probes - 24/7 cloudimg support
Prerequisites
An active Azure subscription, an SSH key pair, and a VNet plus subnet in the target region. Standard_B2s (2 vCPU / 4 GiB RAM) is a sensible starting point; size up for busier sites or larger databases. NSG inbound: allow 22/tcp from your management network and 80/tcp (plus 443/tcp once you add TLS) for the web tier. The MariaDB server is never exposed: it listens on 127.0.0.1 only, so port 3306 stays off the network and needs no NSG rule.
Step 1 - Deploy from the Azure Marketplace
Sign in to the Azure Portal, choose Create a resource, search the Marketplace for LEMP Stack by cloudimg, and select Create. On Basics pick your subscription, resource group, region and size; under Administrator account choose SSH public key and paste your key; under Inbound port rules allow SSH (22) and HTTP (80). Review the two dedicated data disks on the Disks tab, then Review + create then Create.
Step 2 - Deploy from the Azure CLI
az vm create \
--resource-group <your-rg> \
--name lemp-stack \
--image <marketplace-image-urn> \
--size Standard_B2s \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard
Then open port 80 to the web tier:
az vm open-port --resource-group <your-rg> --name lemp-stack --port 80
Step 3 - Confirm the stack is running
SSH in as azureuser and confirm nginx, PHP FPM and MariaDB are all active, along with the one shot first boot service that generated this VM's credentials.
systemctl is-active nginx php8.3-fpm mariadb lemp-stack-firstboot
active
active
active
active
Check what is listening. nginx is on port 80 on all interfaces; MariaDB is on 127.0.0.1:3306 only, so it is unreachable from the network.
ss -tlnp | grep -E ':80 |:3306 ' | sed 's/ */ /g'
LISTEN 0 80 127.0.0.1:3306 0.0.0.0:* users:(("mariadbd",pid=14319,fd=21))
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=14679,fd=5),("nginx",pid=14678,fd=5),("nginx",pid=14351,fd=5))
LISTEN 0 511 [::]:80 [::]:* users:(("nginx",pid=14679,fd=6),("nginx",pid=14678,fd=6),("nginx",pid=14351,fd=6))

Step 4 - Retrieve this VM's credentials
Every VM generates its own database passwords on first boot and writes them to a root only file. Nothing is shared between VMs and no default login ships in the image. Read the file with sudo:
sudo cat /root/lemp-stack-credentials.txt

The file records three things you will use:
LEMP_MARIADB_ROOT_PASSWORD- sign in to phpMyAdmin as userrootwith this.LEMP_APP_DB_NAME,LEMP_APP_DB_USERandLEMP_APP_DB_PASSWORD- a database that already exists, with a user whose privileges are limited to it. Point your application at these.LEMP_URL,LEMP_STATUS_URLandLEMP_PHPMYADMIN_URL- the stack's entry points, built from the address the VM detected for itself at first boot. If your VM's public IP is assigned after that point, or you are reading this from outside the VNet, substitute the VM's public IP from the Azure Portal in these URLs; the paths are unchanged.
On the box itself you can always reach the database directly over the local unix socket, with no password, because root@localhost is pinned to socket authentication:
sudo mariadb -e 'SELECT VERSION();'
Step 5 - Verify the stack end to end
The health endpoint is served by nginx alone, with no PHP involved, and is what an Azure Load Balancer probe should target:
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/healthz
200
The /status.php page is the real proof. Serving it exercises all three tiers in a single request: nginx accepts it, PHP FPM executes it, and it opens a connection to MariaDB and reads a row back. It prints the LEMP_STATUS_OK marker only when every tier answered.
curl -s http://127.0.0.1/status.php | grep -oE 'LEMP_STATUS_OK|nginx/[0-9.]+|PHP [0-9.]+ via [a-z-]+|MariaDB [0-9][^<]*' | head -5
LEMP_STATUS_OK
nginx/1.24.0
PHP 8.3.6 via fpm-fcgi
MariaDB 10.11.14-MariaDB-0ubuntu0.24.04.1
Open the same page in a browser at http://<public-ip>/status.php and you get the readable version, including the live query result read back from the database:

Browsing to http://<public-ip>/ gives you the welcome page, which links to the status page, the PHP runtime report and phpMyAdmin:

Step 6 - Administer the database from phpMyAdmin
Browse to http://<public-ip>/phpmyadmin/ and sign in as user root with the LEMP_MARIADB_ROOT_PASSWORD from Step 4. phpMyAdmin uses cookie authentication, protected by a blowfish secret that is also unique to this VM.

Once signed in, the left navigation lists every database on the server, including your application database and the phpMyAdmin control database. The SQL tab gives you a full query editor: type a statement, click Go, and the results render below with export, chart and bookmark options.

Step 7 - Use the application database
The application database already exists and its user is limited to it, so you can start creating tables immediately. Read the password from the credentials file and connect over the loopback interface:
PW=$(sudo grep '^LEMP_APP_DB_PASSWORD=' /root/lemp-stack-credentials.txt | cut -d= -f2-); mariadb -h127.0.0.1 -ulempapp -p"$PW" lempapp -e 'SHOW TABLES; SELECT * FROM cloudimg_status;'
Tables_in_lempapp
cloudimg_status
id status checked_at
1 ok 2026-08-06 22:31:31
cloudimg_status is the single row the status page reads. It is safe to drop once your own schema is in place.
Step 8 - Deploy your own PHP application
Put your application in /var/www/html and nginx serves it immediately, with index.php taking precedence over the cloudimg welcome page. Your application's database credentials are already on the VM, in a file readable by the web user but outside the document root, so the password is never web reachable. Any PHP file can pull them in:
sudo tee /var/www/html/hello.php >/dev/null <<'PHP'
<?php
$db = require '/etc/cloudimg/lemp-db.php';
$c = new mysqli($db['host'], $db['user'], $db['pass'], $db['name'], (int) $db['port']);
printf("Connected to database '%s' as '%s' on MariaDB %s\n", $db['name'], $db['user'], $c->server_info);
PHP
curl -s http://127.0.0.1/hello.php
Connected to database 'lempapp' as 'lempapp' on MariaDB 10.11.14-MariaDB-0ubuntu0.24.04.1
To host a site under its own name, add a server block. The stack's own block is at /etc/nginx/sites-available/cloudimg-lemp; copy its location ~ \.php$ section, which is what routes PHP to the FPM socket:
server {
listen 80;
server_name www.example.com;
root /var/www/example;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
}
Enable it and reload nginx:
sudo ln -s /etc/nginx/sites-available/example /etc/nginx/sites-enabled/example
sudo nginx -t && sudo systemctl reload nginx
Step 9 - Component versions
nginx -v 2>&1; php -v | head -1; mariadb --version
nginx version: nginx/1.24.0 (Ubuntu)
PHP 8.3.6 (cli) (built: Jul 16 2026 18:30:41) (NTS)
mariadb Ver 15.1 Distrib 10.11.14-MariaDB, for debian-linux-gnu (x86_64) using EditLine wrapper
grep -oE "VERSION = '[0-9.]+'" /usr/share/phpmyadmin/libraries/classes/Version.php | head -1
VERSION = '5.2.3'
The /phpinfo.php page reports the full PHP runtime and every loaded extension in the browser. Delete that file before putting a production site live.

Step 10 - Where your data lives
The two tiers sit on separate dedicated Azure data disks, so the database and your application files are both kept off the operating system disk and can be resized independently.
df -h /var/lib/mysql /var/www | sed 's/ */ /g'
Filesystem Size Used Avail Use% Mounted on
/dev/sda 20G 127M 19G 1% /var/lib/mysql
/dev/sdb 20G 52K 19G 1% /var/www
findmnt -no SOURCE,TARGET,FSTYPE /var/lib/mysql; findmnt -no SOURCE,TARGET,FSTYPE /var/www
/dev/sda /var/lib/mysql ext4
/dev/sdb /var/www ext4

Both disks are captured into the image and re-provisioned with every VM, so this layout is what you get on every deployment.
Step 11 - Review the database accounts
Every account on the server is created by first boot with a password unique to this VM. root@localhost is deliberately pinned to unix_socket, which means it is usable only by root on the box itself and has no network usable password at all.
sudo mariadb -e "SELECT CONCAT(User,'@',Host) AS account, plugin FROM mysql.user ORDER BY User,Host;"
account plugin
lempapp@127.0.0.1 mysql_native_password
lempapp@localhost mysql_native_password
mariadb.sys@localhost mysql_native_password
mysql@localhost mysql_native_password
pma@127.0.0.1 mysql_native_password
pma@localhost mysql_native_password
root@127.0.0.1 mysql_native_password
root@::1 mysql_native_password
root@localhost unix_socket
There is no anonymous account, no test database and no packaged maintenance user: those are removed when the image is built and again on first boot. mariadb.sys and mysql are internal MariaDB accounts that are locked and reachable only over the local socket.
Enabling HTTPS with Let's Encrypt
Point a DNS A record at the VM's public IP, open 443/tcp in the NSG, then install certbot with the nginx plugin and request a certificate. Certbot edits the nginx server block in place and installs a renewal timer.
sudo apt-get update && sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d www.example.com --agree-tos -m you@example.com --redirect
Verify the renewal timer afterwards with systemctl list-timers | grep certbot.
Security notes
- The stack serves plain HTTP on port 80 out of the box. For anything beyond a trusted network, add TLS as above or put the VM behind Azure Application Gateway with your own certificate.
- MariaDB listens on
127.0.0.1only and is never reachable from the network. Administer it through phpMyAdmin, over SSH withsudo mariadb, or from your application over the loopback interface. - Every password is unique to this VM. Keep
/root/lemp-stack-credentials.txtprotected, and rotate the passwords if you share access to the machine. - Point your application at the
lempappuser rather thanroot. Its privileges are limited to the application database, so a compromised application cannot reach the rest of the server. - The application credentials live at
/etc/cloudimg/lemp-db.php, outside the document root and readable only by root and the web user, so they cannot be fetched over HTTP. - Delete
/var/www/html/phpinfo.phpbefore a production launch. It reports your PHP configuration in detail and is a build time convenience, not something to leave exposed. - Restrict inbound
80/tcpand443/tcpin your NSG to the networks that need them, and keep22/tcplimited to your management range.
Support
This image is maintained by cloudimg with 24/7 support. If you need help deploying or operating the LEMP stack on Azure, contact us at cloudimg.co.uk.