Applications Azure

Contember 2 on Ubuntu 24.04 on Azure User Guide

| Product: Contember 2 on Ubuntu 24.04 LTS on Azure

Overview

This guide covers the deployment and configuration of Contember 2.0.8 on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. Contember is an open source (Apache-2.0), self-hosted headless CMS and content-engineering platform. You model content as TypeScript entities, Contember generates a GraphQL content API from that schema, and a React-based Admin/Studio gives editors a clean interface to manage the content. It is a strong fit for structured content, editorial workflows and content-as-a-service back ends for websites and apps.

The image runs the official upstream Contember containers, pinned by tag, under a single systemd unit via Docker Compose on a private network:

  • Enginecontember/engine:2.0.8, the Node GraphQL server exposing the /tenant, /system and /content APIs. It listens on the internal Docker network only and is published to 127.0.0.1:1481 on the host (loopback), never to the public interface.
  • PostgreSQLpostgres:16-alpine, the content datastore, on the private network only (no host port).
  • MinIOminio/minio, S3-compatible object storage for media uploads, on the private network only (no host port).
  • CLIcontember/cli:2.0.8, run on demand to apply schema migrations at first boot.

The Admin/Studio is built to static assets at image-build time and served directly by host nginx on port 80, which also reverse-proxies the engine's GraphQL endpoints — so the whole appliance is same-origin and needs no baked-in IP address.

Security model. No privileged credential ships in the image. On first boot a one-shot service mints a set of secrets unique to your VM — the privileged content-API root token, the Studio superadmin password, the engine encryption key, and the PostgreSQL and MinIO credentials — writes them 0600 to a root-only env file, and only then starts the engine and datastores. The engine's public login token is a non-secret front-end token (it authorises the login endpoint only; signing in still requires the per-VM superadmin password). The upstream demo root token (forty zeros) is never present and is rejected. After the stack is up, a post-boot service applies the content-model migration, seeds a couple of demo articles, and verifies the superadmin can sign in.

What is included:

  • Contember Engine 2.0.8, CLI 2.0.8, PostgreSQL 16 and MinIO as pinned containers under contember.service (Docker Compose)

  • The Admin/Studio built to static assets at /opt/contember/admin-dist, served by nginx on port 80 (single public surface)

  • contember-firstboot.service — mints the per-VM root token, superadmin password, encryption key and datastore secrets before the stack starts

  • contember-postboot.service — creates the MinIO bucket, applies the content migration, seeds demo content, and verifies the superadmin sign-in

  • A demo Article content type (title, content, published date) migrated and seeded, plus an Articles management page in the Studio, so a fresh instance shows real content

  • A credential round-trip prover at /usr/local/sbin/contember-roundtrip.sh

  • Ubuntu 24.04 LTS base with the latest security patches applied at build time

  • 24/7 cloudimg support with a guaranteed 24 hour response SLA

Prerequisites

  • An active Azure subscription

  • A subscription to the Contember 2 on Ubuntu 24.04 listing on Azure Marketplace

  • An SSH public key for VM authentication

  • A virtual network and subnet in the target region

Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM). The full stack (engine, PostgreSQL and MinIO) uses roughly 150 MB of container memory at idle and fits comfortably on 4 GB. Scale up for larger content sets, heavier editorial concurrency or media-heavy workloads.

Step 1: Deploy from the Azure Portal

Navigate to Marketplace in the Azure Portal, search for Contember, select the cloudimg publisher entry, and click Create.

On the Networking tab attach a network security group that allows inbound TCP 22 from your management IP range and TCP 80 from the client networks that will use the Studio. Add TLS (port 443) before managing content over the public internet — see Step 9.

Click Review + create, wait for validation, then Create. Deployment takes around two minutes.

Step 2: Deploy from the Azure CLI

RG="contember-prod"
LOCATION="eastus"
az group create --name "$RG" --location "$LOCATION"

az vm create \
  --resource-group "$RG" \
  --name contember-01 \
  --image <publisher>:<offer>:<sku>:latest \
  --size Standard_B2s \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

# Allow SSH (22) and the Studio (80) from your trusted ranges
az vm open-port --resource-group "$RG" --name contember-01 --port 22 --priority 1001
az vm open-port --resource-group "$RG" --name contember-01 --port 80 --priority 1002

Replace <publisher>:<offer>:<sku> with the URN shown on the Marketplace listing's Usage Information tab.

Step 3: Connect via SSH

ssh azureuser@<vm-public-ip>

Use the private key that matches the public key you supplied at deploy time. The login user for this image is azureuser.

