Container Runtime
Run nodes from official chain images
Chains that publish an official image are container-first: the instance runs that image out of the box. No Go toolchain, no go build, no hunting for a release asset — and the node you test against is the exact artifact the network itself ships.
import { Instance } from 'starskiff';
// Boots ghcr.io/xpladev/xpla — the image XPLA publishes
const instance = Instance.xplad({
accounts: [{ mnemonic: '...', coins: '1000000000000000000000axpla' }],
});
await instance.start();Docker must be running. The image is pulled on first use and cached by Docker afterward.
This is not orchestration
starskiff does not become Kubernetes-shaped when it runs a container. The image is only where the node comes from — everything else is unchanged:
- The chain CLI (
init,keys add,gentx, …) runs in disposable containers against a host temp directory that's bind mounted as the node's home. - Genesis and config patching stays plain host-side file I/O. The container and starskiff read the same bytes.
- The node runs attached as a child process, so
stdout/stderrstreaming, the message buffer, events, and exit detection are identical to the binary runtime. - Ports are published 1:1, so
rpcUrl/apiUrl/evmUrland every cosmjs or viem client work exactly as before. stop()kills the process, force-removes the container, and deletes the temp home directory.
There is no Helm, no cluster, no pod scheduling — just docker run in place of exec.
Escape hatches
Not every chain ships an image, and sometimes you need your own build. Two ways out:
// 1. Bind your own image (a fork, a local build, a private registry)
Instance.simd({ image: 'my-registry/simd:custom' });
// 2. Use a binary on PATH — the classic runtime
Instance.simd({ binary: 'simd' });Naming a binary opts out of the container runtime entirely.
For instances with no default image (marood) these aren't overrides but a requirement: constructing one without image or binary throws.
Instance.marood({ image: 'my-registry/marood:private' }); // or { binary: 'marood' }Where the image comes from
An instance is container-first when it can source a node from an image. That image comes from one of two places:
- The chain's own official image, when it exists and tracks the live network — e.g.
xpladrunsghcr.io/xpladev/xpla. - An image starskiff builds and publishes, when the chain ships none (or a stale one) — e.g.
evmdrunsghcr.io/2wheeh/starskiff/evmd, built fromcosmos/evmsource because cosmos/evm publishes no image.
| Instance | Default runtime | Source |
|---|---|---|
simd | Container | Official ghcr.io/cosmos/simapp image (minor-line pin) |
wasmd | Container | Official cosmwasm/wasmd image, exact version tags |
xplad | Container | Official ghcr.io/xpladev/xpla image, tracks mainnet |
evmd | Container | starskiff-published image (cosmos/evm ships none) |
gaiad | Container | Official ghcr.io/cosmos/gaia image, tracks mainnet |
mantra | Container | Official ghcr.io/mantra-chain/mantrachain image |
xrplevm | Container | Official peersyst/exrp image (amd64-only) |
hermes | Binary | IBC relayer, run as a host process |
hermes is the one deliberate binary default — a relayer that drives its own CLI and config on the host, not a chain node the container runtime boots; starskiff's own CI provisions it via config/binaries.json. marood has no default at all — its node source is private — so it requires an injected image or binary and is never in that manifest. starskiff doesn't publish an image for every missing/stale upstream — only the deliberate allowlist in config/images.json (currently just evmd).
Images we publish
For chains with no usable upstream image, starskiff builds a multi-arch (amd64 + arm64) image from source and pushes it to its public GHCR namespace, ghcr.io/2wheeh/starskiff/<chain>. Public GHCR packages are free to store and pull, so this stays within the free tier.
The list of what we publish is an explicit allowlist in config/images.json — the single source of truth for both the publish workflow and the instance's default image. A dedicated publish-images workflow builds each entry; targets are never derived from the instance list, so a chain is only ever redistributed by a deliberate edit.
Building a chain image ourselves means we redistribute someone else's node, so images are pinned to an exact upstream ref + commit and (after first publish) to the multi-arch manifest digest, with build provenance and an SBOM attached.
CI
Both runtimes work in CI. The container runtime removes the binary-provisioning step entirely — no build job, no release assets to keep in sync — at the cost of an image pull per cold run. See the CI guide.
- run: pnpm vitest run # docker is preinstalled on GitHub runnersCaveats
- Docker becomes a hard dependency for container-backed instances. On a machine without a running daemon,
start()fails with an actionable error telling you to pass abinaryinstead. - First run pays the pull (a few hundred MB). The pull is part of
start(), reports progress through instance messages, obeys the start timeout, and is cached afterward. - On macOS, containers run in a VM, so filesystem I/O is slower than a native binary. Boot is still seconds, not minutes.