moxzi
Docs / Server / Operating moxzid

Operating moxzidalpha

How do I run this as a service: health, metrics, logs, shutdown, TLS?

Running moxzid as a service is a supervised process with a state directory, a token, and a reverse proxy in front. The constraint that decides most of the layout: moxzid does not terminate TLS and has no TLS stack in the binary, so the proxy is not optional for anything that leaves the host — the bearer token crosses the wire in plaintext without it.

Probes#

ProbeAnswersUse it for
GET /healthok as soon as the listener is boundliveness — is the process alive
GET /health/readyready, or 503 starting: <note>readiness — gate traffic on this one

The listener binds before recovery, deliberately: a server that is invisible until it is ready is indistinguishable from one that is wedged. The 503 body carries what recovery is doing (reading the manifest, recovering <name>), so a long journal replay is legible. Wait on readiness in scripts:

until [ "$(curl -sf http://127.0.0.1:7000/health/ready)" = ready ]; do sleep 0.25; done

Shutdown#

SIGTERM and SIGINT are the graceful path: the message in flight finishes, nothing new starts, every actor is checkpointed, and the process exits 0 after printing shutdown: N actor(s) checkpointed. Give it room — TimeoutStopSec=60 — because the in-flight message runs to completion; it is not preempted. A hard kill is also safe, it is merely slower to come back, since the restart replays a journal instead of reading a snapshot.

systemd#

# /etc/systemd/system/moxzid.service
[Unit]
Description=moxzid Motoko actor server
After=network-online.target

[Service]
ExecStart=/usr/local/bin/moxzid -m /etc/moxzi/actors.json -s /var/lib/moxzi -l 127.0.0.1:7000
Environment=MOXZID_TOKEN=change-me        # or an EnvironmentFile= with 0600 perms
Restart=on-failure
TimeoutStopSec=60
User=moxzi
StateDirectory=moxzi

[Install]
WantedBy=multi-user.target

Docker#

services:
  moxzid:
    build: .            # the repo's Dockerfile, or your registry's image
    ports: ["7000:7000"]
    environment:
      MOXZID_TOKEN: change-me
    volumes:
      - moxzi-state:/state
      - ./actors.json:/app/actors.json:ro
volumes:
  moxzi-state:

Inside a container moxzid binds 0.0.0.0, which is exactly why the token is not optional there: a non-loopback bind without one is refused at startup.

TLS, in a reverse proxy#

# Caddyfile — TLS, HTTP/2, automatic certificates
actors.example.com {
    reverse_proxy 127.0.0.1:7000
}

nginx is the same idea: a stock proxy_pass http://127.0.0.1:7000; block with your certificates. Rate limiting, IP allowlists and origin restrictions belong here too — none of them exist in the binary, and saying so is more useful than half-implementing them.

Metrics#

GET /metrics (with the token) serves Prometheus exposition text.

MetricTypeLabels
moxzid_uptime_secondsgauge
moxzid_http_requests_totalcounterendpoint — one bucket per endpoint class, not per URL
moxzid_actor_messages_totalcounteractor, as the caller addressed it (name or hex)
moxzid_actor_traps_totalcountersame
moxzid_actor_cyclesgaugeactor, by hex id
moxzid_actor_heap_bytesgaugeactor, by hex id
moxzid_state_bytesgauge— (only with -s; the state directory is walked on demand)
scrape_configs:
  - job_name: moxzid
    authorization: { credentials: change-me }
    static_configs: [{ targets: ["127.0.0.1:7000"] }]

Reading them: a trap spike on one actor with a flat line on the rest is that actor's bug, not the server's. A falling moxzid_actor_cycles that reaches zero produces 402s — POST /cycles/<actor> with a decimal amount refills it. Growing moxzid_actor_heap_bytes past --snapshot-below silently moves that actor onto the WAL path, which lengthens recovery.

Logs#

Server output is human prose on stderr by default. --log-json switches to one JSON line per event:

{"ts":1787342315926,"event":"job","kind":"call","actor":"9c13…","method":"add","ok":"true","ms":"18"}
EventWhen
jobevery call, query, install, upgrade, topup, cycles read, log read, list, inspect, history
readythe readiness flag flipped, with the actor count
recoveredone actor replayed, with how many messages
shutdowngraceful stop, with how many actors were checkpointed
stable-types-changedan upgrade changed the actor's stable signature, with old and new

Heartbeat and timer ticks are deliberately not logged — they fire every second forever and would bury the lines you are grepping for. Metrics jobs are excluded for the same reason. The actors' own debug output is separate: GET /logs/<actor> takes and clears it, GET /events/<actor> streams it as SSE, and --verbose forwards it to the server's stderr.

Resource limits#

All per actor, all flags.

FlagDefaultBounds
--instruction-limit40,000,000,000one message's compute, enforced with wasmtime fuel. Exhaustion traps and rolls back; the actor keeps serving. Applies to canister_init and the upgrade hooks too
--cycles100,000,000,000,000the starting allowance. 0 disables metering. A message costs 590,000 plus 4 cycles per 10 instructions; 402 when dry
--snapshot-below50 MBsnapshot-per-message threshold; larger actors ride the write-ahead log
--heartbeat-ms1000system func heartbeat cadence; 0 disables
--timer-ms100global-timer expiry checks; 0 disables. Separate from the heartbeat--heartbeat-ms 0 does not disable Timer.setTimer
--history50recent messages kept per actor, with bounded argument previews; 0 disables
--no-outcallsoffrefuses http_request host-wide, rather than failing in a way a program could mistake for the far side being down
--outcall-timeout-secs30one outcall's wall clock
body size64 MiB, or 8 MiB on the open gatewayrequest bodies; oversize is 413, checked before the body is read
MOXZI_WASM_STACK4 MiBthe guest stack, chosen to match a replica (measured: ~80,000 frames) rather than to be generous

Failure to fix#

SymptomCauseFix
503 starting: …still recoveringwait on /health/ready
401missing or wrong bearer tokensend Authorization: Bearer <token>
402the actor's allowance is dryPOST /cycles/<actor> with a decimal amount
400 … is already installed …install over an existing nameuse /upgrade/<name> — install would discard state
400 no wasm recorded for that actorbody-less upgrade of a runtime-created actorPOST the module as the body
413body over the capa smaller module, or a smaller gateway request
refuses to startnon-loopback bind with no tokenset --auth-token/MOXZID_TOKEN, or state --insecure deliberately
REJECTED <name> … at startupthat module would not loadthe other actors still serve; fix and re-install
WARNING: upgrade … CHANGES its stable-types signaturethe signature changed and no moxzi binary was found to run the compatibility checkinstall moxzi beside moxzid (or set MOXZI_BIN) and moxzid will refuse a proven-unsafe upgrade instead of warning; the IC's replica refuses these too

What this is not#

Single node: no clustering, no replication, nothing coordinating two moxzids. One shared token: no roles. No TLS in process. No certificates. Zero-downtime process replacement is not built — but upgrading an actor does not restart the server, which covers the common case. The behaviour contract is scripts/moxzid_*.sh; run them against your build rather than trusting this page.

Next#

On this pageProbesShutdownsystemdDockerTLS, in a reverse proxyMetricsLogsResource limitsFailure to fixWhat this is notNext