Production Deployment
A production cluster is merod nodes, each in an Intel TDX VM booted from a
signed mero-tee image: three or more for redundancy, or one that grows
(below). Calimero runs shared clusters and dedicated ones
(Calimero Cloud); the steps
here are the same for either. Nodes run on Linux only. This page is what
you decide and configure; the flags are in the merod reference
and the file in Configuration.
The shape of a deployment
Section titled “The shape of a deployment”| Piece | What it is |
|---|---|
| Nodes | merod run in TDX mode (no --dev), one per TD. Each serves the node API on --listen and is reached at its --public-url. |
| mero-kms | Releases each node’s storage key after verifying its quote against the signed release policy. |
| Package registry | Where nodes fetch signed .mpk releases (--registry-url). Optional if every release is uploaded. |
| Backup storage | A directory (--backup-dir) that receives sealed app snapshots. |
| TLS front | Whatever terminates HTTPS for browsers in front of each node. |
| DNS | A name per node, and a wildcard name for --apps-domain if you serve app frontends. |
The image provides MERO_TEE_VERSION (the mero-tee release it is) and
MERO_TEE_PROFILE (its profile). A TDX node refuses to start without them,
because its attestation names them and it judges its peers by them.
Storage key
Section titled “Storage key”Each node takes a storage key K_r =
HKDF-SHA256(KMS key, salt calimero.cloud.storage.v1, info = --cluster), and
seals everything on disk under the cluster key K_c, which it keeps wrapped
under K_r (The cluster key).
In TDX mode the KMS key comes from mero-kms only:
merod --node n1 init ... --kms-url https://kms.internal.example.com/ \ --kms-identity /data/disk-identity --cluster eu1- mero-kms is trusted only if its own quote matches the signed release the
environment names (
MERO_KMS_RELEASE_TAG, elseMERO_KMS_VERSION, elseMERO_TEE_VERSION); the key is sealed to this TD. - The KMS derives one key per identity. All nodes of a cluster must present the
same disk identity file and the same
--cluster, so they derive the same K_r. The founding node takes its K_r as K_c; the others, and later every node of a new release (whose mero-kms gives another K_r), take K_c from a peer.merodcreates the identity file if it is missing; create it once and give every node a copy. --storage-keyand--storage-key-fileare refused without--dev: a key the host could hand the node would let whoever runs the host read the disks.
merod kms disk-key is the same client as a command, for a boot script that
needs a disk-unlock key before merod starts; it writes the key to tmpfs or
ramfs only.
Form the cluster
Section titled “Form the cluster”-
Give every founding node the same cluster settings: the same
--cluster, KMS identity,--peerslist (every founding node, asid=url),--rp-id,--originsand governance flags, and a unique--node-idand--public-url.Terminal window merod --node n1 init --node-id 1 --public-url https://n1.eu1.example.com/ \--cluster eu1 \--peers 1=https://n1.eu1.example.com/,2=https://n2.eu1.example.com/,3=https://n3.eu1.example.com/ \--kms-url https://kms.internal.example.com/ --kms-identity /data/disk-identity \--rp-id example.com --origins https://app.example.com \--ops-token-sha256 "$OPS_TOKEN_SHA256" --backup-dir /backups \--registry-url https://registry.example.com/ -
Bootstrap on one node. Add
--bootstrapto exactly one founding node’s first start. It forms the system group with--peersas voters; the others wait to be contacted. A restart with--bootstrapstill set does nothing. -
Start them all with
merod --node nX run. The first leader writes the genesis (the governance below), registers the--peersin the fleet directory and adds the first session-signing key. Nobody can sign up before genesis. -
Check it formed:
Terminal window meroctl --api https://n1.eu1.example.com/ --tee-profiles "$PROFILE" network statusmeroctl --api https://n1.eu1.example.com/ --tee-profiles "$PROFILE" \--ops-token "$OPS_TOKEN" ops statusops statusshows the system group’s voters and leader and the fleet.
Nodes added later are not in --peers: see
Add a node.
Growing a cluster
Section titled “Growing a cluster”A cluster may be bootstrapped on one node (--peers naming only itself) and
grown. While it is one node, every app gets one replica and that node is a
single point of failure. Each node registered later (POST /ops/nodes, with
its zone) joins the system group as a learner; once two learners have caught
up, the system leader makes them voters in one change, so the system group
goes from one voter to three, never two, and never past three by itself. Apps
placed while the cluster was small are spread by replica moves: mdma’s
rebalancer reads GET /ops/placement and sends one
POST /ops/apps/{app}/replicas at a time, and the app’s leader hands the new
replicas the app’s bundle and blobs before they join. To retire a node, move
its replicas away first; DELETE /ops/nodes/{id} is refused while an app
keeps a replica there. The steps are in the runbooks:
Grow from one node,
Remove a node.
Governance
Section titled “Governance”Three decisions are each given, when the cluster is formed, to either each app’s owner (the default; an organization’s owner for its apps, approving with a passkey) or the operator (for every app, with the ops token):
| Flag | The decision |
|---|---|
--release-approvals |
Which mero-tee releases may hold apps’ data (cluster upgrades). |
--app-upgrades |
Which bundle an app runs. |
--outbound-approvals |
Which URL prefixes apps may call out to. |
They are fixed at genesis, attested with every node’s receipt key, and
reported to users (meroctl attest --context), so a user can check them before
trusting the cluster. Give the operator a decision only where users trust it
with code, or destinations, that can read their data. See
Security.
Run it in a container
Section titled “Run it in a container”The repository’s Dockerfile builds an image whose entrypoint is merod, with
CALIMERO_HOME=/data and /data a volume. Every key can be an environment
variable, so a container needs no init:
docker run -d --name n1 -p 2528:2528 -v n1-data:/data \ -e MEROD_NODE_ID=1 -e MEROD_PUBLIC_URL=https://n1.eu1.example.com/ \ -e MEROD_DATA_DIR=/data/n1 -e MEROD_CLUSTER=eu1 \ -e MEROD_PEERS=1=https://n1.eu1.example.com/,2=https://n2.eu1.example.com/,3=https://n3.eu1.example.com/ \ -e MEROD_KMS_URL=https://kms.internal.example.com/ -e MEROD_KMS_IDENTITY=/data/disk-identity \ -e MEROD_OPS_TOKEN_SHA256="$OPS_TOKEN_SHA256" \ -e MERO_TEE_VERSION="$RELEASE" -e MERO_TEE_PROFILE="$PROFILE" \ merod runmerod here is the image tag (docker build -t merod .); the arguments after
it go to the merod binary.
The image EXPOSEs 2428 and 2528; only 2528 (the default --listen
port) is used.
TLS and load balancers
Section titled “TLS and load balancers”merod serves plain HTTP. What it serves is already sealed end to end to the
attested node, so a TLS terminator in front of it sees only sealed bytes, plus
/health and /attestation, which are public. You still need HTTPS in front:
- Passkeys work only in a secure context, and a page on
https://cannot call anhttp://API. Serve each node’s--public-urlover HTTPS. --public-urlmust be the URL clients and peers actually use, and it must reach that node, not any node: peers send Raft traffic to it, and the system directory records it.- One name may balance across nodes: a passkey ceremony or a confirmation
begun on one node can finish on another, which takes the challenge from the
node that issued it (
internal/challenge/take). /apps/{app}/eventsand/apps/{app}/presence/streamare server-sent event streams; turn off response buffering for them. Each node serves at most 4096 open streams.- Requests up to 64 MiB must pass (bundle and package uploads).
App frontends
Section titled “App frontends”With --apps-domain apps.example.com and a wildcard DNS name
*.apps.example.com pointing at the cluster, each installed package’s frontend
is served from its signed release at {label}.apps.example.com and at this
cluster’s {cluster_label}.apps.example.com, and each
deployment’s at its own {dlabel}.apps.example.com. TLS for the
wildcard name is the front’s job; a front shared by several clusters routes
each cluster label to its cluster. Keep --cluster stable: the cluster labels
derive from it. A custom domain a package’s manager registered as a served
origin also needs its own DNS record to the front and a certificate for its
name; GET /ops/served-origins lists them. See
Networking.
Checklist
Section titled “Checklist”- No node runs with
--dev,--storage-key,--storage-key-fileor--allow-unsigned-bundles. -
MERO_TEE_VERSIONandMERO_TEE_PROFILEcome from the image;--peer-profileslists only the profiles you run. - Every node has the same
--cluster, disk identity,--rp-id,--originsand governance flags. - Exactly one founding node had
--bootstrapon its first start. - The ops token is configured as
--ops-token-sha256, and the token itself is kept outside the machines. -
--backup-diris set and its contents leave the machine. -
--rp-idis the domain users sign in on;--originslists the HTTPS origins of your frontends. - Quotas are set for your cluster (
meroctl ops set-quotas). - Something collects
/ops/metricsfrom every node through the sealed channel; see Observability.