SaaS Box Architecture
The SaaS box (public IP 62.171.156.201) runs Mezzanine as a hosted, multi-tenant product. Instead of a single hand-configured nginx like the App box, it uses Traefik for wildcard TLS and dynamic, label-driven routing, a control plane that provisions and manages tenants, and one isolated frontend/backend container pair per tenant. Everything lives on a single Docker bridge network, mezzanine-tenants.
Topology
The diagram below shows the two long-lived platform containers (Traefik and the control plane) and two example tenants. Traefik is the only container that publishes ports to the host; every other container is reachable only on the mezzanine-tenants network. Routing rules come from labels on each tenant’s frontend container, so Traefik never needs a config reload when a tenant is added or removed.
graph TD
Internet["Internet to 80 and 443"] --> Traefik["mezzanine-traefik (traefik:v3) publishes 80 and 443"]
subgraph net["Docker network: mezzanine-tenants (bridge)"]
Traefik -->|"Host try.mezzanine.cloud"| CP["mezzanine-control-plane (4000)"]
Traefik -->|"Host mya.mezzanine.cloud via label"| F1["mz-mya-frontend (3000 internal)"]
Traefik -->|"Host satwik.mezzanine.cloud via label"| F2["mz-satwik-frontend (3000 internal)"]
F1 -->|"api and websockets"| B1["mz-mya-backend (3001 internal)"]
F2 -->|"api and websockets"| B2["mz-satwik-backend (3001 internal)"]
CP -. "provisions and controls lifecycle" .-> F1
CP -. "provisions and controls lifecycle" .-> F2
end
B1 --> V1[("tenant_mya_data")]
B2 --> V2[("tenant_satwik_data")]
CP --> VCP[("mezzanine_cp_data")]
Traefik --> VACME[("saas_traefik_acme")]
Key points:
mezzanine-traefik(traefik:v3) is the only container that publishes ports to the host:80and443. It terminates TLS using a wildcard certificate for*.mezzanine.cloud, obtained via the Cloudflare DNS-01 challenge (cert resolvercf) and persisted in thesaas_traefik_acmevolume.mezzanine-control-plane(mezzanine-control-plane:latest, port4000) is the brain of the box. It handles signup, accounts, billing (Razorpay, env-gated), the superadmin/ops console, the provisioner, and the tenant lifecycle state machine.try.mezzanine.cloudroutes to it — that is the public signup site.- Each tenant gets a pair:
mz-<slug>-frontend(mezzanine-frontend:latest, listens on3000) andmz-<slug>-backend(mezzanine-backend:latest, the Express + better-sqlite3 + Socket.IO API on3001). The frontend proxies/apiand websockets to its own backend over themezzanine-tenantsnetwork — neither3000nor3001is published to the host. This is exactly how the App box wires frontend to backend; the only difference is multi-tenancy is achieved by running one stack per tenant rather than one stack for everyone. - The current tenants are
mya,satwik, andviharikakallamyaccessiocom— all currently hibernated.
Routing is driven by container labels
Traefik does not have a static vhost file. Routing for a tenant is declared as labels on that tenant’s frontend container, so the provisioner can stand up or tear down a route just by creating or removing a container. This is the exact label set the provisioner applies to each mz-<slug>-frontend:
| Label | Value | Purpose |
|---|---|---|
traefik.enable | true | Opt this container into Traefik routing |
| router rule | Host rule matching <slug>.mezzanine.cloud | Match the tenant’s subdomain |
| entrypoint | websecure | Bind the route to the HTTPS (443) entrypoint |
tls.certresolver | cf | Issue/renew TLS via Cloudflare DNS-01 |
loadbalancer.server.port | 3000 | Send traffic to the frontend’s internal port |
mezzanine.tenant | <slug> | Tenant marker used by the control plane / ops tooling |
The signup site is the one exception: try.mezzanine.cloud is routed by Traefik directly to the control plane on port 4000, not to any tenant pair.
Volumes
| Volume | Owner | Contents |
|---|---|---|
mezzanine_cp_data | control plane | Control-plane database (accounts, tenants, billing state) |
saas_cp_backups | control plane | Tenant backups |
saas_traefik_acme | Traefik | Let’s Encrypt / ACME TLS certificates (*.mezzanine.cloud) |
saas_traefik_logs | Traefik | Traefik access/runtime logs |
tenant_<slug>_data | tenant backend | That tenant’s SQLite database + uploads (one per tenant) |
Each tenant’s data lives entirely in its own tenant_<slug>_data volume. This is what makes hibernation safe: stopping a tenant’s containers never touches its data.
Tenant lifecycle
The control plane drives each tenant through a state machine. Idle tenants hibernate — their containers are stopped but their data volume is kept — and wake on the next request. This keeps the box’s resource usage proportional to active tenants rather than total tenants.
stateDiagram-v2 [*] --> provisioning: signup or ops create provisioning --> running: containers created and started running --> hibernated: idle timeout, containers stopped and volume kept hibernated --> running: wake on next request restarts containers running --> suspended: ops or billing hold suspended --> running: reinstated hibernated --> suspended: ops or billing hold running --> destroyed: teardown removes containers and volume suspended --> destroyed: teardown removes containers and volume destroyed --> [*]
- provisioning — the provisioner uses the current
:latestimages, creates themz-<slug>-frontendandmz-<slug>-backendcontainers (applying the Traefik label set above), creates thetenant_<slug>_datavolume, and starts them. - running — both containers up; Traefik is routing
<slug>.mezzanine.cloudto the frontend. - hibernated — containers stopped after idle,
tenant_<slug>_dataretained. No compute cost, data intact. - wake — the next request triggers a container start on the stopped pair, returning the tenant to running.
- suspended — an ops or billing hold; the tenant is intentionally kept offline regardless of traffic.
- destroyed — teardown: containers and the data volume are removed. Terminal.
Contrast with the App box
SaaS box (62.171.156.201) | App box (5.189.154.35) | |
|---|---|---|
| Edge / TLS | mezzanine-traefik, wildcard *.mezzanine.cloud, Cloudflare DNS-01 (cf) | mezzanine-nginx, certbot/Let’s Encrypt on the host |
| Routing config | Dynamic, from container labels | Static nginx vhosts |
| Tenancy | Many isolated stacks (one pair per tenant) | Single stack (MYACCESS’s own panel) |
| Network | mezzanine-tenants | docker_default |
| Lifecycle | provision / hibernate / wake / suspend / destroy | always-on |
See also
- SaaS operator guide — day-to-day provisioning, waking, recreating, and tearing down tenants.
- App box guide — the single-tenant panel architecture.
- Architecture overview — how all three boxes and the DNS providers fit together.