Applications Azure

Evolution CMS on Ubuntu 24.04 on Azure User Guide

| Product: Evolution CMS 3.5.7 on Ubuntu 24.04 LTS on Azure

Overview

This guide covers the deployment and configuration of Evolution CMS on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. Evolution CMS is an open source PHP content management system and application framework, developed by the Evolution CMS community and published at evo.im, descended from the long standing MODX Evolution lineage and still under active development today. It imposes no fixed structure on a site: pages are called resources, and everything a template needs — reusable snippets and chunks, template variables, a flexible tag based templating language — is composed by the developer. A full featured Manager admin dashboard gives a content team resource editing, a file manager, user roles and permissions, and a plugin and module system for extending the CMS without touching core code.

The image installs Evolution CMS 3.5.7 from a cloudimg-mirrored copy of the official tagged release, nginx and PHP 8.3 FPM to serve it, and MariaDB from Ubuntu 24.04 to hold the site data, and wires all three together. Unattended security upgrades are configured to keep the server patched on your running VM.

One appliance, three services. nginx and php8.3-fpm serve the site on port 80, and mariadb holds every resource, account and setting. MariaDB is bound to 127.0.0.1:3306 and PHP FPM listens on a Unix socket, so neither is ever exposed to the network. Port 80 is the only reachable surface.

An allowlist nginx configuration, not a single web root. Unlike a modern framework with a public/ subdirectory, Evolution CMS serves directly from its own tree root, so the nginx configuration is an explicit allowlist. core/ (the application code, its vendored dependencies, and the database configuration file that holds this VM's database password) and install/ are both denied outright, regardless of file type, even though they live inside the web root on disk. assets/ (the file manager's upload, cache and backup trees) is served as static content only — PHP execution is refused there so an uploaded .php file can never run. /manager/ is the one PHP-executing area outside the public front controller: Evolution CMS's own admin application, reached exactly as its own routing expects.

Security by design — there is no administrator account in the image. Rather than shipping a default login and rotating it, this image ships no database schema, no administrator account and no install wizard reachable at any point. On the very first boot of every VM, a one shot service runs Evolution CMS's own non-interactive installer to create the database schema and the single administrator account with a unique per-VM password, proves the new password signs in through the real Manager login form and that admin/admin, a blank password and other common guesses do not, and only then deletes the install directory from disk and opens the web server. Until every one of those steps has completed, nothing is listening on port 80 at all.

The site cannot serve an unprovisioned instance. nginx and php8.3-fpm are each gated on a bootstrap marker that first boot writes only after every credential is in place and the install wizard is gone. Until that marker exists, systemd skips those units entirely, so there is no window in which a half provisioned site — or the setup wizard — is reachable. The units are still enabled, so the site comes straight back after a reboot.

What is included:

  • Evolution CMS 3.5.7 served by nginx and PHP 8.3 FPM, with the repository root as the web root and core/, install/ and PHP execution inside assets/ all denied at the web server

  • MariaDB (mariadb.service) holding the site data, bound to loopback

  • A full featured Manager admin dashboard: resource editing, a file manager, user roles and permissions, and a plugin and module system

  • A per instance database password and administrator password generated on first boot and documented in /root/evolution-cms-credentials.txt (0600)

  • The install wizard is deleted from disk as part of the same first boot step that creates your administrator account, so it can never be reached again

Prerequisites

  • Active Azure subscription, SSH public key, VNet and subnet in the target region

  • Subscription to the Evolution CMS listing on Azure Marketplace

  • Network Security Group rules allowing TCP 22 (administration) and TCP 80 (the website) from the networks that need them

  • A registered domain if you want the site to be reachable by name rather than by IP address, plus access to your DNS provider

Step 1: Deploy from the Azure Portal

Search Evolution CMS in Marketplace, select the cloudimg publisher, and click Create. Choose Standard_B2s or larger. Configure the Network Security Group to allow TCP 80 for the website from the internet (or from your own networks while you set it up) and TCP 22 for administration from your administrative networks only. The database and the PHP runtime stay on loopback and are never exposed.

Step 2: Deploy from the Azure CLI

RG="content-prod"; LOCATION="eastus"; VM_NAME="evolution-cms"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/evolution-cms/versions/<version>"
SSH_KEY="$(cat ~/.ssh/id_rsa.pub)"
az group create --name "$RG" --location "$LOCATION"
az vm create \
  --resource-group "$RG" --name "$VM_NAME" \
  --image "$GALLERY_IMAGE_ID" \
  --size Standard_B2s \
  --admin-username azureuser \
  --ssh-key-values "$SSH_KEY" \
  --public-ip-sku Standard
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 80 --priority 1001
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 22 --priority 1002

Step 3: First boot and your credentials

On first boot the image generates this VM's database password and administrator password, creates the database schema and the single administrator account, opens the bootstrap gate, starts the web server, verifies the new credentials work and the common defaults are rejected, deletes the install directory, and writes /root/evolution-cms-credentials.txt. This completes within a minute or two. SSH in as azureuser and read the details:

