LNMP stack
Overview
The LNMP stack (Linux, Nginx, MySQL and PHP-FPM) is a classic platform for PHP web applications. This SecureAnyCloud image gives you the full stack on Ubuntu 24.04 LTS, ready to host your application.
What you get:
- Nginx 1.28 stable from the official nginx.org APT repository
- MySQL 8.4 LTS from Oracle's upstream repository
- PHP 8.3 with PHP-FPM (FastCGI to /run/php/php-fpm.sock) and the common extension set: mysqli, pdo_mysql, gd, mbstring, curl, xml, zip, intl and opcache
- A unique MySQL root password and application database password generated on first boot, with no credentials baked into the image
- 24/7 SecureAnyCloud support
Prerequisites
- An Azure subscription with permission to create virtual machines
- An SSH key pair, with the public key uploaded during VM creation
curlor a web browser to exercise the running stack
Step 1 - Deploy from the Azure Portal
- Find LNMP Stack on Ubuntu 24.04 LTS by SecureAnyCloud in the Azure Marketplace and click Create.
- On the Basics tab, select or create a resource group, choose East US as the region, and set the VM size to Standard_D2s_v3 (2 vCPU / 8 GB RAM). Larger sizes are fine for higher traffic.
- Under Administrator account, choose SSH public key and paste your public key.
- On the Networking tab, open inbound ports 80 (HTTP) and 443 (HTTPS) in addition to 22 (SSH).
- Click Review + create, then Create.
Step 2 - Deploy with the Azure CLI
Create the VM from the image gallery:
az vm create \
--resource-group <your-resource-group> \
--name lamp-stack \
--image "$(az sig image-version list \
--resource-group AZURE-SAC \
--gallery-name sacGallery \
--gallery-image-definition lamp-stack-ubuntu-24-04 \
--query '[0].id' -o tsv)" \
--size Standard_D2s_v3 \
--admin-username azureuser \
--ssh-key-values ~/.ssh/id_rsa.pub \
--public-ip-sku StandardThen open the HTTP and HTTPS ports from your local workstation. Replace <your-resource-group> with your resource group name:
az vm open-port --port 80 --priority 1001 --resource-group <your-resource-group> --name lamp-stack
az vm open-port --port 443 --priority 1002 --resource-group <your-resource-group> --name lamp-stackStep 3 - Connect over SSH
Connect to the VM as azureuser:
ssh azureuser@<vm-ip>On first boot, lnmp-firstboot.service initialises the MySQL data directory and rotates the root password. It also creates the default application database and user, and starts Nginx and MySQL. Allow up to 60 seconds for the service to complete.
Check that first boot has finished:
sudo test -f /var/lib/sac/data/lnmp-firstboot.done && echo "firstboot done"Step 4 - Retrieve your MySQL credentials
Read the credentials file with sudo:
sudo cat /stage/scripts/lnmp-credentials.logThe file is owned by root:root (mode 0600). It contains the MySQL root password, the default application database name, the application user name and the application password.
The file looks like this:
mysql_root_password=<generated> app_db=sac_app app_db_user=sac_app app_db_password=<generated>Extract the passwords into shell variables for the rest of this guide:
MYSQL_ROOT_PASS=$(sudo awk -F= '/^mysql_root_password=/ {print $2}' /stage/scripts/lnmp-credentials.log)
APP_DB_PASS=$(sudo awk -F= '/^app_db_password=/ {print $2}' /stage/scripts/lnmp-credentials.log)
echo "root password length: ${#MYSQL_ROOT_PASS}"
echo "app password length: ${#APP_DB_PASS}"Step 5 - Verify that Nginx and MySQL are running
Check both services and the listening ports:
sudo systemctl is-active nginx.service
sudo systemctl is-active mysql.service ss -tln | grep -E ':(80|3306)\b'The output shows active twice, with both ports listening.
Step 6 - Request the sample PHP page
Request the sample page with curl:
curl -s -o /tmp/index.html -w "HTTP %{http_code}\n" http://127.0.0.1/ grep -q "LNMP Stack" /tmp/index.html && echo "sac welcome page served"Open http://<vm-ip>/ in a browser to see the same page. It shows a live MySQL connection test and the runtime details of your server.
Component versions
Once the VM is running, SSH in and confirm the three stack components at the versions SecureAnyCloud ships: Nginx 1.28 stable, MySQL 8.4 LTS and PHP 8.3.
Service health
All three services run under systemd. Use systemctl status to confirm that nginx, MySQL and PHP-FPM are active.
sudo systemctl status mysql.service nginx.service php8.3-fpm.service --no-pagerMySQL round-trip
On first boot, the first-boot service rotates the MySQL root password and creates a sac_app database and user. Read the generated credentials from /stage/scripts/lnmp-credentials.log (root-only), then confirm that you can log in and query:
APP_PASS=$(sudo awk -F= '/^app_db_password=/ {print $2}' /stage/scripts/lnmp-credentials.log)
echo "SELECT VERSION(), DATABASE(), USER();" | sudo MYSQL_PWD="$APP_PASS" mysql -u sac_app sac_app -tThe green CONNECTED badge on the sample page is a self-test. The page reads the app user password from /stage/scripts/lnmp-credentials.log (readable by www-data at mode 0640 root:www-data) and opens a PDO connection to MySQL on 127.0.0.1:3306. It then prints the server version returned by SELECT VERSION().
If the page shows the first-boot-not-complete fallback instead, wait another minute and refresh.
Step 7 - Run a MySQL round trip as the app user
Create a table, insert a row and read it back as the application user:
APP_DB_PASS=$(sudo awk -F= '/^app_db_password=/ {print $2}' /stage/scripts/lnmp-credentials.log) MYSQL_PWD="$APP_DB_PASS" mysql -u sac_app --host=127.0.0.1 sac_app <<'SQL' CREATE TABLE IF NOT EXISTS demo (id INT PRIMARY KEY, label VARCHAR(64)); INSERT INTO demo (id, label) VALUES (1, 'hello') ON DUPLICATE KEY UPDATE label='hello'; SELECT * FROM demo; SQLThe output shows the row 1 | hello.
Step 8 - Replace the sample page with your application
The document root is /var/www/html. Remove the sample page and put your PHP application in its place:
sudo rm /var/www/html/index.php
sudo tee /var/www/html/index.php >/dev/null <<'PHP' <?php echo "Hello from my LNMP app!"; PHP curl -s http://127.0.0.1/For a real application, upload over SCP or SFTP, or run git clone into /var/www/html (or a subdirectory). Make sure ownership is www-data:www-data and the files are readable by the web server.
Step 9 - Serve a custom domain
Point your DNS A record at the VM's public IP, then create an Nginx virtual host:
sudo tee /etc/nginx/sites-available/example.com.conf >/dev/null <<'CONF' <server block *:80> ServerName example.com ServerAlias www.example.com DocumentRoot /var/www/html ErrorLog /var/log/nginx/example.com-error.log CustomLog /var/log/nginx/example.com-access.log combined </server block> CONF
sudo a2ensite example.com.conf
sudo nginxctl configtest
sudo systemctl reload nginxStep 10 - Add HTTPS with Let's Encrypt
Install Certbot and obtain a certificate:
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y certbot python3-certbot-apache
sudo certbot --apache -d example.com -d www.example.com --non-interactive --agree-tos -m you@example.comCertbot configures a second virtual host on port 443, installs the certificate and adds a systemd timer that renews it automatically every 60 days.
Step 11 - Common admin tasks
Reload Nginx or restart MySQL after a config change:
sudo systemctl reload nginx.service
sudo systemctl restart mysql.serviceTail the logs. Use -F for continuous output in an interactive session, or -n for a one-shot look:
sudo tail -n 20 /var/log/nginx/error.log /var/log/mysql/error.log 2>/dev/null || trueTo administer MySQL as root, authenticate over the Unix socket with the rotated password from /stage/scripts/lnmp-credentials.log. Use sudo env so the password variable survives sudo's default env_reset. This example lists the existing users:
MYSQL_ROOT_PASS=$(sudo awk -F= '/^mysql_root_password=/ {print $2}' /stage/scripts/lnmp-credentials.log)
sudo env MYSQL_PWD="$MYSQL_ROOT_PASS" mysql -u root --socket=/var/run/mysqld/mysqld.sock \
-e "SELECT user, host FROM mysql.user ORDER BY user, host;"To change the root password, replace new-strong-password with your own value and run the ALTER statement against the same socket:
sudo env MYSQL_PWD="$MYSQL_ROOT_PASS" mysql -u root --socket=/var/run/mysqld/mysqld.sock \
-e "ALTER USER 'root'@'localhost' IDENTIFIED BY 'new-strong-password';"To add another MySQL database and user, pick a unique name and a strong password:
MYSQL_ROOT_PASS=$(sudo awk -F= '/^mysql_root_password=/ {print $2}' /stage/scripts/lnmp-credentials.log)
sudo env MYSQL_PWD="$MYSQL_ROOT_PASS" mysql -u root --socket=/var/run/mysqld/mysqld.sock <<'SQL' CREATE DATABASE reports CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'reports_app'@'localhost' IDENTIFIED BY 'choose-a-strong-password'; GRANT ALL PRIVILEGES ON reports.* TO 'reports_app'@'localhost'; FLUSH PRIVILEGES; SQLStep 12 - Security hardening checklist
- Put MySQL behind a firewall. In production,
ss -tlnshould show:3306bound only to127.0.0.1. - Rotate the root password on a schedule and store it in a secret manager.
- Run
sudo apt-get update && sudo apt-get -y upgradeon a regular cadence. - Use
fail2banif SSH is reachable from the public Internet.
Support
SecureAnyCloud provides 24/7/365 expert technical support, with a guaranteed response within 24 hours and a one-hour average for critical issues. Contact sales@secureanycloud.com.
Visit secureanycloud.com/products for our full catalogue.
Deploy on Azure
Launch lnmp-stack with 24/7 support from SecureAnyCloud.
View on Marketplace
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com