Payload CMS on Ubuntu 24.04 on Azure User Guide
Overview
Payload is a modern, open-source headless CMS and application framework built on Next.js and React. It is code-first - you define your collections, fields and access control in TypeScript - and it generates a fully customisable admin panel together with a REST and GraphQL API automatically. The cloudimg image installs Payload CMS 3.85.1 on Node.js 22 LTS, builds the Next.js production app, runs it as a systemd service behind an nginx reverse proxy on port 80, stores the SQLite database and uploaded media on a dedicated Azure data disk, and generates the application secret and a unique admin user on the first boot of every VM. Backed by 24/7 cloudimg support.
What is included:
- Payload CMS 3.85.1 on Node.js 22 LTS with the Next.js admin panel pre-built
- The admin panel (
/admin) and REST + GraphQL content API published on port 80 via nginx - A dedicated Azure data disk at
/var/lib/payloadfor the SQLite database and uploaded media - A per-VM
PAYLOAD_SECRETand a unique admin user generated on first boot payload.service+nginx.serviceas systemd units, enabled and active- 24/7 cloudimg support
Prerequisites
An active Azure subscription, an SSH key pair, and a VNet + subnet in the target region. Standard_B2ms (2 vCPU / 8 GiB RAM) is a good starting point. NSG inbound: allow 22/tcp from your management network and 80/tcp for the admin panel and API (front with TLS for public exposure - see Enabling HTTPS).
Step 1 - Deploy from the Azure Marketplace
Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Payload CMS by cloudimg, and select Create. On Basics 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). Review the dedicated data disk on the Disks tab, then Review + create -> Create.
Step 2 - Deploy from the Azure CLI
az vm create \
--resource-group <your-rg> \
--name payload \
--image <marketplace-image-urn> \
--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 payload --port 80 --priority 1010
Step 3 - Connect to your VM
ssh azureuser@<vm-public-ip>
Step 4 - Confirm the services are running
systemctl is-active payload.service nginx.service
Both report active. On first boot Payload generates its secret, creates the admin user and builds a clean database.
Step 5 - Retrieve your admin credentials
The admin password is generated uniquely on the first boot of your VM and written to a root-only file:
sudo cat /root/payload-credentials.txt
This file contains payload.admin.email (admin@cloudimg.local) and payload.admin.pass. Store the password somewhere safe.
Step 6 - Check the admin panel responds
The Payload admin panel is served on port 80 and returns 200 when healthy:
curl -s -o /dev/null -w '%{http_code}\n' http://localhost/admin
Step 7 - Open the admin panel
Browse to http://<vm-public-ip>/admin and sign in with admin@cloudimg.local and the password from Step 5.

The admin dashboard gives you an overview of your collections, globals and account:

Payload generates the admin UI from the collections you define in code. The blank image ships the Users and Media collections - add your own in /opt/payload/src/collections:

The systemd services and version are shown below:

Step 8 - Authenticate against the API
Payload issues an auth token from /api/users/login. Confirm the generated credentials work:
curl -s -H 'Content-Type: application/json' --data '{"email":"admin@cloudimg.local","password":"<PAYLOAD_ADMIN_PASSWORD>"}' http://localhost/api/users/login | head -c 120; echo
A successful login returns a JSON object containing a "token" and the authenticated user.
Step 9 - Use the content API
Every collection you define is served over REST and GraphQL on the same port 80. For example, the built-in users and media collections:
# REST - list documents in a collection (auth required for protected collections)
curl http://<vm-public-ip>/api/media
# GraphQL endpoint
curl -X POST http://<vm-public-ip>/api/graphql \
-H 'Content-Type: application/json' \
--data '{"query":"{ Users { totalDocs } }"}'
Add collections and fields in your TypeScript config and Payload regenerates the admin UI and the API automatically.
Step 10 - Confirm content lives on the dedicated disk
The SQLite database and uploaded media are stored on the dedicated Azure data disk so they survive OS changes and can be resized independently:
findmnt /var/lib/payload
The mount is backed by a separate Azure data disk captured into the image and re-provisioned on every VM. The database is /var/lib/payload/payload.db and uploads are written under /var/lib/payload/uploads.
Enabling HTTPS
The nginx reverse proxy terminates plain HTTP on port 80. For public exposure, put a certificate in front of it - add a DNS name for the VM and install certbot, or use the companion cloudimg nginx-ssl-certbot image as a TLS reverse proxy. Keep Payload bound to loopback (127.0.0.1:3000) so the only public surface is the TLS-terminated proxy. Set serverURL in /opt/payload/src/payload.config.ts to your HTTPS address and restart Payload.
Maintenance
- Backups: snapshot the
/var/lib/payloaddata disk to back up the database and media. - Database: the image uses SQLite, ideal for a single server; for production scale, switch to PostgreSQL by changing the adapter in
/opt/payload/src/payload.config.tsand theDATABASE_URLin/opt/payload/.env. - Service:
sudo systemctl restart payloadafter configuration changes; logs viajournalctl -u payload. After editing collections, rebuild withsudo -u payload npm run buildin/opt/payloadthen restart. - Security patches: unattended-upgrades remains enabled so the OS continues to receive security updates automatically.
Support
cloudimg provides 24/7 expert support for this image. Contact support@cloudimg.co.uk.