Step 4: Verify the Contember Services

Confirm the three containers are healthy, the systemd units are active, and the listening sockets are as expected. The engine is published to 127.0.0.1:1481 (loopback only) and nginx on port 80 is the single customer-facing surface.

sudo docker compose --env-file /etc/contember/contember.env -f /etc/contember/compose.yaml ps
systemctl is-active docker contember nginx
sudo ss -ltn | grep -E ':80 |127.0.0.1:1481 '

Expected output:

SERVICE    IMAGE                                      STATUS
engine     contember/engine:2.0.8                     Up (healthy)
minio      minio/minio:RELEASE.2025-09-07T16-13-09Z   Up (healthy)
postgres   postgres:16-alpine                         Up (healthy)
active
active
active
LISTEN 0  511          0.0.0.0:80        0.0.0.0:*
LISTEN 0  4096       127.0.0.1:1481      0.0.0.0:*

The three Contember containers healthy, the docker, contember and nginx units active, and the engine published to loopback with nginx on port 80

Step 5: Confirm the Datastores Are Not Exposed

PostgreSQL and MinIO run on the private Docker network only — they are never published to a host port. The engine is reachable on loopback but not off the VM. Confirm:

echo "engine loopback only:"; sudo ss -ltn | grep 1481
echo "datastores on host ports (empty = good):"; sudo ss -ltn | grep -E ':5432 |:9000 |:9001 ' || echo "(none published)"

Expected output:

engine loopback only:
LISTEN 0  4096       127.0.0.1:1481      0.0.0.0:*
datastores on host ports (empty = good):
(none published)

The engine is published to loopback only and PostgreSQL and MinIO are not published to any host port

Step 6: Retrieve Your Per-VM Credentials

On the first boot of your virtual machine, contember-firstboot.service generated a privileged content-API root token, a Studio superadmin password, an engine encryption key, and the PostgreSQL and MinIO secrets unique to your instance, and contember-postboot.service migrated and seeded the demo content and verified the superadmin sign-in. Retrieve the Studio login with:

sudo cat /root/contember-credentials.txt

The file reports the Studio URL and the superadmin login:

STUDIO_URL=http://<vm-public-ip>/
ADMIN_EMAIL=admin@contember.local
ADMIN_PASSWORD=<generated-per-vm>
PROJECT=app

A shipped prover confirms the whole security model without exposing a privileged secret — it proves the Studio loads through nginx, the default forty-zero token and unauthenticated queries are rejected, the per-VM root token reads the seeded content, the superadmin signs in, and a wrong password does not:

sudo /usr/local/sbin/contember-roundtrip.sh

Expected output:

OK round-trip: studio=http://<vm-public-ip>/ admin=admin@contember.local project=app (default zero-token + unauth content rejected; per-VM root token reads content; superadmin signIn ok:true; wrong password rejected)

The per-VM credentials file (password redacted) and the credential round-trip prover reporting OK

Step 7: Understand the Access Model

The content API is gated by the engine's tokens, and the Studio is gated by the per-VM superadmin login. You can see the token model directly against the content endpoint (/content/app/live) through nginx: the upstream default forty-zero token is rejected, while your per-VM root token reads the seeded articles.

RT=$(sudo grep '^CONTEMBER_ROOT_TOKEN=' /etc/contember/contember.env | cut -d= -f2-)
echo "# default 40-zero token (rejected):"
curl -s -X POST http://127.0.0.1/content/app/live -H 'Content-Type: application/json' \
  -H "Authorization: Bearer 0000000000000000000000000000000000000000" \
  -d '{"query":"{ listArticle { title } }"}'
echo; echo "# per-VM root token (reads seeded content):"
curl -s -X POST http://127.0.0.1/content/app/live -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $RT" \
  -d '{"query":"{ listArticle { title publishedAt } }"}' | jq .

Expected output:

# default 40-zero token (rejected):
{"errors":[{"code":401,"message":"Authorization failure: API key was not found"}]}
# per-VM root token (reads seeded content):
{
  "data": {
    "listArticle": [
      { "title": "Welcome to Contember", "publishedAt": "2026-01-01T09:00:00.000Z" },
      { "title": "Getting started", "publishedAt": null }
    ]
  }
}

The content API rejects the default forty-zero token with a 401 and returns the seeded articles for the per-VM root token

Step 8: Sign In to the Studio

In a browser, go to http://<vm-public-ip>/. Contember shows its sign-in page. Enter the superadmin email (admin@contember.local) and the ADMIN_PASSWORD from Step 6:

