Troubleshooting¶
Every entry here was a real failure hit while building rightsize itself, not a hypothetical. Where a fix already lives inside rightsize (most of them), the entry tells you that so you know it's not something you need to work around yourself — these are included so you recognize the symptom instantly if you ever see it (e.g. while debugging your own image against the msb CLI directly) rather than re-discovering the cause from scratch.
Container never starts / image ENTRYPOINT seems to do nothing¶
Symptom: A container reports as created but the workload inside never actually
runs — msb status shows the image's default command as metadata, but nothing is
listening, nothing is logging.
Cause: microsandbox's detached mode (msb run -d) boots the VM with only its init
process — it does not start the image's own ENTRYPOINT/CMD at all on msb 0.6.2.
Fix: Not something you need to fix yourself — rightsize's microsandbox backend
runs every sandbox in attached mode instead, holding a supervising child process
per container so the ENTRYPOINT runs exactly as it would under Docker. If you're
driving msb directly outside of rightsize and hit this, that's the fix: drop -d.
msb exec hangs forever¶
Symptom: Any execInContainer(...) call (or a raw msb exec invocation) never
returns.
Cause: msb exec blocks until its stdin hits EOF — a subprocess pipe left open
(the default for most process-spawning APIs) keeps it waiting forever.
Fix: Handled inside rightsize — the backend closes the child's stdin immediately
after spawning. If you're shelling out to msb exec yourself, make sure you close or
redirect-from-/dev/null the child's stdin rather than leaving a pipe open.
followOutput's last line arrives after the container reports stopped¶
Symptom: A followOutput subscriber on the microsandbox backend sees its final
log line delivered slightly after the container itself reports stop() complete,
rather than exactly at stream close.
Cause: msb logs -f never exits once its sandbox stops (a documented gap in msb
0.6.2) — it blocks on read forever rather than hitting EOF.
Fix: Already handled — the microsandbox backend runs a watchdog that, once the sandbox is confirmed stopped, quiesces the stuck follow process and replays only the not-yet-delivered tail exactly once. The output itself is still correctly ordered with no duplicates on either backend; this is a timing nuance in when the last line arrives relative to stop, not a correctness issue. See Backends for the summary.
Guest-side write to a "read-only" mount succeeds when it shouldn't¶
Symptom: A test asserts that a write to a file mounted with readOnly = true
fails inside the container, and the assertion fails — the write went through.
Cause: FileMount.readOnly is not enforced in-guest on the microsandbox backend
(msb 0.6.2). Docker enforces it correctly.
Fix: Don't write a test that depends on guest-side write protection when running
under RIGHTSIZE_BACKEND=microsandbox. If your test's correctness genuinely depends
on this, either verify it only under RIGHTSIZE_BACKEND=docker, or add your own
in-test guard that doesn't rely on the guest OS enforcing the flag. See
Files & Memory.
msb ls output looks different than you expected¶
Symptom: Code (or a script) parsing msb ls output breaks, or --json isn't
recognized.
Cause: msb ls --format json is the correct flag — there is no --json flag
on ls itself (that spelling exists on msb logs, not ls). The JSON shape is a flat
array of objects with keys created_at, image, name, status, and status is
capitalized (e.g. "Running").
Fix: Use msb ls --format json, not msb ls --json. rightsize's own parsing is
tolerant of key order (a brace-scanner, not a fixed-order deserializer) for exactly
this kind of drift-proofing; if you're parsing msb ls output yourself, don't assume
key order either.
A JVM-based image fails to launch only on the microsandbox backend¶
Symptom: A container boots fine under RIGHTSIZE_BACKEND=docker but fails to
launch (or the JVM aborts with an insufficient-memory error) under
RIGHTSIZE_BACKEND=microsandbox.
Cause: microsandbox's default microVM has roughly ~450 MB of guest-available RAM. Images that compute fixed memory regions above this at startup — Paketo-buildpack JVM images in particular, which size heap/metaspace/thread-stack regions ahead of time from image metadata — fail to launch when that computed size exceeds the default.
Fix: Call .withMemoryLimit(megabytes) on the container, sized with real headroom
above the image's actual requirement — don't just raise it until it barely stops
OOM-killing; see Files & Memory
for the full story, including a case (Pinot) where "just enough to
avoid the OOM killer" (3072 MB) still caused intermittent RPC timeouts under memory
pressure, and 4096 MB was the number that actually left headroom.
An image pull is slow, rate-limited, or fails intermittently¶
Symptom: Pulling a particular image (Redpanda's in particular) is slow, gets rate-limited, or fails in CI specifically.
Cause: Registries rate-limit anonymous pulls. Separately, microsandbox pulls single-arch image layers (matching your host architecture) rather than a multi-arch manifest — this is generally a benefit (smaller pulls; an ArangoDB pull was measured at ~203 MB single-arch vs. an estimated ~600 MB multi-arch), but it doesn't help if the registry itself is rate-limiting you.
Fix: For a rate-limited or otherwise hard-to-pull image, seed it into the msb
cache ahead of time: docker save <image> -o /tmp/img.tar && msb load -i /tmp/img.tar -t <image> (this is exactly how
Redpanda-on-msb was finally verified during rightsize's own development). In CI, cache
~/.microsandbox/cache/ between runs the same way you'd cache any other dependency
directory.
A control-character panic (InvalidAscii) on container boot¶
Symptom: A container fails to boot under the microsandbox backend with an
InvalidAscii-style panic from the krun VMM, before the guest workload ever starts —
reproduced even with zero rightsize-set environment variables.
Cause: microsandbox 0.6.2's krun VMM rejects environment variable values
containing control characters. The official postgres:*-alpine image is a known
example: it bakes DOCKER_PG_LLVM_DEPS with a literal tab character (from a
Dockerfile-internal package list built with \t\t continuation).
Fix: Already handled for PostgreSQLContainer — it overrides the offending
variable to an empty string, which is a no-op on Docker and the fix on microsandbox.
If you hit this with an image rightsize doesn't ship a module for, look for a baked env
var with an unusual byte in it (check via docker inspect <image>) and override it the
same way; there's no general-purpose sanitizer built into the backend yet, so this is
handled per-image.
Sandboxes are left behind after a crashed or SIGKILLed process¶
Symptom: msb ls (or docker ps) shows containers named rz-<hex>-<n> still running,
with no rightsize process left that could stop them — usually after a test process was
SIGKILLed, OOM-killed, or a CI step crashed mid-run.
Cause: Normal lifecycle (stop(), the @Sandboxed extension, the shutdown hook) only runs
while the owning process is alive. A process that dies without running it leaves its sandboxes
with nothing left to stop them.
Fix: Already handled, on a short delay rather than instantly — rightsize's reaper reaps a
crashed run in one of two ways: a per-run watchdog process reacts within seconds of the crash
(unless RIGHTSIZE_REAPER=sweep or off), and failing that, the next rightsize process to
start sweeps any dead run it finds in the shared cache dir before creating its own first
sandbox. If you're seeing leftovers survive both — e.g. RIGHTSIZE_REAPER=off was set, or no
further rightsize process has run since the crash — either unset RIGHTSIZE_REAPER (or set it
to at least sweep) and run any rightsize process once, or clean up by hand with msb rm/
docker rm -f. See Orphan Reaping for the full mechanism, including why a
watchdog can't help against a remote docker daemon whose CI VM was torn down mid-run (the next
process to talk to that daemon still sweeps it).
Docker backend reports the daemon unreachable, but Docker is clearly running¶
Symptom: RIGHTSIZE_BACKEND=docker (or auto-selection resolving to Docker) fails
claiming the daemon is unreachable, even though docker ps works fine from a shell.
Cause: If you're on an older or custom-built backend-docker configuration using
the httpclient5 docker-java transport, and your own classpath separately manages
Apache HttpClient 5 to >= 5.4 (true of any Spring Boot 3.4+ application), docker-java
silently dials TCP localhost:2375 instead of the daemon's real unix:// socket.
Fix: rightsize's shipped backend-docker already uses the zerodep transport
specifically to rule this class of bug out — see
Backends for the full story.
If you're still seeing an unreachable-daemon error with the shipped backend, check
DOCKER_HOST and your Docker CLI context instead (Colima/OrbStack/Docker Desktop on a
non-default socket) — see the environment variables table.
Log lines appear truncated, duplicated, or split mid-line (Docker backend)¶
Symptom: .logs or followOutput occasionally shows a line broken across two
deliveries, or a partial line at the end.
Cause: docker-java's log-streaming API delivers frames whose boundaries are raw
chunking artifacts — a single logical log line can straddle two frames.
Fix: Already handled — the Docker backend reassembles frames with a line buffer and flushes the trailing fragment exactly once at stream end. If you're bypassing rightsize and reading docker-java's log stream directly, you'll need the same reassembly logic yourself.
Two containers occasionally fail to start with a port-bind error¶
Symptom: An occasional (not consistent) failure on start() mentioning a port
already in use or already allocated, especially in test suites that start several
containers concurrently.
Cause: Host ports are pre-allocated JVM-side (an ephemeral ServerSocket(0)
bind-then-release) before being handed to the backend, because msb only supports
static host:guest port maps. There's an unavoidable window between "we picked this
port" and "the backend actually bound it" where a sibling container in the same JVM
can grab the same port first.
Fix: Already handled — GenericContainer.start() retries the whole create+start
sequence up to 5 times with freshly allocated ports whenever it detects this specific
race (both a typed exception and a message-based fallback classifier catch it). You
shouldn't need to add your own retry logic around start() for this; if you're
seeing it persist past the built-in retries, that suggests something external
(another process, a port scanner) is racing the allocator continuously on that host,
not a rightsize bug.
Cross-container connection reachable once, then hangs on the second request (microsandbox backend)¶
Symptom: A container-to-container call over a Network
alias works exactly once, then every subsequent connection from the same consumer
hangs indefinitely.
Cause: msb 0.6.2's port-publish proxy never propagates the target's own TCP close
back to the tunnel's host-side socket — a host client reading a published port never
observes EOF even after the guest workload closes its end. Naively pumping until a
natural read() == 0 therefore blocks forever after the first exchange, so the
in-guest listener is never respawned for a second connection.
Fix: Already handled — the exec-tunnel relay infers "this exchange is over" from a
read-timeout heuristic instead of waiting for a natural close (a generous first-byte
deadline before any data has arrived, tightened to a short idle timeout once data
starts flowing). This is exactly why
Networking documents
"one connection at a time per tunnel" as a hard limit rather than a bug to be fixed —
it's the tunnel's designed contract, not an oversight. If your test needs a sustained,
multi-request connection to a sibling, that's the case to run under
RIGHTSIZE_BACKEND=docker instead.
Readiness passes but the server isn't really answering yet (either backend)¶
Symptom: Wait.forListeningPort() reports ready, but the very first real request
against the container fails or hangs.
Cause: Both backends have a layer between the host socket and the in-guest server that can accept a TCP connection before the server itself is listening — Docker's userland proxy on one side, microsandbox's loopback forwarder on the other. A bare connect-and-declare-victory check can be satisfied by that layer alone.
Fix: Wait.forListeningPort() already does a best-effort read-probe past this (see
Wait Strategies), which closes
the worst of the gap — but for a server that doesn't clearly fit "accepts a connection
and either speaks first or waits silently," prefer Wait.forHttp(...),
Wait.forLogMessage(...), or a protocol-aware custom wait strategy (see
MemcachedContainer for a worked example). This is exactly
the class of bug the shipped modules were built to avoid — check whether a module
already exists for your image before writing your own wait strategy from scratch.