Skip to content

Images and snapshots ​

Sandboxes start from images. OpenDsec keeps images in an image store shared by every node, each image a stack of read-only layers. This page covers image names, importing, making images from sandboxes, using snapshots, and the store's requirements and cleanup.

1. Names ​

An image name is <project>/<name>:<tag>:

PartRuleExample
ProjectAn existing project path; each segment [A-Za-z0-9][A-Za-z0-9._-], up to 64 charactersroot, root/rl
Name[a-z0-9][a-z0-9._-], up to 128 characters, lower case onlypy, swe-4821
Tag[A-Za-z0-9_][A-Za-z0-9._-], up to 128 characters; latest when left out3.12, v1, step3

An image belongs to the project in its name, and permissions follow that project:

ActionPermission
Create sandboxes from it, view, listlayer.use on the image's project
Import, compose, pack_diff, snapshot, deletelayer.publish on the image's project

A name without / (such as host) is a node-local image (edges[].images), for development and tests only.

2. Importing ​

Import from a registry or a file with opendsecctl image import. Run it as root on a machine that mounts the image store (any compute node, or the single-node machine): it writes the layers into the store directly, then registers the image through the API.

sh
# public registry (docker.io/library/ can be left out for Docker Hub)
opendsecctl image import python:3.12 -as root/py:3.12 -store /mnt/opendsec-images

# private registry: docker login's saved credentials, or pass them
echo "$HARBOR_TOKEN" | opendsecctl image import harbor.example.com/team/app:1.0 -as root/app:1.0 \
    -store /mnt/opendsec-images -username 'robot$ci' -password-stdin

# an internal registry without TLS
opendsecctl image import 10.0.0.5:5000/base:1 -as root/base:1 -store /mnt/opendsec-images -plain-http

# files: a docker save tar, an OCI layout directory or tar
docker save myimage:dev -o myimage.tar
opendsecctl image import ./myimage.tar -as root/rl/myimage:dev -store /mnt/opendsec-images
opendsecctl image import ./bundle.tar#app:1 -as root/app:1 -store /mnt/opendsec-images   # a file holding several images
  • The image's environment variables, working directory and default user come from its own config (a Dockerfile's ENV, WORKDIR, USER). ENTRYPOINT and CMD do not run by themselves: you run commands once the sandbox is up.
  • Identical layers are stored once: a second image on the same base reuses its layers.
  • -overwrite replaces an image of the same name; without it, an existing name is an error.
  • On a single-node install, . /root/opendsec-ops/env already sets OPENDSEC_IMAGE_STORE, so -store can be left out.

Data centers without internet access: save the image to a file on a connected machine, copy it over and import it:

sh
opendsecctl image save python:3.12 -out python-3.12.oci.tar      # on a connected machine; no cluster needed
opendsecctl image import ./python-3.12.oci.tar -as root/py:3.12 -store /mnt/opendsec-images

3. Composing ​

Stack several images in order into a new one, copying no data:

sh
opendsecctl image compose root/rl/swe-4821:v1 \
    root/rl/python:3.12 \          # bottom: the base environment
    root/rl/django-ws:a1b2 \       # middle: a repository and its dependencies
    root/tools/agent-kit:3         # top: the agent's tools
python
await client.compose_image("root/rl/swe-4821:v1",
                           ["root/rl/python:3.12", "root/rl/django-ws:a1b2", "root/tools/agent-kit:3"],
                           env={"TASK": "4821"}, workdir="/srv/repo")
  • Later images lie on top of earlier ones: a file in an upper layer wins, and a file an upper layer deleted is gone from the result.
  • Environment variables are merged in order, with env applied last; workdir and user default to the parts'.
  • It suits RL's "base environment × repository × tools" combinations: each part is built once, and any combination costs no extra space.

4. Making images from sandboxes ​

Two ways:

pack_diffcreate_snapshot
Backendscontainer, fncallcontainer, microvm
SavesThe sandbox's changes against its image, as a new layer on top of that imageThe sandbox's whole state; with memory on microVMs
CleanupRemoves /tmp, /var/tmp, /root/.cache, shell and Python history, plus what you addNone; saved as is
A sandbox created from itStarts fresh, with the files as savedmicroVM: resumes from the snapshot with its processes running
Works forcontainer and microVMthe same backend (a container snapshot holds files only)
Typical useReusable environments: dependencies installed, code builtWarm starts: models loaded, services running; the state at one step

pack_diff ​

python
sb = await client.run_container(DSecContainerRunArgs(
        container_image="root/rl/python:3.12", network_rules={"pypi": True, "github": True}))
