LAPP stack
Overview
The LAPP stack is the PostgreSQL sibling of the classic LAMP stack: Linux, the Apache HTTP Server (the A), PostgreSQL (the P) and PHP. Apache embeds the PHP interpreter in the web server process with mod_php, so it executes PHP in place without a separate process pool. PostgreSQL provides a robust, standards-compliant relational database. Together they form a proven platform for content management systems, custom PHP sites and PostgreSQL-backed applications.
This SecureAnyCloud image wires the whole stack together and hardens it, on an Ubuntu 24.04 LTS base patched at build time. Apache 2.4 serves port 80 with PHP 8.3 running as mod_php, and PostgreSQL 16 listens only on the loopback interface. Every password is generated uniquely on the first boot of each VM, there is no default login anywhere in the image, and an application database is ready for your code before you first sign in.
What you get:
- Apache 2.4 serving port 80, with PHP executed in-process by mod_php under the prefork MPM
- PHP 8.3 with the extension set modern applications expect, including pdo_pgsql, pgsql, mbstring, gd, intl, bcmath, zip, curl, xml and opcache
- PostgreSQL 16, bound to
localhostonly and never exposed to the network, with scram-sha-256 password authentication - A ready-to-use application database and a least-privilege database role, created on first boot
- A live
/status.phpstack status page that proves Apache, PHP and PostgreSQL are all working together - Adminer 6.0.0 at
/adminerfor browser-based PostgreSQL administration - Per-VM passwords for the PostgreSQL superuser and the application role, recorded in a root-only file
- A first-boot-gated web tier: Apache starts only once first boot has generated this VM's credentials
apache2.serviceandpostgresql@16-main.serviceas enabled systemd units- An unauthenticated
/healthzendpoint for Azure Load Balancer health probes - 24/7 SecureAnyCloud support
Prerequisites
You need an active Azure subscription, an SSH key pair, and a VNet with a subnet in the target region.
Standard_B2s (2 vCPU / 4 GiB RAM) is a sensible starting size. Choose a larger size for busier sites or larger databases.
Configure the network security group (NSG) to allow inbound 22/tcp from your management network and 80/tcp (plus 443/tcp once you add TLS) for the web tier. PostgreSQL is never exposed. It listens on localhost only, so port 5432 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 LAPP 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).
Select Review + create, then 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:lapp:default:latest
az vm create \
--resource-group <your-rg> \
--name lapp-stack \
--image sac1647283583153:lapp:default:latest \
--size Standard_B2s \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku StandardOpen port 80 for the web tier:
az vm open-port --resource-group <your-rg> --name lapp-stack --port 80Step 3 - Confirm the stack is running
SSH in as azureuser. Confirm that Apache and PostgreSQL are active, along with the one-shot first-boot service that generated this VM's credentials.
systemctl is-active apache2 postgresql@16-main lapp-stack-firstbootactive active activeCheck what is listening. Apache is on port 80 on all interfaces. PostgreSQL is on 127.0.0.1:5432 only, so it is unreachable from the network.
ss -tlnp | grep -E ':80 |:5432 ' | sed 's/ */ /g'LISTEN 0 244 127.0.0.1:5432 0.0.0.0:* users:(("postgres",pid=15275,fd=6)) LISTEN 0 511 *:80 *:* users:(("apache2",pid=17118,fd=4),("apache2",pid=17117,fd=4))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/lapp-stack-credentials.txtThe file records everything you need:
LAPP_POSTGRES_PASSWORD: sign in to Adminer as userpostgreswith this password for full administration.LAPP_APP_DB_NAME,LAPP_APP_DB_USERandLAPP_APP_DB_PASSWORD: a database that already exists, with a role whose privileges are limited to it. Point your application at these values.LAPP_URL,LAPP_STATUS_URLandLAPP_ADMINER_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 first boot, or you are reading the URLs from outside the VNet, substitute the VM's public IP from the Azure Portal. The paths are unchanged.
On the VM itself, you can always reach the database directly over the local unix socket, with no password. The postgres operating system account uses peer authentication:
sudo -u postgres psql -c 'SELECT version();'Step 5 - Verify the stack end to end
Apache serves the health endpoint on its own, with no PHP involved. 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/healthz200The /status.php page is the real proof. A single request exercises all three tiers: Apache accepts it, mod_php executes it, and the script opens a connection to PostgreSQL and reads a row back. The page prints the LAPP_STATUS_OK marker only when every tier answered.
curl -s http://127.0.0.1/status.php | grep -oE 'LAPP_STATUS_OK|Apache|PHP [0-9.]+ via [a-z0-9]+|PostgreSQL [0-9.]+' | sort -uApache LAPP_STATUS_OK PHP 8.3.6 via apache2handler PostgreSQL 16.14Open http://<public-ip>/status.php in a browser for the readable version, including the live query result read back from the database.
Browse to http://<public-ip>/ for the welcome page. It links to the status page, the PHP runtime report and Adminer.
Step 6 - Administer the database from Adminer
Browse to http://<public-ip>/adminer/. Choose PostgreSQL as the system and enter 127.0.0.1 as the server. Sign in as user postgres with the LAPP_POSTGRES_PASSWORD from Step 4. Optionally, set the database to lappapp.
After you sign in, the left navigation lists every database on the server, including your application database. Select a table to see its rows, or use the SQL command link for a full query editor. The sac_status table holds the row that the status page reads.
Step 7 - Use the application database
The application database already exists, and its role 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 '^LAPP_APP_DB_PASSWORD=' /root/lapp-stack-credentials.txt | cut -d= -f2-); PGPASSWORD="$PW" psql -w -h 127.0.0.1 -U lappapp -d lappapp -c '\dt' -c 'SELECT * FROM sac_status;'List of relations Schema | Name | Type | Owner --------+-----------------+-------+--------- public | sac_status | table | lappapp (1 row) id | status | checked_at ----+--------+------------------------------- 1 | ok | 2026-08-08 04:42:33.947132+00 (1 row)The sac_status table holds the single row that the status page reads. You can drop it once your own schema is in place.
Step 8 - Deploy your own PHP application
Put your application in /var/www/html and Apache serves it immediately. An index.php file takes precedence over the SecureAnyCloud welcome page.
Your application's database credentials are already on the VM, in a file that the web user can read. The file sits outside the document root, so the password is never reachable over the web. Any PHP file can load the credentials:
sudo tee /var/www/html/hello.php >/dev/null <<'PHP' <?php $db = require '/etc/sac/lapp-db.php'; $pdo = new PDO("pgsql:host={$db['host']};port={$db['port']};dbname={$db['name']}", $db['user'], $db['pass']); $v = explode(' on ', (string) $pdo->query('SELECT version()')->fetchColumn())[0]; printf("Connected to database '%s' as '%s' on %s\n", $db['name'], $db['user'], $v); PHP curl -s http://127.0.0.1/hello.phpConnected to database 'lappapp' as 'lappapp' on PostgreSQL 16.14 (Ubuntu 16.14-0ubuntu0.24.04.1)To host a site under its own name, add a virtual host. The stack's own vhost is at /etc/apache2/sites-available/sac-lapp.conf. Copy it as a starting point:
<VirtualHost *:80> ServerName www.example.com DocumentRoot /var/www/example DirectoryIndex index.php index.html <Directory /var/www/example> Options -Indexes +FollowSymLinks AllowOverride None Require all granted </Directory> </VirtualHost>Enable it and reload Apache:
sudo a2ensite example && sudo apache2ctl configtest && sudo systemctl reload apache2Step 9 - Check the component versions
Print the Apache, PHP and PostgreSQL versions:
apache2 -v | head -1; php -v | head -1; sudo -u postgres psql -tAc 'SELECT version()'Server version: Apache/2.4.58 (Ubuntu) PHP 8.3.6 (cli) (built: Jul 16 2026 18:30:41) (NTS) PostgreSQL 16.14 (Ubuntu 16.14-0ubuntu0.24.04.1) on x86_64-pc-linux-gnu, compiled by gcc 13.3.0, 64-bitAdminer 6.0.0 is served at /adminer. The /phpinfo.php page reports the full PHP runtime and every loaded extension in the browser. Delete that file before you put a production site live.
Step 10 - Review the database roles and hardening
First boot creates every login role on the server with a password unique to this VM. The lappapp application role is not a superuser and is limited to the application database. The postgres superuser is reachable over TCP only with the per-VM password.
List the login roles:
sudo -u postgres psql -c "SELECT rolname, rolsuper, rolcanlogin FROM pg_roles WHERE rolcanlogin ORDER BY rolname"rolname | rolsuper | rolcanlogin ----------+----------+------------- lappapp | f | t postgres | t | t (2 rows)PostgreSQL listens on localhost only. Its host-based authentication uses scram-sha-256 on every TCP path, with no trust method anywhere. Peer authentication on the local unix socket is what lets sudo -u postgres psql work on the VM without a password.
sudo -u postgres psql -tAc 'SHOW listen_addresses'; grep -E '^(local|host)' /etc/postgresql/16/main/pg_hba.conflocalhost local all postgres peer local all all peer host all all 127.0.0.1/32 scram-sha-256 host all all ::1/128 scram-sha-256Enabling 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 Apache plugin and request a certificate. Certbot edits the Apache virtual host in place and installs a renewal timer.
sudo apt-get update && sudo apt-get install -y certbot python3-certbot-apache
sudo certbot --apache -d www.example.com --agree-tos -m you@example.com --redirectAfterwards, verify the renewal timer with systemctl list-timers | grep certbot.
Security notes
- Encrypt web traffic. 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.
- Keep PostgreSQL private. It listens on
localhostonly and is never reachable from the network. Administer it through Adminer, over SSH withsudo -u postgres psql, or from your application over the loopback interface. - Protect the passwords. Every password is unique to this VM. Keep
/root/lapp-stack-credentials.txtprotected, and rotate the passwords if you share access to the machine. - Use the application role. Point your application at the
lappapprole rather thanpostgres. Its privileges are limited to the application database, so a compromised application cannot reach the rest of the server. - Keep application credentials off the web. They live at
/etc/sac/lapp-db.php, outside the document root and readable only by root and the web user, so they cannot be fetched over HTTP. - Delete phpinfo. Remove
/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 ports. Limit inbound
80/tcpand443/tcpin your NSG to the networks that need them, and keep22/tcplimited to your management range.
Support
SecureAnyCloud maintains this image and provides 24/7 support. For help deploying or operating the LAPP stack on Azure, contact us through secureanycloud.com.
Deploy on Azure
Launch LAPP 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