Skip to content

Networking & Routing Roots

Every Mezzanine hostname resolves through the same chain — DNS provider → A record → server public IP → reverse proxy → app container → TLS — but the implementation of each hop differs per box. This page traces all of them.

One invariant holds everywhere: only the reverse proxy (mezzanine-nginx, mezzanine-traefik, or system nginx) publishes ports 80/443 to the host. Every app container — the Next.js frontend on 3000, the Express backend on 3001, the control plane on 4000 — is reachable only on its box’s internal Docker network. Nothing talks to the backend except the proxy in front of it.

The routing roots at a glance

Three public IPs, three reverse-proxy strategies, two certificate mechanisms.

graph LR
  subgraph "DNS providers"
    CF["Cloudflare zone mezzanine.cloud"]
    HOST["Hostinger zone myaccess.cloud"]
    WIX["Wix nameservers myaccessio.com"]
  end
  subgraph "App box 5.189.154.35"
    NGX["mezzanine-nginx (80 / 443) certbot"]
    FE1["mezzanine-frontend (3000 internal)"]
    BE1["mezzanine-backend (3001 internal)"]
    STATIC["static files in /var/www"]
  end
  subgraph "SaaS box 62.171.156.201"
    TRAEFIK["mezzanine-traefik (80 / 443) certresolver cf"]
    CP["mezzanine-control-plane (4000 internal)"]
    TEN["mz-slug-frontend (3000 internal)"]
  end
  subgraph "Development box 192.210.241.34"
    SYSNGX["system nginx (80 / 443) certbot"]
    INV["investors container (8090)"]
  end
  CF -->|"app.mezzanine.cloud"| NGX
  CF -->|"docs / www / apex"| NGX
  CF -->|"try.mezzanine.cloud"| TRAEFIK
  CF -->|"slug.mezzanine.cloud"| TRAEFIK
  HOST -->|"investors.myaccess.cloud"| SYSNGX
  WIX -.->|"not via panel"| WIX
  NGX -->|"proxy /"| FE1
  NGX -->|"proxy /api and ws"| BE1
  NGX -->|"serve"| STATIC
  TRAEFIK -->|"route by label"| TEN
  TRAEFIK -->|"signup"| CP
  SYSNGX -->|"proxy"| INV

Domain → box → proxy → SSL → DNS host

The authoritative mapping. Use it when you need to know where a hostname actually terminates.

DomainBox (public IP)Reverse proxyBacking targetSSL methodDNS host
app.mezzanine.cloudApp box (5.189.154.35)mezzanine-nginxfrontend:3000 (/) + backend:3001 (/api, Socket.IO)certbot / Let’s Encrypt (host, mounted into nginx)Cloudflare
mezzanine.cloud (apex)App box (5.189.154.35)mezzanine-nginxstatic /var/www/mezzanine.cloudcertbot / Let’s Encrypt (host, mounted into nginx)Cloudflare
www.mezzanine.cloudApp box (5.189.154.35)mezzanine-nginxstatic /var/www/mezzanine.cloudcertbot / Let’s Encrypt (host, mounted into nginx)Cloudflare
docs.mezzanine.cloudApp box (5.189.154.35)mezzanine-nginxstatic /var/www/docs.mezzanine.cloud (this site)certbot / Let’s Encrypt (host, mounted into nginx)Cloudflare
try.mezzanine.cloudSaaS box (62.171.156.201)mezzanine-traefikcontrol-plane :4000 (signup)traefik certresolver=cf (DNS-01 wildcard)Cloudflare
<tenant>.mezzanine.cloudSaaS box (62.171.156.201)mezzanine-traefikmz-<slug>-frontend:3000 (via label)traefik certresolver=cf (DNS-01 wildcard)Cloudflare
investors.myaccess.cloudDevelopment box (192.210.241.34)system nginxinvestors container :8090certbot / Let’s Encrypt (host)Hostinger

One request, end to end

A browser hitting app.mezzanine.cloud/api/... resolves through Cloudflare, terminates TLS at the App box’s nginx, and is split by path: the UI to the Next.js frontend on the internal 3000, the API and the Socket.IO upgrade to the Express backend on the internal 3001. Neither app port is published to the host — nginx is the only way in.

sequenceDiagram
  participant U as "Browser"
  participant D as "Cloudflare DNS"
  participant N as "mezzanine-nginx (443)"
  participant F as "frontend (3000 internal)"
  participant B as "backend (3001 internal)"
  U->>D: "resolve app.mezzanine.cloud"
  D-->>U: "A record 5.189.154.35"
  U->>N: "HTTPS GET (TLS via certbot cert)"
  alt "path is slash api or websocket"
    N->>B: "proxy on docker net to 3001"
    B-->>N: "JSON or Socket.IO upgrade"
  else "any other path"
    N->>F: "proxy on docker net to 3000"
    F-->>N: "rendered HTML or assets"
  end
  N-->>U: "HTTPS response"

The SaaS path is the same shape with two differences: DNS still resolves at Cloudflare, but to 62.171.156.201, and TLS terminates at traefik, which reads the Host header and matches the per-tenant router rule Host(\.mezzanine.cloud`)to forward to that tenant'smz--frontend:3000(and from there to itsmz--backend, again only on the internal mezzanine-tenantsnetwork).try.mezzanine.cloudfollows the same traefik entry but routes to the control plane on:4000`.

How traefik knows where a tenant lives

On the SaaS box there is no per-host nginx vhost file. Routing is entirely label-driven: when the control-plane provisioner creates a tenant, it stamps the tenant’s frontend container with labels that traefik reads live.

graph TD
  PROV["control-plane provisioner"] -->|"creates container with labels"| LBL["mz-slug-frontend labels"]
  LBL --> R1["traefik.enable=true"]
  LBL --> R2["router rule Host slug.mezzanine.cloud"]
  LBL --> R3["entrypoint websecure"]
  LBL --> R4["tls.certresolver=cf"]
  LBL --> R5["loadbalancer.server.port=3000"]
  LBL --> R6["mezzanine.tenant=slug"]
  R1 --> T["mezzanine-traefik picks up route"]
  R2 --> T
  R3 --> T
  R4 --> T
  R5 --> T
  T -->|"serves slug.mezzanine.cloud"| OUT["tenant frontend 3000"]

Cross-references