Skip to content

Production Deployment

This page covers running merod as a long-lived service: process supervision, which ports to publish, persisting the home directory, backups, and terminating TLS in front of the HTTP server. It builds on Install & first run, the configuration reference, and the merod reference — it does not repeat them.

One merod process owns one home directoryconfig.toml, the identity keypair, the RocksDB datastore, and the blob store — and opens two listeners:

ListenerDefault portBind defaultAudience
libp2p swarm2428 (TCP + QUIC)0.0.0.0 / :: (all interfaces)Other nodes — must be reachable by peers
HTTP server2528 (TCP)127.0.0.1 / ::1 (loopback)meroctl, apps, the admin API — keep private

The swarm listens on all interfaces by default because peers must dial it. The server listens on loopback only, so nothing reaches the admin/JSON-RPC surface off-host until you deliberately widen it or front it with a proxy.

merod run is a foreground process — it does not daemonise. Put it under a supervisor (Docker or systemd) that restarts it on crash and on reboot, and point it at a persistent home directory.

The root Dockerfile produces a runtime image whose entrypoint is merod. It sets CALIMERO_HOME=/data, declares /data as a VOLUME, exposes 2428 and 2528, and runs as a non-root user (uid 10001). Append a node name and subcommand after the image.

  1. Build (or pull) the image.

    Terminal window
    docker build -t calimero-node .
  2. Create a named volume so the home directory survives container replacement, and initialise the node into it once.

    Terminal window
    docker volume create calimero-data
    docker run --rm \
    -v calimero-data:/data \
    calimero-node --node node1 init

    The image’s CALIMERO_HOME=/data means the node home is /data/node1.

  3. Run it as a long-lived, restarting service. Publish the swarm port to the world; keep the server port on loopback so it is only reachable through a proxy on the same host (drop the 127.0.0.1: prefix only if you front it).

    Terminal window
    docker run -d --name node1 \
    --restart unless-stopped \
    -v calimero-data:/data \
    -p 2428:2428 \
    -p 2428:2428/udp \
    -p 127.0.0.1:2528:2528 \
    calimero-node --node node1 run
  4. Check health from the host (the server is on loopback):

    Terminal window
    curl -s http://127.0.0.1:2528/admin-api/health

    A healthy node returns a status of alive.

Everything stateful lives under the node home. Losing it loses the node’s identity and replicated state.

Sub-pathSection in config.tomlWhat it holds
config.tomlConfig and the identity keypair ([identity])
data/[datastore] path = "data"RocksDB datastore (groups, contexts, governance DAG)
blobs/[blobstore] path = "blobs"On-disk blob storage

The datastore and blobstore paths are relative to the node home, so a single persistent volume mounted at the home directory captures all three.

A consistent backup is a cold copy of the whole home directory taken while the node is stopped (or an atomic filesystem/volume snapshot). There is no dedicated merod backup subcommand. The full, ordered procedure — stop, copy config + datastore + blobs as one set, restart, let it re-converge — lives in the runbooks.

The HTTP server speaks plain HTTP and has no built-in TLS. For any access beyond loopback, terminate TLS at a reverse proxy (nginx, Caddy, Traefik, …) and forward to the loopback bind. The proxy is also where you enforce authentication when running the default auth_mode = "proxy".

# nginx sketch — TLS at the edge, forward to the loopback server bind
server {
listen 443 ssl;
server_name node1.example.com;
ssl_certificate /etc/letsencrypt/live/node1.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/node1.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:2528;
# WebSocket (/ws) and SSE (/sse) need upgrade + unbuffered streaming
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_buffering off;
}
}