Cc
Applications Azure

Concrete CMS on Ubuntu 24.04 on Azure User Guide

| Product: Concrete CMS on Ubuntu 24.04 LTS on Azure

Overview

This guide covers deploying and using the Concrete CMS 9.5.7 on Ubuntu 24.04 LTS image from cloudimg on Microsoft Azure.

Concrete CMS (formerly concrete5) is a free and open source content management system known for in-context editing: signed-in editors change a page on the page itself, adding and arranging blocks of content, while the Dashboard manages the sitemap, files, users, permissions, forms, calendars and themes. The image starts with Concrete's own Elemental sample site (pages, a blog, a portfolio and sample images) for you to edit or replace.

The cloudimg image adds:

  • The 9.5.7 security update. Concrete CMS 9.5.7 is the newest stable release. It includes the security fixes of 9.5.3 to 9.5.5 (more than sixty CVEs, several reachable without signing in, such as the user search autocomplete CVE-2026-18110) and updated bundled libraries, including Guzzle 7.16 (the HTTP client; CVE-2026-69246 is fixed in 7.15.2). The download is verified against a pinned checksum (Step 3 shows the bundled versions).
  • No web installer, no shared database. Concrete's web installer creates the database connection and the first administrator for whoever opens it first. This image ships no database at all: the MariaDB data directory on the data disk is empty in the image, and there is no database.php. Your VM's first boot initialises its own database and runs Concrete's own command line installer (c5:install) before the web server starts, which creates the schema, the sample site, the super user admin with a password generated on your VM, and the configuration.
  • Secrets of your VM's own. The database password, the admin password, Concrete's three security tokens (they protect scheduled jobs, encrypted values and validation links) and, when you switch the REST API on, its RSA key pair are all generated on your VM. Nothing from the image build is reused, so no two VMs share a secret. No secret is ever passed on a command line.
  • No calls home. Concrete normally connects a new site to its marketplace, checks concretecms.com for updates, and fetches news, tutorials and background images from concretecms.com servers. On this image the server makes none of those calls (Step 7 shows each switch; the image was tested by counting the network connections PHP opens while an administrator browses the Dashboard, and there were none), and the geoPlugin visitor geolocation lookup is off. The Elemental theme loads its web font from Google Fonts and the Dashboard loads Google Charts in your browser.
  • Closed by default. Public registration is off, e-mail is off until you configure a mail server, and error details are never shown to visitors.
  • HTTPS from the first boot. nginx serves Concrete on port 443 with a certificate generated on your VM's first boot. Port 80 redirects to HTTPS.
  • Fail closed. nginx does not start until first boot has finished; if first boot cannot complete, the site stays offline rather than open.
  • Only the front controller runs PHP. Every page goes through Concrete's index.php. Its configuration (application/config), the command line tool, the bundled libraries (concrete/vendor), the update folder, add-on code and composer files are never served, and no other PHP file can be called. Uploaded files in application/files are served as static files only: every one with nosniff and a sandboxing content security policy, and only passive types (images, PDF, fonts, audio and video) open in the browser; anything else, SVG and HTML included, is sent as a download.
  • A rate-limited login. nginx limits sign in and registration requests to 6 a minute from one address after a burst of 10, and Concrete bans an address for ten minutes after five failed sign ins in five minutes. Both use the address of the connection, never a header a client sends.
  • Hardened defaults. The Concrete code belongs to root and is read only to PHP (only application/files and application/config are writable); PHP cannot run shell commands and is confined to Concrete's folders. MariaDB has no network listener at all (Concrete talks to it over a local socket) and its root account signs in only through the socket as the operating system's root. Sessions are kept in the database. nginx, PHP, MariaDB and the first boot service are blocked from the Azure Instance Metadata Service and the WireServer (the Azure VM agent and cloud-init keep their access), requests for a host name the VM does not serve are dropped, and Concrete always uses the address you configure (its canonical URL), never one a request claims.
  • Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.

What is on the image: Concrete CMS 9.5.7 (the official release, checksum verified, Elemental starting point), MariaDB 10.11, PHP 8.3 FPM, nginx, Ubuntu 24.04 LTS, cloud-init and the Azure Linux agent. The database, uploads and Concrete's configuration are on a separate 30 GiB Azure data disk mounted at /var/lib/concrete.

