Skip to content
starskiff

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/stderr streaming, the message buffer, events, and exit detection are identical to the binary runtime.
  • Ports are published 1:1, so rpcUrl / apiUrl / evmUrl and 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:

  1. The chain's own official image, when it exists and tracks the live network — e.g. xplad runs ghcr.io/xpladev/xpla.
  2. An image starskiff builds and publishes, when the chain ships none (or a stale one) — e.g. evmd runs ghcr.io/2wheeh/starskiff/evmd, built from cosmos/evm source because cosmos/evm publishes no image.
InstanceDefault runtimeSource
simdContainerOfficial ghcr.io/cosmos/simapp image (minor-line pin)
wasmdContainerOfficial cosmwasm/wasmd image, exact version tags
xpladContainerOfficial ghcr.io/xpladev/xpla image, tracks mainnet
evmdContainerstarskiff-published image (cosmos/evm ships none)
gaiadContainerOfficial ghcr.io/cosmos/gaia image, tracks mainnet
mantraContainerOfficial ghcr.io/mantra-chain/mantrachain image
xrplevmContainerOfficial peersyst/exrp image (amd64-only)
hermesBinaryIBC 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 runners

Caveats

  • 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 a binary instead.
  • 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.