Skip to content

SaaS box — hosted control plane

This is the operator tutorial for the SaaS box — the server that runs Mezzanine as a hosted, multi-tenant product. Each customer gets their own isolated Mezzanine instance on a subdomain, created on demand. For running a single panel for yourself, see the App box guide instead.

What the SaaS box is

A single host running the control plane + Traefik, which together provision and route a fleet of per-tenant Mezzanine instances:

PieceRole
mezzanine-control-planeSignup, accounts, billing, superadmin/ops console, the provisioner, and tenant lifecycle
mezzanine-traefikWildcard TLS + routes *.mezzanine.cloud to the right tenant containers
mz-<slug>-frontend / mz-<slug>-backendOne pair per tenant, built from the shared mezzanine-frontend/​backend:latest images
tenant_<slug>_dataOne Docker volume per tenant (their SQLite DB + uploads) — isolated, persistent

The public entry point is the signup site (e.g. try.mezzanine.cloud), served by the control plane.

📷 Screenshot — try.mezzanine.cloud signup page

Architecture at a glance

visitor ─▶ try.mezzanine.cloud (control plane: signup/billing/ops)
│ provision
<slug>.mezzanine.cloud ─▶ Traefik ─▶ mz-<slug>-frontend ─▶ mz-<slug>-backend
tenant_<slug>_data (volume)

See the system-wide Architecture page for how this sits next to the App box.

1. The tenant signup flow

  1. A visitor hits the signup site and creates an account (email + password, plan).
  2. The control plane provisions a tenant: creates the data volume, launches mz-<slug>-frontend/backend from :latest, and registers Traefik labels so <slug>.mezzanine.cloud routes to them.
  3. The tenant lands in their own isolated Mezzanine panel — same UI as the App box.

📷 Screenshot — signup → “your instance is being provisioned” 📷 Screenshot — a tenant’s freshly provisioned panel

2. The ops / superadmin console

The control plane exposes a superadmin area to run the platform:

  • Accounts & instances — every tenant, plan, status (running / hibernated / suspended), last active.
  • Lifecycle actions — provision, suspend/resume, hibernate/wake, destroy, back up, export data.
  • Orphans — a safety sweep for containers/volumes whose tenant row is gone, with one-click cleanup.

📷 Screenshot — ops console: instances table with lifecycle actions

3. Tenant lifecycle

StateMeaningTrigger
runningContainers up, serving trafficprovision / wake / resume
hibernatedContainers stopped to save resources; data volume keptidle reaper, or manual
suspendedStopped (e.g. non-payment); resume to restoreadmin / billing
destroyedContainers and the data volume removedadmin (irreversible)

Hibernate/wake is automatic: free, idle instances are stopped after an idle window; the next request to a hibernated subdomain hits a wake fallback that starts the containers back up.

📷 Screenshot — a tenant being hibernated / woken in the ops console

4. Billing (env-gated)

Billing is wired to Razorpay and only active when its env keys are set (test or live). Plans gate quotas (instances, resources). Subscription events (created/resumed/cancelled) drive suspend/resume.

📷 Screenshot — billing/plan view

5. Custom domains & data export

  • Custom domains — a tenant can map their own domain to their instance; Traefik issues a cert via the custom-cert resolver.
  • Data export — an admin (or the tenant) can export an instance’s data for portability/backup.

📷 Screenshot — custom domain setup for a tenant

6. Shipping app updates to tenants

This is the operational heart of running the SaaS. The tenant instances run the same mezzanine-frontend/​backend app as the App box, but from this box’s own images — so panel changes must be deployed here too, not just on the App box.

Build the new images

Terminal window
# on the SaaS box, from the synced source at /opt/mezzanine
docker build -f deploy/docker/Dockerfile.backend -t mezzanine-backend:latest .
docker build -f deploy/docker/Dockerfile.frontend -t mezzanine-frontend:latest .

(The per-tenant docker-compose.yml template needs a tenant DOMAIN, so build the images directly rather than compose build.)

  • New tenants provisioned after this use the new images immediately.
  • ⚠️ Existing tenants keep their old image until their containers are recreated (wake only restarts). Recreating must preserve the tenant_<slug>_data volume.

📷 Screenshot — terminal: building the two images on the SaaS box

7. Configuration reference

The control plane reads its config from environment variables (set in the SaaS box’s .env):

VarPurpose
CP_PROVISIONon to enable real Docker provisioning
CP_TENANT_DOMAINBase domain for tenant subdomains (e.g. mezzanine.cloud)
CP_TENANT_NETWORKDocker network tenants attach to
CP_CERT_RESOLVER / CP_CUSTOM_CERT_RESOLVERTraefik cert resolvers (wildcard vs custom domains)
MZ_IMAGE_FRONTEND / MZ_IMAGE_BACKENDTenant image tags (default mezzanine-frontend/backend:latest)
CP_HOST / CP_PORTAL_URLThe signup/portal host (e.g. try.mezzanine.cloud)
Razorpay keysEnable billing when present

📷 Screenshot — redacted .env showing the CP* keys_

8. Backups & safety

  • Per-instance backups + back up all are available from lifecycle ops; back up the tenant_<slug>_data volumes regularly.
  • Account deletion purges all of a tenant’s containers + volume and aborts on failure, so deletes never silently orphan data.
  • Run the orphan sweep periodically to catch stray containers/volumes.

📷 Screenshot — ops console: backups + orphan cleanup