Default user: the administrator account you choose at deployment (azureuser in the examples) · Architecture: x64 (Gen2) · Recommended size: Standard_B2ms

Prerequisites

  • An Azure subscription, an SSH key pair, and a virtual network and subnet in the target region.
  • A network security group that allows TCP 22 (SSH), TCP 443 (Concrete CMS) and TCP 80 (redirects to HTTPS) from the networks that should reach them. Keep 22 to your own networks. Open 443 to your visitors for a public site, or only to your team while you build it.

Step 1: Deploy from the Azure Marketplace

In the Azure Portal choose Create a resource, search the Marketplace for Concrete CMS by cloudimg and select Create. On Basics pick your subscription, resource group, region and size (Standard_B2ms to start); under Administrator account choose SSH public key and paste your key; under Inbound port rules allow SSH (22), HTTP (80) and HTTPS (443). Keep the data disk on the Disks tab (it holds the database, uploads and configuration; you can make it larger there), then Review + create and Create.

With the Azure CLI (replace the values in angle brackets):

az vm image terms accept --urn cloudimg1647283583153:concrete-cms-ubuntu-24-04:default:latest

az vm create \
  --resource-group <your-rg> --name concrete-cms \
  --image cloudimg1647283583153:concrete-cms-ubuntu-24-04:default:latest \
  --size Standard_B2ms \
  --admin-username azureuser \
  --ssh-key-values ~/.ssh/id_ed25519.pub \
  --vnet-name <your-vnet> --subnet <your-subnet> \
  --public-ip-sku Standard

az vm open-port --resource-group <your-rg> --name concrete-cms --port 80,443 --priority 1010

Step 2: Connect to your VM

ssh azureuser@<vm-public-ip>

Step 3: Check the services

