Known limitations
Things DevKit does not (yet) do well, with the rationale and workarounds where applicable. This list is updated as limitations are resolved.
Instance naming
Section titled “Instance naming”--namemust be a DNS label. Names are validated against RFC 1123: 1-63 chars of lowercase[a-z0-9-], must start and end with[a-z0-9]. Uppercase, underscores, and leading/trailing hyphens are rejected. DNS-label form was chosen so the same name is safe to embed as a hostname in a future{service}.{instance}.localhostrouting model without a second translation step. Name validation is centralized so every surface enforces the same rule. Migration: instances created with an older release that still allowed uppercase or underscore names (e.g.MyStack,my_stack) must be torn down with that older binary and re-created under a DNS-label name.
Container image pinning
Section titled “Container image pinning”-
Splice container images are pulled by mutable ghcr tags, not digests. The catalogue pins the source TREE (commit SHA + post-extract ContentSHA), but Splice’s compose references every image through a single shared
IMAGE_TAGvariable (image: "${IMAGE_REPO}canton:${IMAGE_TAG}",${IMAGE_REPO}splice-app:${IMAGE_TAG}, the web UIs, …). Because one variable addresses ~6 distinct images, per-image@sha256:digests cannot be injected via the compose env — a single digest can’t pin six different images.Instead DevKit VERIFIES post-up: after services are healthy it records each running image’s content digest (image ID) in
state.json(image_digests) and, on a laterupof the SAME version, WARNs if a digest changed — i.e. a mutable ghcr tag was republished under you (restartreuses the existing containers, so no re-check happens there). This is a warning, not a gate (a digest can legitimately change if you manually re-pull), and it’s best-effort (a capture failure just skips the check). True digest-pinning at pull time would need upstream Splice to expose per-image digest variables in its compose.
Compose env reconstruction
Section titled “Compose env reconstruction”composeContextrebuilds env from registry state.down/restart/pause/cleanneed the env that was passed toup; DevKit reconstructs it fromstate.jsonso a fresh shell can still operate the instance. (logsandcredsread the registry/containers directly and need no env reconstruction.) Any new env var a future Splice release adds that is not captured in state will silently break operations from a fresh shell.
Integration testing
Section titled “Integration testing”- Integration coverage for
localnet upagainst real Splice runs nightly, not on every PR. Unit tests cover parsers and orchestration on every PR, but the end-to-end bring-up flow runs only nightly (and on PRs labeledrun-integration) via.github/workflows/integration.yml, so drift in the upstream Splice compose contract can land up to a day before CI notices.
Memory requirements
Section titled “Memory requirements”-
Splice’s full stack wants ~12 GB of Docker memory.
cluster/compose/localnet/resource-constraints.yaml(from canton-network/splice) sums to canton 4 GB + splice 3 GB + postgres 2 GB + console 2 GB + 7 UI services @ 256-512 MB (plus nginx/swagger-ui) ≈ 13 GB of limits — DevKit’s coded recommendation is 12 GB. In practice a single instance runs on 7-8 GB because most of those limits are headroom. But:- Two concurrent instances exceed 8 GB Docker → splice in one of
them gets OOM-restarted by docker, never reaches healthy, and
WaitForHealthytimes out at 25 min. - GitHub
ubuntu-latestrunners have 16 GB RAM on public repos but only 8 GB on private repos — on private-repo runnersupstarts but Splice’s onboarding may not complete; use a larger runner class or a self-hosted runner there. - Docker Desktop defaults to 50% of host memory (8 GB on a 16 GB Mac). Bump via Settings → Resources before running multi-instance scenarios.
The preflight check enforces a per-version hard floor: 8 GB for the 0.6 line (0.6.3, 0.6.4, 0.6.9/
latest, 0.6.10, 0.6.11, the V2 alpha — and any uncurated 0.6.x tag, which inherits the strictest catalogued floor for its major), 4 GB for 0.5.18 and only for tags whose major has no catalogued entry. The 12 GB figure is the coded recommendation threshold (recommended_memory_bytes) — below it preflight WARNs but does not refuse.On timeout,
WaitForHealthynow dumps the lastdocker compose pssnapshot in its error so the stuck service + state are visible without re-running anything. - Two concurrent instances exceed 8 GB Docker → splice in one of
them gets OOM-restarted by docker, never reaches healthy, and
Snapshot restore
Section titled “Snapshot restore”-
restorerequires the target instance to already exist (wasupat least once). A restore loads apg_dumpallstream into the instance’s EXISTING Postgres volume via a throwaway loader container. That volume is only a Docker-Compose-owned volume after anupcreated it. Restoring into a never-upinstance would force the loader’sdocker run -v <vol>:...to CREATE the volume out of band — producing a volume Compose does not own.docker compose down --volumes(used bylocalnet downandlocalnet remove --force) only removes volumes Compose itself created, so such an orphan volume survives teardown and is silently adopted (with a “volume … already exists but was not created by Docker Compose” warning) on the nextup. To keep the “never create a volume outside Compose” invariant, restore now refuses when the instance is not registered and tells the user to runlocalnet up <name>first. This also applies to cross-name restore: the target name must have beenuptoo. Workaround: runlocalnet up <name>(orup <newname>for a cross-name restore) beforelocalnet restore. -
The restore precondition is enforced via the registry, not a Docker volume-ownership check (known gap). The guard checks that a registry record exists for the target instance, which is a PROXY for “the Compose-owned volume exists”.
upwritesstate.jsonearly (at “creating” time, before the volume is guaranteed to exist), so a crashed/half-finishedupcan leave a registry entry with no volume — in which case restore would still proceed and the loader would re-create the volume out of band, reintroducing the orphan. The robust check is to verify the volume carries Compose’s ownership labels (com.docker.compose.project=canton-<name>andcom.docker.compose.volume=postgres) via a label-filtereddocker volume ls— a baredocker volume inspect <name>is NOT sufficient because a detacheddocker run -vvolume exists under the same name yet lacks those labels. That label-based volume-ownership check is intentionally deferred for now; the registry gate covers the common case. Tracked as a follow-up.
Platform parity
Section titled “Platform parity”- Homebrew formula targets macOS arm64 and Linux x86_64 only. Matches the release matrix. macOS Intel, Linux ARM, and Windows are intentionally out of scope.
- Windows users: use the standalone zip from GitHub Releases
rather than DPM until the Windows
.exepath through DPM is verified.
Observability: transitional dual stack
Section titled “Observability: transitional dual stack”DevKit runs a host-level shared Prometheus + Grafana stack — one stack serves every running LocalNet via file-based service discovery, refcounted by target file. See Observability for the topology.
up --observability-modeselects the sidecar stack.auto(default) serves metrics from the shared stack and skips the per-instance Prometheus + Grafana overlay when the shared stack is reachable — avoiding roughly ~600 MiB of duplicated overhead per environment.sharedforces shared-only;per-instanceforces the overlay. The choice is persisted, so a re-up preserves it.- Why the per-instance fallback is kept. The per-instance scrape
uses in-network service DNS (
canton:10013) rather thanhost.docker.internal, so it works on any platform regardless of the Linuxhost-gatewaymapping;autofalls back to it when the shared stack can’t be started. - Native-Linux validation.
scripts/e2e-observability.sh(self-hosted Linux CI) brings up a shared-mode instance and asserts the shared Prometheus scrapes it viahost.docker.internal.autoalso health-probes the shared Prometheus and falls back to the per-instance overlay when it is up but not serving. Makingsharedthe default (and dropping the overlay entirely) follows that e2e going green; until thenper-instanceis the platform-independent escape hatch.