English · 中文
A universal “verify · launch · update” loader: one small static Rust binary that verifies a packaged app (integrity and publisher identity), launches it, supervises it, and hot-updates it. Bake it into a generic image once — switching apps is just a different manifest, never an image rebuild.
- Image:
docker.io/dotns/lode(Docker Hub) - Binaries: Linux (x86_64 / aarch64, musl-static) + macOS (x86_64 / arm64) — Releases
- Platforms: Unix only (lode is a process supervisor — PID-1 subreaper, signal forwarding,
execpassthrough).
| You are… | You want to… | Go to |
|---|---|---|
| Operator | run & keep an app updated in a container | Quick start + lode.example.toml |
| App author | make your app updatable by lode | Integration §2 — the app contract |
| Publisher | package, sign & publish a release | Integration §3 — publish versions |
| Curious | understand the design | Architecture |
The Integration guide covers the whole chain — configure (lode.toml) → run (state.json) → publish (manifest.json).
Full doc index (bilingual): docs/. Working examples:
tests/apps (a Rust + a Bun server) and tests/compose (live update/rollback).
Point lode at a signed manifest and run the generic image. By default lode reads
/srv/lode/lode.toml and keeps its state under /srv/lode:
docker run --rm \
-v "$PWD/lode.toml:/srv/lode/lode.toml:ro" \
-e LODE_TRUSTED_KEYS="<key_id>:<base64-pubkey>" \
docker.io/dotns/lode:latestA minimal lode.toml (see docs/lode.example.toml for all options):
[global]
app = "myapp"
[update]
manifest = "https://releases.example.com/myapp/manifest.json"# or: github = "owner/repo"policy = "auto"# off | check | auto
[command]
run = "./myapp"# how to launch the app (literal command, cwd = version dir)
[trust]
require_signature = "enforce"If
/srv/lode/lode.tomlis missing on first run, lode scaffolds a starter there and tells you to fill in the source. Override the base dir withLODE_DIR. No config file needed if you pass--manifest/--github(orLODE_*) instead.
To build your own app image, layer lode onto any base:
FROM oven/bun:1 # or any runtime your app needs
COPY --from=docker.io/dotns/lode:latest /usr/bin/lode /usr/bin/lode
ENTRYPOINT ["/usr/bin/lode"]generic image ┌─────────────────────────────────────┐
zzci/ubase ────► │ lode (static Rust binary) │
└───────────────────┬─────────────────┘
│ lode.toml + env + CLI
▼
[update].manifest ──HTTPS(+headers)──► manifest.json (channels → versions → assets[name])
│ (remote; never stored locally)
pick platform ──┤── download → verify sha256 + ed25519
▼
$LODE_DIR/versions/<ver> ──(atomic rename)──► current
│
▼
lode → runs `run` (supervised service: auto-update + rollback)
lode <args…> → runs `exec` + <args> (one-shot CLI passthrough)
lode is a multi-call binary. As lode it is the loader with no subcommands — arguments
are forwarded to the app. One caveat: lode parses its own flags first (--version, --help and
the LODE_* global options), so a leading app argument that matches one of them is consumed by
lode. Use lode -- <args…> to forward flag-like arguments verbatim. As lode-cli (a symlink
shipped alongside it) it is the operator/publisher toolkit.
| Invocation | Does |
|---|---|
lode | start & supervise the app ([command].run); auto-update per policy |
lode <args…> / lode -- <args…> | passthrough: run [command].exec + <args> (e.g. lode run db:init); use -- when an arg collides with a lode flag |
lode-cli status / update / rollback / restart / versions / seed | manage a running instance (via state.json); seed installs a local version offline for dev/testing |
lode-cli keygen / sign / verify / manifest / manifest-sign / init | publisher/operator tools |
lode.toml— local TOML; the operator's config (how to fetch & run). The app never writes it. →docs/lode.example.tomlstate.json— local JSON; runtime comms. lode writes status; the app writes requests (target/restart_nonce/ready). → Integration §2manifest.json— remote JSON; the signed version catalog (never stored locally). →docs/manifest.example.json
- Update
[update].policy = off | check | auto; source is eithermanifest(nativelode/v1JSON) orgithub = "owner/repo"(Releases). - Rollback — a new version that exits within
health_graceis reverted to the last known-good (single-strike). - Restart
[supervise].restart = off | on-failure | always—on-failure(default, keep-alive) retries a failing apprestart_maxtimes then pauses (lode stays alive, never crash-looping the container);offopts back into mirroring the child; lode-initiated update/rollback/restart always relaunch. - Trust —
sha256+ed25519; set[trust].trusted_keys+require_signature = off | auto | enforce. Note: verification defaults toauto(enforced only when trusted keys are configured) — setrequire_signature = "enforce"for production. Signing is the publisher's job — see Integration §3. - Private sources —
[http].headers(with${ENV}expansion) is sent on every fetch.
lode is three crates: lode-core (clap-free, signal-free — config, manifest
resolution, verified download/install, the Engine facade), lode-supervisor
(the supervise loop + readiness/stop handshakes, over lode-core), and lode
(the binary: clap + authoring). Embed either library without inheriting the CLI or
lode's process-global side effects — those are opt-in via InitOptions.
[dependencies]
lode-core = "0.2"# config + Engine (no clap, no signals)lode-supervisor = "0.2"# + the supervise loop, driven by an injected SignalSourcecargo run -p lode-core --example engine # config in code + read-only Engine
cargo run -p lode-supervisor --example embedded # host-owned signals, no subreaper/flockcargo build --profile dist --target x86_64-unknown-linux-musl # release static binary
cargo fmt --check && cargo clippy --all-targets && cargo test# gatescd tests && bun install && LODE_BIN=../target/debug/lode bun test src/ # e2eStack follows pma-rust (edition 2024, #![forbid(unsafe_code)], deny-warnings, rustls + aws-lc-rs, musl + +crt-static).
MIT