First boot takes about a minute (it creates the database and runs Concrete's installer with the sample site). Then nginx, PHP, MariaDB, the VM address reader and the first boot unit are active:

systemctl is-active nginx php8.3-fpm mariadb concrete-netinfo concrete-firstboot
active
active
active
active
active

nginx serves ports 80 and 443; PHP and MariaDB listen on local sockets only, so the only open ports are 22, 80 and 443:

sudo ss -tlnp | awk 'NR==1 || /nginx|sshd|php|maria/' | awk '{print $1, $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g'
State Local Peer
LISTEN 0.0.0.0:80 users:(("nginx"),("nginx"),("nginx"))
LISTEN 0.0.0.0:22 users:(("sshd"),("systemd"))
LISTEN 0.0.0.0:443 users:(("nginx"),("nginx"),("nginx"))
LISTEN [::]:80 users:(("nginx"),("nginx"),("nginx"))
LISTEN [::]:22 users:(("sshd"),("systemd"))
LISTEN [::]:443 users:(("nginx"),("nginx"),("nginx"))

The Concrete release, three of its bundled libraries (the Guzzle HTTP client, the Twig template engine and Concrete's own security patch set for them), the PHP and MariaDB versions and the data disk:

grep "'version' =>" /var/www/concrete/concrete/config/concrete.php | head -1
sudo jq -r '.packages[] | select(.name == "guzzlehttp/guzzle" or .name == "twig/twig" or .name == "concretecms/dependency-patches") | "\(.name) \(.version)"' /var/www/concrete/concrete/vendor/composer/installed.json
php -v | head -1
mariadb --version
findmnt -no TARGET,FSTYPE,SIZE /var/lib/concrete
    'version' => '9.5.7',
concretecms/dependency-patches 1.9.0
guzzlehttp/guzzle 7.16.0
twig/twig v3.11.3
PHP 8.3.6 (cli) (built: Oct 19 2026 15:16:58) (NTS)
mariadb  Ver 15.1 Distrib 10.11.14-MariaDB, for debian-linux-gnu (x86_64) using  EditLine wrapper
/var/lib/concrete ext4   29.4G

Twig 3.11.3 is the version Concrete 9.5.7 bundles; Concrete's patch set 1.9.0 applies the fixes for its advisories in place (including CVE-2026-46633), and the remaining Twig advisories concern Twig's sandbox, which Concrete does not use.

Terminal: the services active, the listening sockets (22, 80 and 443 only), Concrete CMS 9.5.7 with Guzzle 7.16.0 and the data disk on LUN 0

Step 4: Get your sign-in details

The credentials file holds the address Concrete is served as (your VM's public IP address by default), the sign in page, the administrator login and password, and the database credentials, all generated on this VM. Show it with:

sudo cat /root/concrete-cms-credentials.txt
CONCRETE_URL=https://<vm-public-ip>/
CONCRETE_LOGIN_URL=https://<vm-public-ip>/login
CONCRETE_ADMIN_USER=admin
CONCRETE_ADMIN_PASSWORD=<24 letters and digits>
CONCRETE_DB_NAME=concrete
CONCRETE_DB_USER=concrete
CONCRETE_DB_PASSWORD=<48 hexadecimal characters>

Terminal: the credentials file with every password redacted, its 0600 mode, the four MariaDB accounts, no network listener, and the datadir initialised on this VM's first boot

Step 5: Visit the site and sign in

Open CONCRETE_URL in your browser. The certificate is self signed (generated on your VM), so accept the browser warning once, or install your own certificate (Step 11). The Elemental sample site appears:

The Elemental sample site home page with its image slider and three feature columns

Open CONCRETE_LOGIN_URL and sign in with the user name admin and CONCRETE_ADMIN_PASSWORD:

The Concrete CMS sign in page with the User Name and Password fields

admin is Concrete's super user, the one account that can always do everything. On the first sign in Concrete offers a short survey ("Tell us a little about your site") and a "learn the basics" panel; choose Skip and Got It! to close them. With the marketplace connection off, survey answers stay on your VM. Create a separate account for each editor under Dashboard > Members > Add User and keep admin for administration.

Step 6: Edit pages in place

Signed in, every page shows the Concrete toolbar. Choose the pencil to put the page in edit mode: each area and block is outlined, and you click a block to edit it, or drag a new one from the Add panel (the plus icon). Publish Changes makes the new version live; every page keeps its full version history.

A page in edit mode with the sitewide header, the image slider and the three columns outlined as editable blocks

The Dashboard (the sliders icon at the top right) manages the whole site:

The Concrete Dashboard Welcome page with the latest form and site health tiles

Sitemap lists every page; drag pages to move them, and add new pages from a page's menu:

The Full Sitemap with the Elemental pages Services, Portfolio, Team, Blog, Search and Contact

Files > File Manager holds every uploaded image and document, with thumbnails, sets and folders:

The File Manager listing the sample site's images

Concrete's command line tool runs from /var/www/concrete as the web server user. c5:info summarises the installation:

cd /var/www/concrete && sudo -u www-data php concrete/bin/concrete c5:info | sed -n '/# Version/,/^$/p'
# Version
Installed - Yes
Core Version - 9.5.7
Version Installed - 9.5.7
Database Version - 20261020120000

Step 7: The closed-by-default settings

The image's settings are written into Concrete's own configuration store (/var/lib/concrete/config/generated_overrides), the same place the Dashboard writes, so you can change each one in the Dashboard or from the shell. The settings in effect, as Concrete reads them:

cd /var/www/concrete && for k in concrete.security.production.mode concrete.user.registration.enabled concrete.marketplace.enabled concrete.updates.skip_core concrete.updates.services.get_available_updates concrete.urls.activity_slots concrete.white_label.background_image concrete.email.enabled concrete.session.handler; do printf '%s = %s\n' "$k" "$(sudo -u www-data php concrete/bin/concrete c5:config get "$k")"; done
concrete.security.production.mode = production
concrete.user.registration.enabled = false
concrete.marketplace.enabled = false
concrete.updates.skip_core = true
concrete.updates.services.get_available_updates = 
concrete.urls.activity_slots = 
concrete.white_label.background_image = none
concrete.email.enabled = false
concrete.session.handler = database

What each switch does, and how to turn it back on:

  • Public registration is off (Dashboard > System & Settings > Login & Registration > Account Options). Create accounts under Dashboard > Members > Add User; if you open registration to the internet, require validation and a CAPTCHA first.
  • Marketplace and updates. The marketplace connection, the add-on update checks and the core update check are off, and the update service address is empty, so the server never contacts concretecms.com. Upgrade from the shell instead (Step 12). To connect the site to the marketplace, set concrete.marketplace.enabled to true and restore concrete.updates.services.get_available_updates (its default is https://www.concretecms.com/api/remote_update/update_core).
  • News, tutorials and backgrounds. The Dashboard's news and tutorial tiles (fetched by the server from concretecms.com) were removed from the Welcome page, the activity feed address is empty, and the sign in page and Dashboard use no remote background image. Add an RSS Displayer block to the Welcome page (Customize) for a feed you choose.
  • Geolocation. Concrete's geoPlugin geolocator, which sends visitors' addresses to geoplugin.net, is off (Dashboard > System & Settings > Environment > Geolocation).
  • E-mail is off, because the image has no mail server; password reset links and form notifications are not sent. Configure your SMTP server under Dashboard > System & Settings > Email > SMTP Method and then turn e-mail on. Until then, reset a forgotten password from the shell (Step 12).

For example, to set a value from the shell (-g writes it where the Dashboard does):

cd /var/www/concrete && sudo -u www-data php concrete/bin/concrete c5:config set -g concrete.email.enabled true

Concrete's own sign in protection: the failed_login rule bans an address for 600 seconds after 5 failures in 300 seconds (Dashboard > System & Settings > Permissions & Access > IP Deny List), and the geolocator is inactive:

sudo mysql -N concrete -e "SELECT iaccHandle, iaccMaxEvents, iaccTimeWindow, iaccBanDuration, iaccEnabled FROM IpAccessControlCategories; SELECT glHandle, glActive FROM Geolocators"
failed_login    5   300 600 1
forgot_password 2   30  600 1
geoplugin   0

Add-ons. The packages folder belongs to root, so PHP cannot write code into the site. To install an add-on, copy it into /var/www/concrete/packages, give it to root (sudo chown -R root:root /var/www/concrete/packages) and install it with sudo -u www-data php concrete/bin/concrete c5:package:install <handle>; an add-on is PHP code that runs with Concrete's rights, so install only ones you trust.

Step 8: Your data

Concrete's data is on its own Azure data disk at /var/lib/concrete: the MariaDB data directory (mysql, initialised on this VM's first boot), uploads (files, which the site serves as /application/files/) and Concrete's configuration (config, which it reads as application/config):

sudo ls -l /var/lib/concrete | sed -E 's/ +/ /g' | cut -d' ' -f1,3,4,9-
total
drwxr-x--- www-data www-data config
drwxr-x--- www-data www-data files
drwx------ root root lost+found
drwxr-x--- mysql mysql mysql

/var/lib/concrete/config/database.php was written by Concrete's installer on first boot and holds the database password. Keep it with your backups.

MariaDB has four accounts: the system accounts root and mysql (which sign in only through the local socket as the matching operating system user), the locked mariadb.sys, and concrete, Concrete's own user with this VM's password. It has no network listener:

sudo mysql -N -e "SELECT CONCAT(User,'@',Host) FROM mysql.global_priv ORDER BY 1; SELECT @@skip_networking, @@datadir"
concrete@localhost
mariadb.sys@localhost
mysql@localhost
root@localhost
1   /var/lib/concrete/mysql/

Back the site up with Azure disk snapshots of the data disk and the OS disk, or as files:

sudo mysqldump --single-transaction concrete | gzip > concrete-db.sql.gz
sudo tar -czf concrete-files.tar.gz -C / var/lib/concrete/files var/lib/concrete/config

Step 9: Security checks

Port 80 sends browsers to HTTPS, the health endpoint answers for load balancer probes, and the installer, registration, the configuration, the command line tool, the bundled libraries and the composer files are not reachable:

H=$(sed -n 's/^set \$cc_host "\(.*\)";$/\1/p' /etc/nginx/concrete-host.conf)
curl -s -o /dev/null -w 'port 80: %{http_code} -> %{redirect_url}\n' --resolve "$H:80:127.0.0.1" "http://$H/login"
curl -sk --resolve "$H:443:127.0.0.1" "https://$H/healthz"
for p in /install /register /application/config/database.php /concrete/bin/concrete /concrete/vendor/autoload.php /composer.json /updates/; do
  curl -sk -o /dev/null -w "$p: %{http_code}\n" --resolve "$H:443:127.0.0.1" "https://$H$p"
done
curl -s -o /dev/null -w 'unknown host name: %{http_code}\n' -H 'Host: example.com' http://127.0.0.1/ || true
port 80: 301 -> https://<vm-public-ip>/login
ok
/install: 404
/register: 404
/application/config/database.php: 404
/concrete/bin/concrete: 404
/concrete/vendor/autoload.php: 404
/composer.json: 404
/updates/: 404
unknown host name: 000

(000 means nginx closed the connection without an answer.) A file in application/files that is not a passive type, here the release's own placeholder page, is offered as a download with the sandbox policy, never shown as a page:

H=$(sed -n 's/^set \$cc_host "\(.*\)";$/\1/p' /etc/nginx/concrete-host.conf)
curl -sk -D - -o /dev/null --resolve "$H:443:127.0.0.1" "https://$H/application/files/index.html" | tr -d '\r' | grep -iE '^(HTTP|content-type|content-disposition|x-content-type|content-security)'
HTTP/2 200 
content-type: text/html
x-content-type-options: nosniff
content-security-policy: default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; sandbox
content-disposition: attachment

Terminal: port 80 redirecting to HTTPS, the installer, registration, configuration, command line tool and bundled libraries returning 404, an unknown host name dropped, and an uploaded HTML file served only as a sandboxed download

The credentials file is readable by root only, database.php by the web server only, the uploads and configuration folders by the web server, and the code (including packages) belongs to root:

sudo stat -c '%a %U:%G %n' /root/concrete-cms-credentials.txt /var/lib/concrete/config/database.php /var/lib/concrete/config /var/lib/concrete/files /var/www/concrete/packages /var/www/concrete/index.php
600 root:root /root/concrete-cms-credentials.txt
600 www-data:www-data /var/lib/concrete/config/database.php
750 www-data:www-data /var/lib/concrete/config
750 www-data:www-data /var/lib/concrete/files
755 root:root /var/www/concrete/packages
644 root:root /var/www/concrete/index.php

The certificate was generated on this VM and names its own addresses:

H=$(sed -n 's/^set \$cc_host "\(.*\)";$/\1/p' /etc/nginx/concrete-host.conf)
echo | openssl s_client -connect 127.0.0.1:443 -servername "$H" 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName
subject=CN = <vm-public-ip>, O = cloudimg
X509v3 Subject Alternative Name: 
    IP Address:<vm-public-ip>, IP Address:<vm-private-ip>, IP Address:127.0.0.1, DNS:localhost

nginx, PHP, MariaDB and the first boot service cannot reach the Azure Instance Metadata Service (169.254.169.254) or the WireServer (168.63.129.16), while the Azure VM agent and cloud-init keep their access; PHP cannot run shell commands and is confined to Concrete's folders; nginx runs only index.php, hands PHP the configured host name and the real client address from the connection (never values a client sends, and never an Authorization header), and rate-limits sign in and other requests:

for u in nginx php8.3-fpm mariadb concrete-firstboot walinuxagent; do d=$(systemctl show $u -p IPAddressDeny --value | tr ' ' '\n' | sort | xargs); echo "$u: ${d:-none}"; done
php-fpm8.3 -i 2>/dev/null | grep -E '^(open_basedir|disable_functions|expose_php) ' | cut -d' ' -f1-3
grep -E 'fastcgi_param (SCRIPT_FILENAME|HTTP_HOST|SERVER_NAME|HTTP_X_FORWARDED_FOR|HTTP_X_FORWARDED_HOST|HTTP_AUTHORIZATION) ' /etc/nginx/snippets/cloudimg-concrete-php.conf
grep -h 'limit_req_zone\|limit_req_status' /etc/nginx/conf.d/cloudimg-concrete-guard.conf
nginx: 168.63.129.16/32 169.254.169.254/32 ::ffff:168.63.129.16/128 ::ffff:169.254.169.254/128 fd00:ec2::254/128
php8.3-fpm: 168.63.129.16/32 169.254.169.254/32 ::ffff:168.63.129.16/128 ::ffff:169.254.169.254/128 fd00:ec2::254/128
mariadb: 168.63.129.16/32 169.254.169.254/32 ::ffff:168.63.129.16/128 ::ffff:169.254.169.254/128 fd00:ec2::254/128
concrete-firstboot: 168.63.129.16/32 169.254.169.254/32 ::ffff:168.63.129.16/128 ::ffff:169.254.169.254/128 fd00:ec2::254/128
walinuxagent: none
disable_functions => exec,passthru,shell_exec,system,proc_open,popen,pcntl_exec
expose_php => Off
open_basedir => /var/www/concrete/:/var/lib/concrete/files/:/var/lib/concrete/config/:/var/lib/php/sessions/:/tmp/
fastcgi_param SCRIPT_FILENAME $document_root/index.php;
fastcgi_param HTTP_HOST $cc_host;
fastcgi_param SERVER_NAME $cc_host;
fastcgi_param HTTP_X_FORWARDED_HOST "";
fastcgi_param HTTP_X_FORWARDED_FOR $remote_addr;
fastcgi_param HTTP_AUTHORIZATION "";
limit_req_zone $cc_login_key zone=cc_login:10m rate=6r/m;
limit_req_zone $cc_post_key zone=cc_post:10m rate=120r/m;
limit_req_status 429;

The cc_post limit allows 120 form submissions and editing requests a minute from one address (after a burst of the same size), which is plenty for editors; raise it in /etc/nginx/conf.d/cloudimg-concrete-guard.conf if you need to. If a burst of wrong passwords locks you out (HTTP 429, or "your IP address has been banned"), wait ten minutes, or lift the ban under Dashboard > System & Settings > Permissions & Access > IP Deny List from another address.

sudo concrete-cred-roundtrip checks the whole sign in path on the VM: the redirect to HTTPS, that no installer is offered and database.php is refused, a sign in through Concrete's login form with the password from the credentials file, and that wrong and common passwords are refused:

sudo concrete-cred-roundtrip
OK https 301 + no installer + database.php 404 + admin login + wrong and default passwords refused

Terminal: the Azure metadata block on nginx, PHP, MariaDB and first boot with the Azure agent untouched, first boot's own BLOCKED probe, PHP hardening and the sign in round trip

Step 10: Command line tools

Concrete's command line tool runs from /var/www/concrete as the web server user. A few useful commands:

cd /var/www/concrete
sudo -u www-data php concrete/bin/concrete list
sudo -u www-data php concrete/bin/concrete c5:clear-cache
sudo -u www-data php concrete/bin/concrete c5:job

Never put a password on a command line (it would be stored in your shell history and the system logs); use the helpers in Step 12.

Step 11: Your own DNS name and certificate

Point a DNS record at the VM's public IP address, then set the name Concrete is served as. This mints a new self signed certificate for the name, updates nginx, Concrete's canonical URL (used in links, redirects and password reset mails) and the addresses in /root/concrete-cms-credentials.txt, clears Concrete's cache and keeps the name across reboots:

sudo concrete-set-hostname <your-domain>

To use a certificate from your own certificate authority instead, copy it over the generated pair and reload nginx (running concrete-set-hostname again replaces it with a self signed one):

sudo install -m 0644 <your-domain>.crt /etc/nginx/ssl/concrete.crt
sudo install -m 0600 <your-domain>.key /etc/nginx/ssl/concrete.key
sudo nginx -t && sudo systemctl reload nginx

Once browsers trust the certificate you can also add an HSTS header (add_header Strict-Transport-Security "max-age=31536000" always;) to /etc/nginx/snippets/cloudimg-concrete-headers.conf. The image does not send one: browsers ignore HSTS from a site whose certificate they do not trust.

Step 12: Passwords and upgrades

concrete-set-password gives admin a new random password through Concrete's own user API, signs out every session and records the password in /root/concrete-cms-credentials.txt. Run it without arguments, or pass your own password (24 to 96 letters and digits) on stdin. Users change their own password under My Account.

sudo concrete-set-password
printf '%s\n' '<your-new-password>' | sudo concrete-set-password --stdin

Upgrades. Concrete releases are announced on concretecms.org and on GitHub. Snapshot the OS disk and the data disk, download the new release zip from https://github.com/concretecms/concretecms/releases (compare its SHA-256 with the release page), read its release notes, and replace the concrete folder as root while keeping application and packages; then run the database migrations:

python3 -m zipfile -e concrete-cms-9.5.<n>.zip /tmp
sudo systemctl stop nginx
sudo rsync -a --delete /tmp/concrete-cms-9.5.<n>/concrete/ /var/www/concrete/concrete/
sudo chown -R root:root /var/www/concrete/concrete
cd /var/www/concrete && sudo -u www-data php concrete/bin/concrete c5:update
sudo -u www-data php concrete/bin/concrete c5:clear-cache
sudo systemctl start nginx

Step 13: Server components

Component Path
Concrete code (root owned) /var/www/concrete
Configuration: database connection, generated settings /var/lib/concrete/config (application/config)
Uploads, thumbnails and Concrete's cache /var/lib/concrete/files (application/files)
Database (MariaDB datadir) /var/lib/concrete/mysql (on the same Azure data disk, LUN 0)
nginx site and request limits /etc/nginx/sites-available/cloudimg-concrete, /etc/nginx/conf.d/cloudimg-concrete-guard.conf
PHP settings /etc/php/8.3/fpm/conf.d/99-cloudimg-concrete.ini
MariaDB settings /etc/mysql/mariadb.conf.d/99-cloudimg-concrete.cnf
Certificate and key /etc/nginx/ssl/
First boot script, the VM address reader and cloudimg helpers /usr/local/sbin/concrete-firstboot.sh, /usr/local/sbin/concrete-netinfo, /usr/local/lib/cloudimg/concrete/
Helpers /usr/local/sbin/concrete-set-hostname, concrete-set-password, concrete-cred-roundtrip
Licence, release and image notes /usr/share/doc/concrete/
Credentials /root/concrete-cms-credentials.txt (mode 0600)

Step 14: Security recommendations

  • Allow port 22 in the network security group from your own networks only.
  • Keep the admin password secret and rotate it if it may have been shared (Step 12). Give each editor their own account with only the permissions they need.
  • Keep /var/lib/concrete/config secret and backed up with the database (Step 8).
  • Think before opening public registration to the internet (Step 7).
  • Use your own DNS name and a certificate your browsers trust (Step 11).
  • Keep Concrete on the newest release (Step 12) and install only add-ons you trust (Step 7).
  • Keep the operating system patched; unattended security upgrades are on, and sudo apt-get update && sudo apt-get upgrade applies everything else. MariaDB, PHP-FPM and PHP's zip extension come from Ubuntu's universe repository; sudo pro security-status --esm-apps shows whether Ubuntu Pro (ESM Apps) has security updates for them (none were pending when the image was built).
  • Snapshot the data disk regularly (Step 8).

Support and licensing

Concrete CMS is free software under the MIT licence (a copy is at /usr/share/doc/concrete/LICENSE); the bundled libraries carry their own open source licences, as listed in Concrete's composer metadata (concrete/vendor/composer/installed.json): mostly MIT and BSD, plus Apache 2.0, the SVG sanitiser under GPL 2.0 or later and htmLawed under LGPL 3.0 or GPL 2.0 or later. There is no per user or per CPU licence fee. This image installs the unmodified upstream release, without its vendored test suites and documentation, plus the cloudimg first boot, settings and helpers described above. The cloudimg charge covers the hardening, first boot setup, image maintenance and 24/7 support.

  • Email: support@cloudimg.co.uk
  • Website: https://www.cloudimg.co.uk
  • Support: 24/7, with a 24 hour response SLA