Applications Azure

Manifold on Ubuntu 24.04 on Azure User Guide

| Product: Manifold on Ubuntu 24.04 LTS on Azure

Overview

Manifold is a free, open source platform for scholarly and digital publishing. Developed by the University of Minnesota Press, the Graduate Center at CUNY and Cast Iron Coding, it transforms monographs and journals into living, annotatable digital works: readers move through a text in an interactive reader, highlight passages, and leave annotations that become part of a shared scholarly conversation, while a resource library holds the images, video and data that accompany each edition. The cloudimg image delivers Manifold 9.2.0 fully installed and configured on Ubuntu 24.04 as a single-VM Docker Compose stack, so a complete publishing platform is running within minutes of launch. Backed by 24/7 cloudimg support.

Manifold is licensed under the GNU General Public License v3.0 (GPL-3.0). All product and company names are trademarks or registered trademarks of their respective holders. This image repackages the upstream open source release with cloudimg's provisioning and support.

What is included:

  • Manifold 9.2.0, deployed from the official prebuilt v9 container images pinned to the release, so the image ships the pinned version and never silently upgrades
  • A single-host Docker Compose stack of five services: postgres:15-alpine (the database and full-text search), the Rails/Puma API, a Postgres-backed good_job worker, the Node/React server-rendered client, and nginx:1.27-alpine as the only public listener on port 80
  • PostgreSQL full-text search (no Elasticsearch) and Postgres-backed background jobs (no Redis, no Sidekiq), tuned to fit Standard_B2s (2 vCPU / 4 GiB) with no swap
  • Local-filesystem object storage on a Docker named volume (no MinIO, no S3 keys)
  • A per-VM administrator, database password and Rails secret generated on first boot and written to a root-only file, so no default or shared login ships in the image
  • manifold.service and docker.service as systemd units, enabled and active
  • 24/7 cloudimg support

The Manifold interactive reader rendering an ingested chapter of a digital edition

Prerequisites

An active Azure subscription, an SSH key pair, and a VNet plus subnet in the target region. Standard_B2s (2 vCPU / 4 GiB RAM) is the recommended starting point; scale up for larger catalogues or heavier reading traffic. NSG inbound: allow 22/tcp from your management network and 80/tcp (HTTP) from your readers. Add 443/tcp if you enable HTTPS.

Step 1 — Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Manifold 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). Then Review + create and Create.

Step 2 — Deploy from the Azure CLI

az vm create \
  --resource-group <your-rg> \
  --name manifold \
  --image <marketplace-image-urn> \
  --size Standard_B2s \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

Open HTTP to reach the platform:

az vm open-port --resource-group <your-rg> --name manifold --port 80 --priority 900

Step 3 — Connect to your VM

ssh azureuser@<vm-public-ip>

Step 4 — Confirm the stack is running

Manifold runs as a Docker Compose project at /var/lib/manifold. The manifold.service and docker.service units should both report active, all five containers should be Up, and only nginx on port 80 is exposed:

systemctl is-active manifold.service docker.service
cd /var/lib/manifold && sudo docker compose ps --format '{{.Name}}\t{{.Status}}'
curl -s -o /dev/null -w 'api /api/up -> %{http_code}\n' http://127.0.0.1/api/up
curl -s -o /dev/null -w 'client /    -> %{http_code}\n' http://127.0.0.1/

Expected: two lines of active, then the five manifold-* containers Up (with manifold-db reporting (healthy)), then two 200 responses. The API (:3011), client (:3010) and PostgreSQL (:5432) listen only on the internal Docker network; nginx on port 80 is the single exposed application port.

The Manifold stack: five containers on one host, with only nginx owning a public port

Step 5 — Retrieve your administrator password

On the first boot of every VM, a one-shot service (manifold-firstboot.service) generates every per-VM secret — the Rails secret key, the PostgreSQL password and a random administrator password — brings the stack up on a fresh empty database, creates a per-VM administrator, confirms it authenticates, and writes the credentials to a root-only file. No shared or default login ships in the image.

sudo cat /root/manifold-credentials.txt

The file (mode 0600, root only) contains the platform URL (manifold.url), the administrator email (MANIFOLD_ADMIN_EMAIL) and the administrator password (MANIFOLD_ADMIN_PASSWORD).

You can prove the sign-in round-trip from the VM's own shell. This reads the per-VM credentials and posts a real sign-in; a correct password returns a bearer token that is then accepted by the authenticated /api/v1/me endpoint:

E=$(sudo grep '^MANIFOLD_ADMIN_EMAIL=' /root/manifold-credentials.txt | cut -d= -f2-)
P=$(sudo grep '^MANIFOLD_ADMIN_PASSWORD=' /root/manifold-credentials.txt | cut -d= -f2-)
TOKEN=$(curl -s -X POST http://127.0.0.1/api/v1/tokens \
  -H 'Content-Type: application/json' \
  -d "$(jq -nc --arg e "$E" --arg p "$P" '{email:$e,password:$p}')" | jq -r '.meta.authToken')
echo "bearer token length -> ${#TOKEN}"
curl -s http://127.0.0.1/api/v1/me -H "Authorization: Bearer $TOKEN" \
  | jq '{fullName:.data.attributes.fullName, role:.data.attributes.role}'

Expected: a non-zero token length, then "role": "admin" — confirming the password in the file signs in as the administrator.

First boot generated every per-VM secret and a fresh admin, and the credential logs in

Step 6 — Sign in

Browse to http://<vm-public-ip>/ and choose Log In (top right). Enter the administrator email and password from the credentials file. Reader self-registration is available too, but a self-registered account is only ever an ordinary reader — it never grants administrator access.

