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.
The shape of a deployment
Section titled “The shape of a deployment”One merod process owns one home directory — config.toml, the identity
keypair, the RocksDB datastore, and the blob store — and opens two
listeners:
| Listener | Default port | Bind default | Audience |
|---|---|---|---|
| libp2p swarm | 2428 (TCP + QUIC) | 0.0.0.0 / :: (all interfaces) | Other nodes — must be reachable by peers |
| HTTP server | 2528 (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.
Run merod as a service
Section titled “Run merod as a service”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.
-
Build (or pull) the image.
Terminal window docker build -t calimero-node . -
Create a named volume so the home directory survives container replacement, and initialise the node into it once.
Terminal window docker volume create calimero-datadocker run --rm \-v calimero-data:/data \calimero-node --node node1 initThe image’s
CALIMERO_HOME=/datameans the node home is/data/node1. -
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 -
Check health from the host (the server is on loopback):
Terminal window curl -s http://127.0.0.1:2528/admin-api/healthA healthy node returns a status of
alive.
For a host install of the merod binary, supervise it with a systemd unit. This
is a sketch — adjust user, paths, and the binary location to your host.
# /etc/systemd/system/merod@.service[Unit]Description=Calimero node (%i)After=network-online.targetWants=network-online.target
[Service]Type=simpleUser=calimeroGroup=calimeroEnvironment=CALIMERO_HOME=/var/lib/calimeroExecStart=/usr/local/bin/merod --home /var/lib/calimero --node %i runRestart=on-failureRestartSec=5# Stop cleanly so RocksDB flushes and releases its lockKillSignal=SIGINTTimeoutStopSec=60
# Hardening (the home dir must stay writable)NoNewPrivileges=trueProtectSystem=strictReadWritePaths=/var/lib/calimero
[Install]WantedBy=multi-user.target-
Create the service user and home, then initialise the node once:
Terminal window sudo useradd --system --home /var/lib/calimero --shell /sbin/nologin calimerosudo -u calimero merod --home /var/lib/calimero --node node1 init -
Enable and start the instance (
%iis the node name):Terminal window sudo systemctl daemon-reloadsudo systemctl enable --now merod@node1 -
Check it is up and tail logs:
Terminal window systemctl status merod@node1journalctl -u merod@node1 -fcurl -s http://127.0.0.1:2528/admin-api/health
Persist the home directory
Section titled “Persist the home directory”Everything stateful lives under the node home. Losing it loses the node’s identity and replicated state.
| Sub-path | Section in config.toml | What it holds |
|---|---|---|
config.toml | — | Config 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.
Backups
Section titled “Backups”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.
Put the server behind a reverse proxy
Section titled “Put the server behind a reverse proxy”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 bindserver { 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; }}