LEMP stack
Overview
The LEMP stack is the nginx counterpart of the classic LAMP stack: Linux, nginx (pronounced "engine x", the E), MariaDB and PHP. nginx serves static files itself and hands PHP requests to a dedicated PHP-FPM process pool over a unix socket. This event-driven model lets LEMP handle large numbers of simultaneous connections on modest hardware. It is a popular choice for content management systems, custom PHP sites and API backends.
This SecureAnyCloud image wires the whole stack together and hardens it, on an Ubuntu 24.04 LTS base patched at build time. 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. An application database is ready for your code before you first sign in.
What you get:
- 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 SecureAnyCloud 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) as a sensible starting point. Size up for busier sites or larger databases
- NSG inbound rules that allow
22/tcpfrom your management network and80/tcp(plus443/tcponce 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 SecureAnyCloud, and select Create.
On the Basics tab, 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 choose Review + create, followed by Create.
Step 2 - Deploy from the Azure CLI
# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:lemp-stack:default:latest
az vm create \
--resource-group <your-rg> \
--name lemp-stack \
--image sac1647283583153:lemp-stack:default:latest \
--size Standard_B2s \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku StandardOpen port 80 to the web tier.
az vm open-port --resource-group <your-rg> --name lemp-stack --port 80Step 3 - Confirm the stack is running
Connect over SSH as azureuser. Confirm that 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-firstbootactive active active activeCheck 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.txtThe 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 from outside the VNet, substitute the VM's public IP from the Azure Portal in these URLs. The paths are unchanged.
On the VM 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
Request the health endpoint. nginx serves it alone, with no PHP involved, and it is the endpoint an Azure Load Balancer probe should target.
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/healthz200Request the /status.php page, which is the real proof. It exercises all three tiers in a single request. nginx accepts the request, PHP-FPM executes the page, and the page 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 -5LEMP_STATUS_OK nginx/1.24.0 PHP 8.3.6 via fpm-fcgi MariaDB 10.11.14-MariaDB-0ubuntu0.24.04.1Open http://<public-ip>/status.php in a browser to see the readable version, including the live query result read back from the database.
Open http://<public-ip>/ to see the welcome page. It 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.
After you sign 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 and select Go. 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, then 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 sac_status;'Tables_in_lempapp sac_status id status checked_at 1 ok 2026-08-06 22:31:31The sac_status table holds 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. An index.php takes precedence over the SecureAnyCloud 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 reachable over the web. Any PHP file can pull them in.
sudo tee /var/www/html/hello.php >/dev/null <<'PHP' <?php $db = require '/etc/sac/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.phpConnected to database 'lempapp' as 'lempapp' on MariaDB 10.11.14-MariaDB-0ubuntu0.24.04.1To host a site under its own name, add a server block. The stack's own block is at /etc/nginx/sites-available/sac-lemp. Copy its location ~ \.php$ section, which 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 the site and reload nginx.
sudo ln -s /etc/nginx/sites-available/example /etc/nginx/sites-enabled/example
sudo nginx -t && sudo systemctl reload nginxStep 9 - Component versions
nginx -v 2>&1; php -v | head -1; mariadb --versionnginx 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 wrappergrep -oE "VERSION = '[0-9.]+'" /usr/share/phpmyadmin/libraries/classes/Version.php | head -1VERSION = '5.2.3'The /phpinfo.php page reports the full PHP runtime and every loaded extension in the browser. Delete that file once you no longer need it, and always before the site serves real traffic.
Step 10 - Where your data lives
The two tiers sit on separate dedicated Azure data disks. The database and your application files are both kept off the operating system disk and can be resized independently. Check the disks and mounts.
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/wwwfindmnt -no SOURCE,TARGET,FSTYPE /var/lib/mysql;
findmnt -no SOURCE,TARGET,FSTYPE /var/www/dev/sda /var/lib/mysql ext4 /dev/sdb /var/www ext4Both 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
First boot creates every account on the server with a password unique to this VM. root@localhost is deliberately pinned to unix_socket. That means only root on the VM itself can use it, and it has no network-usable password at all. List the accounts.
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_socketThere is no anonymous account, no test database and no packaged maintenance user. They 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 and 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 --redirectAfterwards, verify the renewal timer 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 described 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 VM. - 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/sac/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 the site serves real traffic. 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 SecureAnyCloud with 24/7 support. If you need help deploying or operating the LEMP stack on Azure, contact us at secureanycloud.com.
Deploy on Azure
Launch LEMP Stack on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.
View this image
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com