The Contember Studio sign-in page — access is gated by the per-VM superadmin login with no anonymous access

Once signed in you land on the Studio dashboard, with the project navigation on the left:

The Contember Studio dashboard after signing in, with Dashboard and Articles navigation

Step 9: Manage Content

Click Articles in the navigation to open the demo content type. The image ships with a small Article model (a title, content and published date) and two seeded entries, so the Studio shows real content immediately. From here editors can sort, search, filter and export, and — once you extend the model — create and edit:

The Articles content grid in the Studio showing the two seeded demo articles with sortable Title, Content and Published-at columns

The same content is available over the GraphQL API for your website or app to consume. Authenticate with the per-VM root token (Step 7) and query /content/app/live, or issue mutations such as createArticle to add content programmatically.

Step 10: Define Your Own Content Types

The demo Article model is just a starting point. The content project lives on the VM at /opt/contember/project. Define entities in api/model/index.ts using Contember's schema definition, then generate and apply a migration with the bundled CLI container:

cd /opt/contember/project
sudo nano api/model/index.ts     # add or edit entities

# Generate a migration from the model change:
sudo docker compose --env-file /etc/contember/contember.env -f /etc/contember/compose.yaml \
  run --rm contember-cli migrations:diff my-change --yes

# Apply it against the running engine:
sudo docker compose --env-file /etc/contember/contember.env -f /etc/contember/compose.yaml \
  run --rm contember-cli migrations:execute --yes

To surface a new entity in the Studio, add a page under admin/app/pages/ and rebuild the admin. See the Contember documentation for the full modelling and Admin component reference.

Step 11: Put TLS in Front (Production)

The Studio is served over plain HTTP on port 80. Before managing content over the public internet, terminate TLS in front of nginx. Add a Let's Encrypt certificate to the bundled nginx:

sudo apt-get update && sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d your-domain.example.com

Point your DNS A record at the VM's public IP first, and open port 443 in the network security group. Certbot updates the bundled nginx vhost in place, preserving the reverse-proxy and static-serving setup. Because the admin calls the API on the request origin, no application change is needed after you add a domain.

Step 12: Back Up the Content

All content lives in the PostgreSQL container's postgres_data volume, and media in MinIO's minio_data volume. Take a consistent database dump through the running container:

sudo docker compose --env-file /etc/contember/contember.env -f /etc/contember/compose.yaml \
  exec -T postgres pg_dump -U contember contember | gzip > contember-backup-$(date +%F).sql.gz
ls -lh contember-backup-*.sql.gz

Store backups off the VM (for example in Azure Blob Storage). Snapshot the MinIO minio_data volume alongside the database dump if you rely on uploaded media.

Step 13: Managing the Contember Services

# Status and logs
systemctl status contember --no-pager
sudo docker compose --env-file /etc/contember/contember.env -f /etc/contember/compose.yaml logs --tail 100 engine

# Restart the stack
sudo systemctl restart contember

# nginx (the front door)
sudo nginx -t && sudo systemctl reload nginx

Architecture Summary

  • Contember Engine 2.0.8 — the Node GraphQL server (/tenant, /system, /content), published to 127.0.0.1:1481 (loopback) and reverse-proxied by nginx. The project slug is app, so the content API is /content/app/live.

  • PostgreSQL 16 and MinIO — the content datastore and media store, on the private Docker network only (no host port). Their data lives in named volumes.

  • nginx on port 80 — serves the static Admin/Studio (the login SPA at /, the app at /app/) and proxies the engine's GraphQL endpoints. Same-origin, so no per-VM IP is baked into the admin.

  • First-boot service mints the per-VM privileged root token, superadmin password, encryption key and datastore secrets before the stack starts; the public login token is a non-secret front-end token. No shared or default privileged credential ships in the image, and the default forty-zero token is rejected.

  • Post-boot service applies the content migration, seeds demo content, and verifies the superadmin sign-in, so a fresh instance is immediately usable.

Support

cloudimg provides 24/7 technical support for this Contember image by email (support@cloudimg.co.uk) and live chat, with a guaranteed 24 hour response SLA and a one hour average response time for critical issues. We help with deployment, retrieving and rotating the first-boot credentials, modelling content types and running migrations, building Admin/Studio pages, consuming the GraphQL content API, configuring MinIO media storage, adding HTTPS and a custom domain, database and media backup and restore, upgrades and patch management, and troubleshooting. For billing, subscription changes or refund requests, contact support@cloudimg.co.uk.

All product and company names are trademarks or registered trademarks of their respective holders. Use of them does not imply any affiliation with or endorsement by them.