Files & Memory¶
MountableFile¶
MountableFile describes a file to mount into a container via
GenericContainer.withCopyFileToContainer(file, guestPath). This is a start-time mount,
configured before start() — see Copying Files for the runtime equivalent
(copyFileToContainer/copyContentToContainer/copyFileFromContainer), which acts on an
already-running container in either direction. Build a MountableFile from either a host path or
a classpath resource:
val fromDisk = MountableFile.forHostPath("/absolute/or/relative/path/on/disk")
val fromClasspath = MountableFile.forClasspathResource("fixtures/seed-data.sql")
container.withCopyFileToContainer(fromClasspath, "/docker-entrypoint-initdb.d/seed.sql")
forHostPathresolves the given path to an absolute path immediately; it doesn't check the file exists until mount time.forClasspathResourceresolves the resource against the current thread's context classloader, copies it into a fresh temp file (backends need a real host path to mount, not a classpath URL), and registers that temp file withFile.deleteOnExit(). That's fine for the short-lived test/build JVMs this is designed for — if your process is long-running and calls this repeatedly, be aware the temp files accumulate until the JVM actually exits.
Read-only mounts¶
FileMount.readOnly (the flag underneath withCopyFileToContainer) defaults to
false: the mount is read-write, and it is a view of the host file rather than a copy
of it — the Docker backend binds the host path directly, the microsandbox backend
hard-links it into its staging directory — so a guest write reaches the host file
itself.
A mount constructed with readOnly = true is enforced on both backends as a
guest-side write block: an in-guest write fails with Read-only file system. Use it
whenever the host copy must not be modified — in particular when the container runs
code you don't trust (see Isolation).
withMemoryLimit(megabytes)¶
Caps a container's guest memory. Maps to msb's -m <megabytes>M flag on the
microsandbox backend and to HostConfig.memory (in bytes) on the Docker backend.
Leave it unset (the default) and each backend applies its own default sizing instead.
When you actually need this¶
microsandbox's default microVM has a small amount of guest-available RAM — roughly 450 MB in practice (the exact figure observed varies a little run to run, somewhere in the ~443–454 MB range, but "~450 MB" is the number to plan around). Most images fit comfortably under that. Two categories don't, and both are shipped modules that had to raise the limit to boot at all:
Spring Cloud Config Server. The hyness/spring-cloud-config-server image is built
on a Paketo buildpack, and Paketo's memory calculator sizes the JVM's fixed regions
(heap + metaspace + thread stacks, computed ahead of time from the image's own
metadata) to around 688 MB — comfortably over the microVM default. SpringCloudConfigContainer
sets withMemoryLimit(1024) for exactly this reason; without it, the JVM fails to
launch under the microsandbox backend (it boots fine under Docker, whose containers
aren't memory-constrained by default, which is precisely why this class of bug is easy
to miss if you only ever test on Docker).
Apache Pinot's QuickStart cluster. PinotContainer runs a QuickStart -type EMPTY
process tree — ZooKeeper, controller, broker, server, and minion, four JVMs plus
ZooKeeper, all inside one container — and the image itself bakes in
JAVA_OPTS=-Xms4G -Xmx4G for the QuickStart driver JVM alone, before any of the four
sub-JVMs it spawns have taken anything. The original plan was withMemoryLimit(2048)
by analogy with the Config Server fix; that under-shot badly. Measured directly against
real boots:
| Memory limit | Result |
|---|---|
| 2048 MB | OOM-killed — timed out waiting for /health, reaped by the kernel |
| 2560 MB | OOM-killed |
| 3072 MB | Boots; /health returns 200 within ~15s — but docker stats settles at ~99% of the limit, and under that pressure the controller's Helix-backed schema/table-config RPCs intermittently time out even though /health reports 200 |
| 4096 MB | Boots cleanly; docker stats settles at ~73–75%; schema POST succeeded on every attempt across a 60s repeated-POST probe |
PinotContainer ships with withMemoryLimit(4096) — the lowest round number that
leaves real headroom above the image's own 4 GiB heap request, not merely enough to
dodge the OOM killer outright. Verified stable on both backends. If you're
configuring memory limits for your own JVM-heavy image, treat "boots without getting
OOM-killed" as a necessary but not sufficient bar — leave headroom, the way this
module does, or you'll see the same kind of intermittent RPC timeout under memory
pressure that 3072 MB produced here.
Images that don't need it¶
Most modules never touch withMemoryLimit at all — Redis, an Erlang VM like
RabbitMQ, single-process HTTP servers like WireMock or ClickHouse's server (not a JVM
process at all), and even MySQL/MariaDB's InnoDB default footprint all boot cleanly
under the ~450 MB default with no adjustment. Only reach for withMemoryLimit when
you actually observe a boot failure or OOM under the microsandbox backend — don't
apply it prophylactically to every module.
withDiskLimit(megabytes) and withTmpfsRoot(megabytes)¶
Two more msb-only knobs control the container's writable root disk. Both are ignored on the Docker backend — Docker runs without a root-disk ceiling and has no RAM-backed rootfs mode, so either builder is a silent no-op there.
GenericContainer("some-image:latest")
.withTmpfsRoot(512) // MB — RAM-backed root disk instead of storage
withDiskLimit caps the writable root disk at the given size (msb's --root-disk <mb>M).
On an msb reboot the cap can only grow, never shrink.
withTmpfsRoot backs the root disk with RAM instead of disk storage (msb's
--root-disk tmpfs:<mb>M) — faster ephemeral containers, and nothing written to the rootfs
leaves a trace on the host disk afterward. It has to fit inside the guest's own memory: msb
defaults the guest to 512M when withMemoryLimit is unset, and setting both with a tmpfs size
larger than the memory limit throws TmpfsRootExceedsMemoryException at start().
Both are mutually exclusive with each other — the root disk is either size-capped or RAM-backed,
never both — and combining them throws RootDiskConflictException at start(), before any
backend call.
Constraints worth knowing before you reach for these¶
- A tmpfs root cannot be checkpointed.
checkpoint()on awithTmpfsRoot()container throwsTmpfsRootCheckpointExceptionbefore touching anything — the root disk lives in guest memory, so there's nothing durable to snapshot. A refused named re-checkpoint leaves the existing checkpoint entirely intact. See Checkpoint / Restore. - Neither survives a
fromCheckpointrestore. msb rejects any root-disk setting (withDiskLimitorwithTmpfsRoot) on a container restored viafromCheckpointbefore boot — the snapshot already pins the root disk from whatever it was when the checkpoint was taken.