The Manifold sign-in page served on first boot

Step 7 — Browse the project library

The home page presents your library of projects. Each project is a scholarly work — a monograph, an edited collection or a journal — with its own landing page describing the edition, its texts and its resources.

The signed-in Manifold project library on the home page

Open a project to reach its landing page, which shows the title, description, cover and the texts and resources that make up the edition.

A Manifold project landing page for a scholarly edition

Step 8 — Read a work in the interactive reader

Choose Read on a project's text to open Manifold's interactive reader. Readers move through the text section by section using the Contents menu, adjust typography, and — signed in — highlight any passage to annotate it or add it to a reading group, turning a static publication into a living, annotatable digital work.

The Manifold interactive reader rendering a chapter of a digital edition

Step 9 — Confirm the security model and the pinned stack

Manifold ships with no known bootstrap credential. The image carries the pulled container images and the Compose configuration, but an empty database and no admin until first boot creates the per-VM secrets. You can confirm anonymous and wrong-password access are refused while the per-VM administrator works:

E=$(sudo grep '^MANIFOLD_ADMIN_EMAIL=' /root/manifold-credentials.txt | cut -d= -f2-)
P=$(sudo grep '^MANIFOLD_ADMIN_PASSWORD=' /root/manifold-credentials.txt | cut -d= -f2-)
echo -n 'anonymous /api/v1/me -> '; curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/api/v1/me
BAD=$(curl -s -X POST http://127.0.0.1/api/v1/tokens -H 'Content-Type: application/json' \
  -d "$(jq -nc --arg e "$E" '{email:$e,password:"definitely-wrong"}')" | jq -r '.meta.authToken // "null (rejected)"')
echo "wrong-password token -> $BAD"
sudo docker port manifold-db || echo '(no host port: PostgreSQL is not published to the host)'

Expected: 401 for the anonymous request, null (rejected) for the wrong password, and no host port mapping for the database.

No known bootstrap credential: anonymous and wrong-password access are refused, the per-VM admin works

The stack is pinned to Manifold 9.2.0 and uses PostgreSQL full-text search with no Elasticsearch, Redis or MinIO, and ships with no swap so it fits Standard_B2s:

cd /var/lib/manifold
sudo docker compose images | awk 'NR>1{print $2":"$3}' | sort -u
sudo swapon --show || echo '(no swap)'
free -m

Expected: the four pinned images (manifold-api:9.2.0, manifold-client:9.2.0, nginx:1.27-alpine, postgres:15-alpine), no swap devices, and memory well within the 4 GiB of a Standard_B2s.

The pinned 9.2.0 container stack, PostgreSQL full-text search, and no swap

Publishing your own works

Manifold projects and texts are managed from the backend and the command line. As the administrator you can create a project in the backend UI, then ingest a text (Markdown, HTML, EPUB or a Google Doc) into it. From the VM you can ingest a local file straight into an existing project:

sudo docker compose -f /var/lib/manifold/docker-compose.yml exec api \
  bin/rails "manifold:project:ingest[<project-id>,/path/to/text.html]"

The ingestion runs through the Postgres-backed worker, creating a text with its sections and table of contents so it renders in the interactive reader. See the Manifold documentation for the full authoring and ingestion workflow, including EPUB and Google Docs sources.

First-boot service and security model

A one-shot manifold-firstboot.service runs after the network is up, gated so it runs exactly once. It generates the Rails secret key, the PostgreSQL password and a random administrator password (writing them mode 0600, root only, to /var/lib/manifold/.env), resolves the VM's reachable address and points the client URLs at it, brings the stack up on the fresh empty database, loads the schema and seeds, creates a per-VM administrator, confirms it authenticates, and writes /root/manifold-credentials.txt. manifold.service is ordered after first boot so the platform never starts before the per-VM secrets and administrator exist.

systemctl is-active manifold-firstboot.service
sudo stat -c '%a %U:%G %n' /root/manifold-credentials.txt /var/lib/manifold/.env

Expected: active, then 600 root:root for both files.

Enabling HTTPS with your own domain

The image serves the platform over plain HTTP on port 80. For a browser-trusted certificate, point a DNS record at the VM, open port 443 in the NSG, and install a certificate from your CA (for example Let's Encrypt). Because nginx terminates the connection in front of the stack, update DOMAIN, CLIENT_URL, CLIENT_BROWSER_API_URL and CLIENT_BROWSER_API_CABLE_URL in /var/lib/manifold/.env to your https://your-domain.example.com address, then restart:

sudo apt-get update && sudo apt-get install -y certbot
sudo systemctl restart manifold.service

Backup and maintenance

All persistent state lives in two Docker named volumes: manifold_pgdata (the PostgreSQL database, including full-text search) and manifold_uploads (the uploaded resources and generated files). Back them up together with a database dump and a volume archive:

sudo docker compose -f /var/lib/manifold/docker-compose.yml exec -T db \
  pg_dump -U postgres manifold_production | gzip > manifold-$(date +%F).sql.gz

Snapshot the OS disk from the Azure portal for a full point-in-time copy. Keep the OS current with sudo apt-get update && sudo apt-get upgrade; the image ships with unattended security updates enabled. Review the Manifold documentation before moving between releases, and always back up before an upgrade.

Support

This image is backed by 24/7 cloudimg support for deployment and initial configuration, retrieving the first-boot administrator password, publishing and ingesting texts (Markdown, HTML, EPUB and Google Docs), configuring reading groups and annotation, HTTPS and custom domain setup, performance tuning and storage administration. Email support@cloudimg.co.uk or use the live chat in the support portal; critical issues receive a one-hour average response.