await sb.run_shell("git clone https://github.com/org/repo /srv/repo && pip install -e /srv/repo")
img = await sb.pack_diff("root/rl/repo-env:v1", scrub=["srv/repo/.git"])
await sb.stop()
sh
opendsecctl image pack $ID root/rl/repo-env:v1 -scrub srv/repo/.git
  • scrub takes globs relative to /.
  • The new image records its parent image (parent) and has one layer more.
  • microVMs do not support pack_diff. For a files-only image, build on container: the image works on microVMs too.

Snapshots ​

python
sb = await client.run_sandbox(DSecRunArgs(backend="microvm", image="root/py:3.12", memory_limit_mb=2048,
                                          network_rules={"pypi": True}))
await sb.run_shell("pip install -q numpy")
await sb.run_shell("(nohup python3 -c 'import numpy, http.server as h; h.test(h.SimpleHTTPRequestHandler, port=8000)' > /tmp/srv.log 2>&1 &)")
img = await sb.create_snapshot("root/rl/warm:v1", memory=True)
await sb.stop()

# created from the snapshot in about 55 ms: the HTTP server, numpy loaded, is already running on port 8000
new = await client.run_sandbox(DSecRunArgs(backend="microvm", image="root/rl/warm:v1", memory_limit_mb=2048))
sh
opendsecctl image snapshot $ID root/rl/warm:v1 -memory
  • microVMs must include memory (memory=True / -memory).
  • Sandboxes created from a snapshot with memory must use the snapshot's memory_limit_mb.
  • A snapshot is placed only on nodes running the Firecracker version it was made with; after a cluster upgrades Firecracker, older snapshots work only on nodes not yet upgraded, so make them again when needed.
  • Every sandbox created from a snapshot gets its own hostname, machine-id and random seed.
  • A snapshot can be used at once on the node of its sandbox; meanwhile it is published to the image store in the background, and other nodes can use it once that finishes (seconds to tens of seconds, depending on the store). Creations on other nodes wait for it until then.

Fork and snapshots ​

fork also takes a snapshot and creates the copies from it, but the snapshot is not registered as an image and is not needed once the copies exist. To create from one state many times over a long period, use create_snapshot; for a few copies now, use fork.

5. The image store ​

Requirements ​

The store is mounted at the same path on every apiserver and edge node (default /mnt/opendsec-images), the config's image_store.root. NFS, CephFS, JuiceFS and other shared file systems work:

  • Root squash must be off: edges write snapshots as root, and apiservers delete them.
  • With NFS, use version 4.2 and these mount options: vers=4.2,nconnect=8,hard,timeo=600,lookupcache=positive,_netdev.
    • 4.1 works too, but cannot find the holes of sparse files: using a memory snapshot on another node then reads it at its full size (more than twice as slow in our tests).
    • lookupcache=positive stops the client from caching "file not found". Without it, a node that looked for an image before it was registered could keep missing it for up to a minute.
  • Every node needs a distinct NFS client identity (deploy/node/install.sh sets one from /etc/machine-id); otherwise the server takes nodes with the same host name for one client.

Check the mount before use:

sh
opendsecctl image store-check -store /mnt/opendsec-images

Every required item must be ok. The items are listed in the CLI reference.

Layout ​

<root>/layers/<layer id>/rootfs/...     layer contents (overlay format)
<root>/layers/<layer id>/meta.json
<root>/images/<project>/<name>/<tag>.json   image manifests: the layers and defaults
<root>/snapshots/<id>/...              snapshots (disks, memory)
<root>/vmdisks/...                     microVM image disks (EROFS with erofs_images)

Do not change these files by hand; use the opendsecctl image commands.

How nodes use images ​

  • Containers: each node keeps local copies of image layers (layer_cache, on by default), copied from the store on first use; later containers stack those local layers. Running containers are unaffected when the store is unreachable.
  • microVMs: each image becomes a read-only image disk on the node. With erofs_images, the disk is built once in the store as EROFS and copied to each node; otherwise every node flattens it to ext4 itself. With image_pmem, VMs of one image share the host's page cache.
  • While the store is unreachable, new creations, pack_diff, snapshots and cross-node forks wait until it is back or their calls time out; running microVMs are unaffected.

6. Deletion and cleanup ​

sh
opendsecctl image delete root/rl/old:v1                      # delete the registration
opendsecctl image gc -store /mnt/opendsec-images -dry-run    # see what would go
opendsecctl image gc -store /mnt/opendsec-images             # remove layers and snapshots no image refers to
  • image delete removes the image's registration only; its layers may still serve other images and are not deleted right away.
  • image gc removes layers and snapshots no image refers to. Anything younger than -grace (default 24 hours) stays, so imports in progress and fresh snapshots are safe.
  • Running sandboxes are unaffected: containers use the node's local copies, microVMs the node's image disks.
  • Edges reclaim their own local copies and disk caches.

Run image gc regularly (daily, say) on a machine that mounts the store.

Implements the paper DeepSeek Elastic Compute (DSec) (arXiv:2609.22978). An independent project, not affiliated with DeepSeek.