ZenphotoCMS on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers deploying and using ZenphotoCMS 1.6.8 on Ubuntu 24.04 from cloudimg on Microsoft Azure.
ZenphotoCMS (Zenphoto) is a mature, open source photo gallery and lightweight content management system. You organise images and videos into albums and sub albums, and Zenphoto publishes them as a themed web gallery with thumbnails, EXIF and IPTC metadata, tags, search, RSS feeds and per album access control. Its Zenpage module adds pages and a news blog, and its backend manages uploads, users, themes and plugins.
The cloudimg image adds:
- HTTPS and a unique admin password from the first boot. nginx serves Zenphoto on port 443 with a certificate generated on your VM; port 80 redirects to HTTPS, and requests for a host name the VM does not serve get no answer. The
adminpassword, and Zenphoto's per install password salt (which also keys its form tokens and cookies), are generated on first boot. The password is written to/root/zenphoto-credentials.txt(readable by root only); nothing secret is baked into the image. - No web installer, ever. Zenphoto's setup wizard was completed when the image was built and then locked with Zenphoto's own protection. The web server refuses the setup pages, and the web server user cannot unlock them. A fresh Zenphoto install would give the first visitor full admin rights until an administrator exists; on this image the gallery does not start until first boot has created the administrator.
- No database password at all. Zenphoto reaches MariaDB as the system user
www-dataover the local socket, and MariaDB checks the connecting process instead of a password. MariaDB listens on 127.0.0.1 only, and the database is created on first boot, so no database from the build ships in the image. - Security fixes that are not in a release yet. ZenphotoCMS 1.6.8 is the newest release, but the Zenphoto developers have fixed several security issues since then in their code. This image applies those fixes (search sorting, the GoogleMap plugin's map page, the rating plugin and backup restore), plus a cloudimg fix for links such as
javascript:...that got through Zenphoto's description filter. It also updates the editor (TinyMCE 7.9.3), the file manager (elFinder 2.1.70) and the HTML sanitiser (htmLawed 1.2.15) to versions without known advisories. The patch is at/usr/share/doc/cloudimg/zenphoto-1.6.8-cloudimg-security.patch. - Hardened defaults. PHP runs only for Zenphoto's own scripts: the configuration, logs, backups and uploaded files are never served or executed. nginx, PHP-FPM, MariaDB and the first boot service are blocked from the Azure Instance Metadata Service and the wire server. Client addresses come from the connection, not from headers a client can forge. A browser request that changes something and comes from another web site is refused.
- One place for your data. Your photos (
albums), uploads, the image cache and the MariaDB database live under/dataon the OS disk, so they are easy to back up and to move to a data disk of your own (Step 11). - No sample content. The gallery starts empty; you add your own photos.
- Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
What is on the image: ZenphotoCMS 1.6.8 (/var/www/zenphoto), nginx, PHP 8.3 FPM, MariaDB 10.11, Ubuntu 24.04 LTS, cloud-init and the Azure Linux Agent.
Default user: the admin user you choose (examples use azureuser) · Architecture: x64, Gen2 · Recommended size: Standard_B2s
Prerequisites
- An Azure subscription and a subscription to this offer in Azure Marketplace.
- An SSH public key or a password for the VM's admin user.
- A network security group that allows TCP 443 (the gallery and its backend) and TCP 80 (redirects to HTTPS) from the people who will use it, and TCP 22 (SSH) from your own networks only. Do not open 3306; MariaDB listens on the VM only.
Step 1: Deploy the virtual machine
In Azure Marketplace search for ZenphotoCMS, select the cloudimg offer and click Create. On the Basics tab choose your subscription, resource group and region, name the VM, choose Standard_B2s or larger and set the administrator account. Under Inbound port rules allow SSH (22), HTTP (80) and HTTPS (443). On the Disks tab size the OS disk for your photos plus roughly a third again for Zenphoto's resized copies. Click Review + create, then Create.
With the Azure CLI (replace the values in angle brackets):
az vm image terms accept --urn cloudimg1647283583153:zenphoto-ubuntu-24-04:default:latest
az vm create --resource-group <your-resource-group> --name zenphoto \
--image cloudimg1647283583153:zenphoto-ubuntu-24-04:default:latest \
--size Standard_B2s --admin-username azureuser --ssh-key-values ~/.ssh/id_rsa.pub --os-disk-size-gb 128
az vm open-port --resource-group <your-resource-group> --name zenphoto --port 80,443 --priority 900
Step 2: Connect to your VM
ssh azureuser@<vm-public-ip>
Step 3: Check the services
First boot takes a few seconds. Then nginx, PHP-FPM, MariaDB and the first boot unit are active:
systemctl is-active nginx php8.3-fpm mariadb zenphoto-firstboot
active
active
active
active
nginx is the only web listener, and MariaDB listens on 127.0.0.1 only:
sudo ss -tlnp | awk 'NR==1 || /nginx|sshd|mariadb/' | awk '{print $1, $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g'
State Local Peer
LISTEN 0.0.0.0:22 users:(("sshd"),("systemd"))
LISTEN 0.0.0.0:80 users:(("nginx"),("nginx"),("nginx"))
LISTEN 0.0.0.0:443 users:(("nginx"),("nginx"),("nginx"))
LISTEN 127.0.0.1:3306 users:(("mariadbd"))
LISTEN [::]:22 users:(("sshd"),("systemd"))
LISTEN [::]:80 users:(("nginx"),("nginx"),("nginx"))
LISTEN [::]:443 users:(("nginx"),("nginx"),("nginx"))

The release and what the image carries on top of it:
cat /etc/cloudimg/zenphoto-release.txt
cloudimg: ZenphotoCMS 1.6.8 (GPL-2.0-or-later) from the upstream v1.6.8 tag; setup completed at build time and protected.
Carries: zenphoto-1.6.8-cloudimg-security.patch (sha256 210d8d9cbd0fe665c8e03824cf9751e5bd7475b180f9a374f892fe95b9f71757; upstream master d0a9edb4df, 6feaa5401f, 33ec9ab613, 0e5e3b7ada; cloudimg kses scheme-separator fix), htmLawed 1.2.15, TinyMCE 7.9.3, elFinder 2.1.70.
Step 4: Get your URL and password
The credentials file holds the address Zenphoto is served as (your VM's public IP address by default, from the Azure Instance Metadata Service), the backend address and the admin user:
sudo grep -E '^ZENPHOTO_(URL|ADMIN_URL|ADMIN_USER)=' /root/zenphoto-credentials.txt
ZENPHOTO_URL=https://20.42.94.124/
ZENPHOTO_ADMIN_URL=https://20.42.94.124/zp-core/admin.php
ZENPHOTO_ADMIN_USER=admin
The admin password, generated on this VM:
sudo grep '^ZENPHOTO_ADMIN_PASSWORD=' /root/zenphoto-credentials.txt | cut -d= -f2-
<ZENPHOTO_ADMIN_PASSWORD>
The rest of the file explains the database account (there is no database password) and the helper commands used in Steps 9 and 10.

Step 5: Open the gallery and sign in
Open ZENPHOTO_URL in your browser. The certificate is self signed (generated on your VM), so accept the browser warning once, or use your own name and certificate (Step 9). The gallery starts empty; once you add albums the front page lists them:

The images in these screenshots are generated test pictures; the image ships with an empty gallery.
Open ZENPHOTO_ADMIN_URL (or add /zp-core/admin.php to the address) and sign in as admin with the password from Step 4:

The Overview tab shows the installation (Zenphoto 1.6.8, PHP 8.3, MariaDB, the GD image library), utility functions and gallery statistics:

The notice "There is no mail handler configured" is expected: e-mail (password reset links, comment notifications) needs a mail plugin and your own mail server settings, under Plugins and Options.
Step 6: Upload photos
Choose the Upload tab, pick an existing album or type a title for a new one, then add images (GIF, JPEG, PNG, WEBP or a ZIP of images). One request can carry up to 256 MB:

For large collections, copy folders straight into the album folder over SSH; every folder becomes an album and Zenphoto picks them up the next time the gallery is opened. The album folder belongs to www-data, so copy to your home directory first and then move the files in. From your own computer, for example:
rsync -av --progress ~/Pictures/Your-Photos/ azureuser@<vm-public-ip>:Your-Photos/
Then on the VM:
sudo mv ~/Your-Photos /var/www/zenphoto/albums/
sudo chown -R www-data:www-data /var/www/zenphoto/albums/Your-Photos
Step 7: Manage albums
The Albums tab lists your albums with their thumbnails, sub album and image counts, and publish, view, refresh and delete actions. Drag them to set the order, or click one to edit its title, description, tags, thumbnail, password and publishing:

Visitors open an album to see its thumbnails:

and an image to see it full size, with next and previous links:

Users creates further accounts and sets what each may do (for example upload to their own album only), Themes switches the gallery's look, Plugins turns on optional features (comments, contact form, Zenpage pages and news, maps, user registration) and Options holds the gallery title, image sizes, watermarks, search and security settings.
Step 8: Check the security model
The image ships a self test that proves the login over HTTPS: the admin password from your credentials file signs in, and blank, whitespace, common and malformed (array) passwords are refused with Zenphoto's own error and no session:
sudo /usr/local/sbin/zenphoto-cred-roundtrip.sh
OK per-instance admin signs in over HTTPS; blank, whitespace, weak and array passwords refused without a session
Plain HTTP redirects to HTTPS; the setup pages, the configuration folder, backups and stray PHP files are refused; a sign in posted from another web site is refused; and a request for a host name the VM does not serve gets no answer:
H=$(sudo sed -nE 's#^ZENPHOTO_URL=https://([^/]+)/.*#\1#p' /root/zenphoto-credentials.txt)
curl -s -o /dev/null -w 'plain HTTP: %{http_code} -> %{redirect_url}\n' --resolve "$H:80:127.0.0.1" "http://$H/"
for p in /zp-core/setup/ /zp-core/setup.php /zp-data/zenphoto.cfg.php /backup/ /uploaded/x.php; do curl -sk -o /dev/null -w "$p: %{http_code}\n" --resolve "$H:443:127.0.0.1" "https://$H$p"; done
curl -sk -o /dev/null -w 'sign in posted from another web site: %{http_code}\n' --resolve "$H:443:127.0.0.1" -H 'Origin: https://evil.example' --data 'login=1&user=admin&pass=x' "https://$H/zp-core/admin.php"
curl -s -o /dev/null -w 'unknown host name: %{http_code}\n' -H 'Host: evil.example' http://127.0.0.1/ || true
plain HTTP: 301 -> https://20.42.94.124/
/zp-core/setup/: 404
/zp-core/setup.php: 404
/zp-data/zenphoto.cfg.php: 404
/backup/: 404
/uploaded/x.php: 404
sign in posted from another web site: 403
unknown host name: 000

The database account has no password, and the value in Zenphoto's configuration is only a placeholder Zenphoto requires (MariaDB ignores it for a socket account):
sudo mariadb -N -e "SELECT CONCAT(User,'@',Host,' ',JSON_VALUE(Priv,'\$.plugin')) FROM mysql.global_priv WHERE User='www-data'"
sudo grep -B2 "^\$conf\['mysql_pass'\]" /var/www/zenphoto/zp-data/zenphoto.cfg.php
www-data@localhost unix_socket
// cloudimg: NOT a password. MariaDB authenticates www-data by the local socket peer (unix_socket) and ignores
// this value; Zenphoto only requires the field to be non-empty. No database password exists on this instance.
$conf['mysql_pass'] = 'unix_socket-auth-no-password'; // Supply your Database password.
nginx, PHP-FPM, MariaDB and the first boot service cannot reach the Azure Instance Metadata Service or the wire server (a separate zenphoto-netinfo.service, which runs no PHP, reads the VM's addresses for the certificate), and the secrets are readable by root only:
systemctl show nginx php8.3-fpm mariadb zenphoto-firstboot -p Id -p IPAddressDeny
sudo stat -c '%a %U:%G %n' /root/zenphoto-credentials.txt /etc/ssl/zenphoto/zenphoto.key
IPAddressDeny=169.254.169.254/32 168.63.129.16/32
Id=nginx.service
IPAddressDeny=169.254.169.254/32 168.63.129.16/32
Id=php8.3-fpm.service
IPAddressDeny=169.254.169.254/32 168.63.129.16/32
Id=mariadb.service
IPAddressDeny=169.254.169.254/32 168.63.129.16/32
Id=zenphoto-firstboot.service
600 root:root /root/zenphoto-credentials.txt
600 root:root /etc/ssl/zenphoto/zenphoto.key

Step 9: Your own DNS name and certificate
Point a DNS record at the VM (a static public IP keeps the address stable), then set the name Zenphoto is served as. This mints a new self signed certificate for the name, updates nginx (which hands the name to Zenphoto, so its links and feeds use it) and the URLs in /root/zenphoto-credentials.txt, and keeps the name across reboots:
sudo zenphoto-set-hostname <your-domain>
To use a certificate from your own certificate authority instead, copy it over the generated pair and reload nginx (running zenphoto-set-hostname again replaces it with a self signed one):
sudo install -m 0644 <your-domain>.crt /etc/ssl/zenphoto/zenphoto.crt
sudo install -m 0600 <your-domain>.key /etc/ssl/zenphoto/zenphoto.key
sudo nginx -t && sudo systemctl reload nginx
Step 10: Change the password
Change your password in the backend under Users (open the admin user, type the new password twice and apply). If you lose it, run the helper on the VM: it sets a new random password, prints it once and updates /root/zenphoto-credentials.txt. Zenphoto's sign in cookie is derived from the stored password, so changing it signs that user out everywhere:
sudo zenphoto-set-password
Step 11: Storage, backups and server components
Your albums, uploads, image cache and the MariaDB data are under /data on the OS disk and bind mounted into place, so a backup of /data (and the database dump below) holds everything. To give them more room, grow the OS disk in the Azure portal and restart the VM (cloud-init grows the root file system at boot), or attach a data disk, copy /data onto it with the services stopped and mount it at /data.
Back up /data/zenphoto/albums (your originals) and the database (sudo mariadb-dump zenphoto > zenphoto.sql), or snapshot the OS disk. The image cache is rebuilt when missing. Zenphoto's own Backup/Restore utility writes database backups to /var/www/zenphoto/backup, which the web server never serves.
Running Zenphoto's setup again. Setup is locked on this image. You only need it after you upgrade Zenphoto's files yourself. Then rename the protected setup scripts back as root (cd /var/www/zenphoto/zp-core/setup && for f in *.php.xxx; do sudo mv "$f" "${f%.xxx}"; done), comment out the two lines in /etc/nginx/zenphoto-setup-lock.conf, reload nginx, run setup from the backend, and then reverse both steps. The Run setup and restore scripts buttons in the backend do not work while the lock is in place.
| Component | Path |
|---|---|
| ZenphotoCMS 1.6.8 | /var/www/zenphoto (root owned) |
| Configuration | /var/www/zenphoto/zp-data/zenphoto.cfg.php (www-data, 0640; holds no secret) |
| Data folder | /data (on the OS disk) |
| Your albums | /data/zenphoto/albums, mounted at /var/www/zenphoto/albums |
| Uploads and image cache | /data/zenphoto/uploaded, /data/zenphoto/cache |
| MariaDB data | /data/mysql, mounted at /var/lib/mysql |
| nginx site | /etc/nginx/sites-available/cloudimg-zenphoto |
| Certificate and key | /etc/ssl/zenphoto/ |
| First boot script | /usr/local/sbin/zenphoto-firstboot.sh |
| Address lookup | /usr/local/sbin/zenphoto-netinfo (zenphoto-netinfo.service) |
| Host name helper | /usr/local/sbin/zenphoto-set-hostname |
| Password helper | /usr/local/sbin/zenphoto-set-password |
| Self test | /usr/local/sbin/zenphoto-cred-roundtrip.sh |
| Credentials | /root/zenphoto-credentials.txt (mode 0600) |
| Security patch and library licences | /usr/share/doc/cloudimg/ |
Step 12: Security recommendations
- Allow port 22 in the network security group from your own networks only; open 80 and 443 only to the people who should see the gallery.
- Keep the admin password private and change it if it may have leaked (Step 10). Give other people their own users with only the rights they need.
- Use your own DNS name and a certificate your users trust (Step 9), so nobody learns to click through certificate warnings.
- Turn on optional plugins (comments, registration, the elFinder file manager, maps) only when you need them, and keep their spam protection on.
- Keep the operating system patched; unattended security upgrades are on, and
sudo apt-get update && sudo apt-get upgradeapplies everything else.
Support and licensing
ZenphotoCMS is free software under the GNU General Public License, version 2 or later (/var/www/zenphoto/LICENSE). TinyMCE is under GPL 2.0 or later, elFinder under the BSD 3 clause licence and htmLawed under LGPL 3 or GPL 2 or later; their licences are in /usr/share/doc/cloudimg/ and the Zenphoto tree. This image runs the ZenphotoCMS 1.6.8 release from https://github.com/zenphoto/zenphoto with the security changes listed above. There is no per user or per CPU licence fee. The image price covers cloudimg's hardening, first boot setup and support.
- Email: support@cloudimg.co.uk
- Website: https://www.cloudimg.co.uk
- Support: 24/7, with a 24 hour response SLA