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#
| Probe | Answers | Use it for |
|---|---|---|
GET /health | ok as soon as the listener is bound | liveness — is the process alive |
GET /health/ready | ready, 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.
| Metric | Type | Labels |
|---|---|---|
moxzid_uptime_seconds | gauge | — |
moxzid_http_requests_total | counter | endpoint — one bucket per endpoint class, not per URL |
moxzid_actor_messages_total | counter | actor, as the caller addressed it (name or hex) |
moxzid_actor_traps_total | counter | same |
moxzid_actor_cycles | gauge | actor, by hex id |
moxzid_actor_heap_bytes | gauge | actor, by hex id |
moxzid_state_bytes | gauge | — (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"}
| Event | When |
|---|---|
job | every call, query, install, upgrade, topup, cycles read, log read, list, inspect, history |
ready | the readiness flag flipped, with the actor count |
recovered | one actor replayed, with how many messages |
shutdown | graceful stop, with how many actors were checkpointed |
stable-types-changed | an 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.
| Flag | Default | Bounds |
|---|---|---|
--instruction-limit | 40,000,000,000 | one 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 |
--cycles | 100,000,000,000,000 | the starting allowance. 0 disables metering. A message costs 590,000 plus 4 cycles per 10 instructions; 402 when dry |
--snapshot-below | 50 MB | snapshot-per-message threshold; larger actors ride the write-ahead log |
--heartbeat-ms | 1000 | system func heartbeat cadence; 0 disables |
--timer-ms | 100 | global-timer expiry checks; 0 disables. Separate from the heartbeat — --heartbeat-ms 0 does not disable Timer.setTimer |
--history | 50 | recent messages kept per actor, with bounded argument previews; 0 disables |
--no-outcalls | off | refuses http_request host-wide, rather than failing in a way a program could mistake for the far side being down |
--outcall-timeout-secs | 30 | one outcall's wall clock |
| body size | 64 MiB, or 8 MiB on the open gateway | request bodies; oversize is 413, checked before the body is read |
MOXZI_WASM_STACK | 4 MiB | the guest stack, chosen to match a replica (measured: ~80,000 frames) rather than to be generous |
Failure to fix#
| Symptom | Cause | Fix |
|---|---|---|
503 starting: … | still recovering | wait on /health/ready |
401 | missing or wrong bearer token | send Authorization: Bearer <token> |
402 | the actor's allowance is dry | POST /cycles/<actor> with a decimal amount |
400 … is already installed … | install over an existing name | use /upgrade/<name> — install would discard state |
400 no wasm recorded for that actor | body-less upgrade of a runtime-created actor | POST the module as the body |
413 | body over the cap | a smaller module, or a smaller gateway request |
| refuses to start | non-loopback bind with no token | set --auth-token/MOXZID_TOKEN, or state --insecure deliberately |
REJECTED <name> … at startup | that module would not load | the other actors still serve; fix and re-install |
WARNING: upgrade … CHANGES its stable-types signature | the signature changed and no moxzi binary was found to run the compatibility check | install 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#
- Persistence and durability — checkpoints, replay, and the backup recipe.
- Authentication — the token, the open routes, and what must never be exposed.
- The HTTP API — every endpoint with its request and response shape.