Deployment guide

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
  • curl or 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 Standard

Then 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-stack

Step 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.log

The 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-pager

MySQL 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 -t

The 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; SQL

The 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 nginx

Step 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.com

Certbot 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.service

Tail 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 || true

To 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; SQL

Step 12 - Security hardening checklist

  • Put MySQL behind a firewall. In production, ss -tln should show :3306 bound only to 127.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 upgrade on a regular cadence.
  • Use fail2ban if 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

Deploy on Azure