sudo cat /root/evolution-cms-credentials.txt

The file is mode 0600 and owned by root, so only a privileged user can read it. It contains the site URL, the administrator username and password, and the database name, user and password.

You can also confirm the install wizard and the database configuration file are both unreachable, exactly as they are on every fresh instance:

for p in /install /install/index.php /install/cli-install.php \
         /core/config/database/connections/default.php; do
  curl -s -o /dev/null -w "%{http_code}  $p\n" "http://127.0.0.1$p"
done

Four HTTP requests to the install wizard and the database configuration file, all returning 404 — the install directory is deleted from disk by Evolution CMS's own installer, and nginx denies it outright regardless of file type

Confirm the credentials file, every enabled unit, and the absence of swap on the OS disk:

sudo stat -c '%a %U:%G %n' /root/evolution-cms-credentials.txt
systemctl is-enabled mariadb.service php8.3-fpm.service nginx.service evolution-cms-firstboot.service
swapon --show

The credentials file at mode 600 owned by root, the mariadb, php8.3-fpm, nginx and first-boot units all enabled, and no output from swapon — zero swap devices on the OS disk

Step 4: Confirm the appliance is running

All three services should report active. ss confirms the site is on port 80 while MariaDB is bound to loopback only, and the response headers carry no server version banner.

systemctl is-active mariadb.service php8.3-fpm.service nginx.service
php -r 'echo "PHP ".PHP_VERSION."\n";'
mysql --version
nginx -v
ss -tlnp | grep -E ':(80|3306) ' | sed 's/users:.*//' | sort
curl -sI http://127.0.0.1/ | head -4

Expected output:

active
active
active
PHP 8.3.6
mysql  Ver 15.1 Distrib 10.11.14-MariaDB, for debian-linux-gnu (x86_64) using  EditLine wrapper
nginx version: nginx/1.24.0 (Ubuntu)
LISTEN 0      511             [::]:80           [::]:*
LISTEN 0      511          0.0.0.0:80        0.0.0.0:*
LISTEN 0      80         127.0.0.1:3306      0.0.0.0:*
HTTP/1.1 200 OK
Server: nginx
Date: ...
Content-Type: text/html; charset=UTF-8

The mariadb, php8.3-fpm and nginx systemd units all reporting active, PHP 8.3.6, MariaDB 10.11.14, nginx 1.24.0, and nginx bound to port 80 while MariaDB stays on loopback 3306

Step 5: Sign in to the Manager

Browse to http://<your-vm-public-ip>/manager/ and you will see the Evolution CMS Manager login page.

The Evolution CMS Manager sign-in page

Sign in with the username admin and the password from the credentials file. On a brand new install the Manager first shows a System configuration review screen — this is expected on first login only; review or accept the defaults and click Save + Continue editing to proceed into the Manager proper.

You can prove the same sign in from the command line. This uses the per instance password from the credentials file to complete a real login through the real Manager form, then checks that common default passwords are refused:

sudo /usr/local/sbin/evolution-cms-login-check.sh admin '<EVOLUTION_ADMIN_PASSWORD>' && echo "per-VM password: ACCEPTED (correct)"
sudo /usr/local/sbin/evolution-cms-login-check.sh admin 'admin' || echo "admin/admin: REJECTED (correct)"
sudo /usr/local/sbin/evolution-cms-login-check.sh admin '' || echo "blank password: REJECTED (correct)"

Expected output:

per-VM password: ACCEPTED (correct)
admin/admin: REJECTED (correct)
blank password: REJECTED (correct)

You can also run the full credential round trip cloudimg itself uses as a build gate. It never prints a credential value — only pass or fail against the real login form:

sudo /usr/local/sbin/evolution-cms-cred-roundtrip.sh

The credential round trip script output, confirming the per-VM administrator password authenticates through the real Manager form, the install wizard is gone from both disk and nginx, the database password is never leaked over HTTP, and every guessable default credential is rejected — with no credential value ever printed

Once past the first-login settings review, the Manager shows the resource tree down the left and the details of the selected resource on the right — the seeded Evolution CMS Install Success resource that ships with every fresh install.

The Evolution CMS Manager, editing the seeded homepage resource

Step 6: Change the administrator password

The first thing to do after signing in is set your own password. Click admin in the top right of the Manager and choose My Settings, then use the password fields to set a new one. Evolution CMS stores passwords using a salted, iterated hash (the same phpass lineage WordPress uses), so the new password is never recoverable from the database. Once you have changed it, the copy in /root/evolution-cms-credentials.txt is stale — keep the file for the database password, or remove the administrator password line from it.

Step 7: Create your first resource

In the Manager's left-hand resource tree, click the New document icon in the toolbar (or right-click the site root and choose Create Resource here) to open the New Resource form. Give it a Title, and write the page body into the Resource content field.

Creating a new resource in the Evolution CMS Manager

Evolution CMS's pagetitle field is metadata used for the browser title and the Manager's own listings — with no template assigned to a resource (the default for a fresh page, matching the seeded homepage), the page renders the raw Resource content field with no wrapper at all. If you want the title to appear on the page itself, include it in the content, for example as an <h1> heading, exactly as a normal HTML page would.

Click Save + Continue editing, then open the public site at the resource's URL (the Manager's toolbar has a Preview button, or browse to http://<your-vm-public-ip>/index.php?id=<resource-id>). The content you wrote in the Manager is now live on the public site — this is the core of what Evolution CMS does, turning what you write in the dashboard into a published page.

The newly published resource rendered on the public front-end site

You can confirm from the command line that a published resource is genuinely public, with no session or cookie involved:

curl -s -o /dev/null -w 'home page: HTTP %{http_code}\n' http://127.0.0.1/
curl -s -o /dev/null -w 'healthz: HTTP %{http_code}\n' http://127.0.0.1/healthz

A freshly deployed instance answers 200 for both — the home page is the seeded resource, and /healthz is an unauthenticated liveness probe nginx serves directly, suitable for an Azure Load Balancer health check.

A note on URLs. This image ships with Evolution CMS's friendly (alias-path) URLs turned off, so every resource is reached by its numeric ID (?id=N) rather than by an alias path. This is a deliberate, fully supported upstream configuration: friendly URLs depend on a file-based alias cache that only reflects a resource created since the last cache rebuild, and ?id=N links resolve directly and reliably regardless of that cache's state — the right trade-off for a freshly provisioned appliance. Evolution CMS's own link generator uses this same ?id=N form throughout the Manager and any auto-generated navigation.

Step 8: Build out your template

A production site normally starts from a Template rather than leaving each resource blank. Open Elements → Manage Templates in the Manager to create one: a template is HTML with placeholder tags like [*pagetitle*] for the resource's title and [*content*] for its body, plus any Template Variables (custom fields) you define under Elements → Template Variables. Once a template exists, assign it to a resource from the Settings tab of that resource's edit form, and the page renders through the template instead of as raw content.

Chunks (reusable HTML snippets, Elements → Manage Chunks) and Snippets (small PHP scripts that output content, Elements → Manage Snippets) let you build out a header, footer, navigation menu or any other reusable piece once and reference it from every template with a tag like {{header}} or [[MySnippet]].

Step 9: Manage users, roles and the file manager

Users → Manage Users lists every Manager account and lets you create more — useful once you have more than one editor. Users → Manage Roles defines what each role can do inside the Manager, so you can give a content editor access to resources without exposing system settings or user management.

The built-in file manager (the folder icon in the toolbar, or Elements → Manage Files) lets you browse, upload and organise files under assets/, which is exactly the tree this image already allows the web server to serve as static content — anything you upload there is immediately reachable at its own URL, with PHP execution refused for defence in depth.

Step 10: Take a database backup

Evolution CMS's own assets/backup/ tooling (reachable from the Manager) can export the database, but you can also take a full backup from the command line at any time:

CREDS=/root/evolution-cms-credentials.txt
DB_PASS=$(sudo sed -n 's/^evolution.db.password=//p' "$CREDS")
sudo mysqldump -u evolution -p"$DB_PASS" evolution_cms > "evolution-cms-backup-$(date +%Y%m%d).sql"

Store the resulting file somewhere outside the VM (Azure Blob Storage, or copy it off with scp) so a database backup survives even if the VM itself is lost.

Step 11: Use your own domain and add TLS

Point an A record for your domain at the VM's public IP address. Evolution CMS resolves the site's own address dynamically from the incoming request, so no configuration change is needed on the application side for the domain to start working once DNS propagates.

For TLS, the simplest path on a single VM is a reverse proxy in front of nginx (for example Caddy or an additional nginx server block with Certbot) terminating HTTPS on 443 and forwarding to the existing site on 127.0.0.1:80. Keep port 80 open for the ACME HTTP-01 challenge if you use Let's Encrypt.

Step 12: Keep the system patched

The base image ships with unattended-upgrades enabled for OS-level security patches. Evolution CMS itself can be updated from the Manager (Dashboard → check for updates, where available) or by following the release notes at github.com/evolution-cms/evolution — always take a database and filesystem backup first, as recommended by upstream's own release notes.

Troubleshooting

Manager shows a 404 instead of the login page. manager/index.php refuses any request with no Accept-Language header — this only affects scripted curl checks without that header; every real browser sends one automatically.

Forgot the administrator password. SSH in and read /root/evolution-cms-credentials.txt again — the password there is only stale if you have already changed it from the Manager. If you have changed it and lost the new one, reset it directly in the database using Evolution CMS's own password hashing (EvolutionCMS\Legacy\PasswordHash) via sudo -u www-data php from /var/www/evolution, or restore from a backup taken before the change.

A resource returns 404 after creating it. Confirm it is Published (the publish checkbox on the resource's edit form) and that you are using its numeric ID (?id=N) — see the note on friendly URLs in Step 7.

Need support. cloudimg provides 24/7 technical support for this image — see the Support tab on the Azure Marketplace listing for contact details.