Skip to main content
The sandbox and image helpers manage clients, stores, and cleanup. Use the options below when you need to select components or control their behavior. For basic workflows, start with Your first sandbox, Import a Docker image, and Build a Docker image. Connection settings remain in Connection sources; Share one client shows one client shared across all three workflows. For the workflow itself, continue with Work with a Session: save checkpoints, compare branches, roll back experiments, and resume saved work.

What each component does

A memory Store keeps history only for that store instance. A SQLite Store keeps it across Python restarts. The filesystem images themselves remain on the ConTree server. Deleting an upload-cache tree removes its reuse records without changing session history. Sandbox history and upload reuse share a memory Store by default. The import and build helpers use a persistent SQLite Store by default. See Advanced: assemble a sandbox yourself for component construction and ownership.

Bring your own storage

SyncStore and AsyncStore are the public storage interfaces. The SDK includes Memory and SQLite implementations. Subclass the matching interface to implement Redis or PostgreSQL storage. One Store holds both history and reusable results. Tree kinds separate their purposes. Store contains typed trees of immutable nodes. Session owns the selected position and local branch names. See Implement history storage for atomic insertion, pagination, and retention requirements. Pass your Store through store= to helpers, sessions, and builders. Use sync implementations with sync APIs and async implementations with async APIs. The SDK uses these interfaces, so changing the storage backend does not require changing its execution code. Your application owns and closes the supplied components.

Share one client

For a workflow with several operations, initialize a client once. Pass it through client= as a dependency to the import helper, build helper, and sandbox factory. This reuses the connection and gives your application one place to configure it. Each operation leaves this client open; the outer context closes it. Use the build-context directory from Create a small build context. These programs import the base under the alpine:latest ConTree tag, build that context, and read its result. The Dockerfile’s FROM alpine:latest resolves the imported image.

Select the session class and components

Some async snippets use top-level await. Run them inside an async function or a compatible notebook. Complete programs use asyncio.run(main()). session_type accepts a class. Without it, the context selects LazySession or AsyncLazySession. An explicitly selected class must match the context mode: session_type=AsyncLazySession with a synchronous with raises TypeError before creating a client. The same rule applies to client_type and supplied store, file transfer, and client objects.
Custom classes implement SyncExecutor or AsyncExecutor. Lazy subclasses receive an underlying ContreeSession or ContreeAsyncSession, plus session_kwargs. Other classes receive the client and the standard Session constructor arguments, plus session_kwargs. Subclass an existing session to retain its execution and lifecycle rules. The factory never substitutes a different class for an explicitly selected class. Selecting ContreeSession or ContreeAsyncSession saves each command by default and starts the next command in a separate VM. Use disposable=True to leave the session image unchanged. See Save after every command. Snapshots are lightweight; saving after every command is supported. A snapshot stops the VM and can return the same image UUID if files have not changed. Uploaded-file cache entries are separated by endpoint, project, and token. The cache does not replace history storage or enable uploads to an already running LazySession VM.

Ownership and extension

Each context object permits one entry, including a failed entry. Re-entry, nesting, and mixing sync/async entry on that object raise RuntimeError; call the factory again to get a new context. The factory closes resources it creates. Supplied clients, storage, and file transfers remain caller-owned and must be ready for use. A session’s context exit runs before the owned client closes. Async cleanup is joined even when the caller cancels again; a body exception takes priority over cleanup errors. All three factories call session_maker(context_type, ...) with their respective context class. SandboxSessionContext composes SyncSandboxSessionContext and AsyncSandboxSessionContext. For a custom context, supply a class accepting SandboxConfig to session_maker. See Sandbox factories for the shared parameter signature and context contracts. For complete sync and async examples of the component assembly behind this factory, see Advanced: assemble a sandbox yourself.

Tag a saved result

Run a setup command, save its result, and give that image a reusable name. This example selects ContreeSession or ContreeAsyncSession, which saves each command by default as an independent operation. The default lazy sandbox instead saves at a snapshot boundary; see Work with a Session.
Use image="tag:prepared-base" in another sandbox. The tag refers to the saved filesystem; it does not copy session history, environment overrides, or defaults. Assigning an existing tag moves it to the new image. Image listing and tag removal are available through list_images() and delete_image_tag() on the client. Closing a client or local store does not delete server images. Deleting a server image can invalidate saved session history.

Assemble the builder yourself

The helpers create a client and one SQLite Store for layer history and reusable uploads. They scope the build session to the context path and connection, call the Docker builder, and close the resources they created. Use the builder directly to manage these dependencies and their lifetime yourself. The following example keeps a builder inside the client context and uses its default memory Store. For persistence, pass store=SyncSQLiteStore(...) or its async equivalent and close it yourself. Helpers accept the same dependency as store=. Use builder_type= and builder_kwargs= to extend a builder through a helper.

Rebuild a multi-stage image after changing inputs

This standalone example creates multi-context in the current directory. Its first stage builds an output file. The second stage copies only that output with COPY --from. Use a base image with cat and a POSIX shell. The builder downloads regular files as bytes and attaches them at their final paths to the next operation. The file specification carries permissions and ownership, including explicit --chown/--chmod overrides. For directory sources, the builder exports tar and asks the live API to unpack it directly into the destination with merge enabled. This preserves existing entries that are absent from the archive. The VM runs sleep infinity during transfer. Explicit directory overrides run as subprocesses after unpacking and before the builder signals the main process to save the layer. These commands change only entries supplied by the archive. Directory copies require sleep infinity and, for overrides, chown or chmod; they do not require tar. Keep the same builder, store, and build session to observe layer reuse. Changing an ignored file leaves the build inputs unchanged. Changing message.txt invalidates the source RUN and the stage copy that depends on its output.
The cache comparison uses file content and execution inputs. It does not require the server to return a different image UUID for every submission. To retain this cache across Python restarts, supply a SQLite Store as above. Source files and directory archives are keyed by immutable image UUID and source path, without a client expiry. Before reuse, the builder checks the uploaded file with HEAD; a missing upload is downloaded from the image and uploaded again. Cached layers also receive a HEAD check, and missing images are rebuilt. Store retention still applies. The live API restores regular-file permission bits from tar. Its current implementation creates archive directories with mode 0755 and does not restore ownership or timestamps. Explicit directory overrides apply after unpacking. File attachments retain the source file mode and ownership unless COPY overrides them.

Observe a live build

Use the build-context files from Build a Docker image for these examples. Pass on_event= to receive BuildEvent notifications while a build runs. Output events arrive before RUN completes and carry raw bytes. Decode or format them in your application. Each operation uses the same reader for output and its final result. event.step.id is unique within one build. event.step.index is the zero-based parsed directive index, including ARG and metadata instructions. Synthetic steps have index=None. A nested step has parent_id set to its enclosing step’s ID. A final COPY/ADD flush appears as a synthetic RUN :; a stage flush appears as a child of the next FROM. If a nested step fails, both it and its parent report the same exception. Events include elapsed step time, image IDs, and the operation UUID when available. The terminal RUN event also contains its typed InstanceResult, including nonzero exit codes. A cache hit emits no output and no new operation UUID.
Callbacks run serially in the thread or task executing build(). The async builder accepts both synchronous and awaitable callbacks. Keep callbacks short. The async example offloads local output writes so they do not block the event loop. For a live UI, a callback can also feed a bounded queue that another task consumes. A callback error, including a closed output pipe, aborts the build and cancels its active RUN operation. Async task cancellation and sync interruption follow the same cleanup path. Error notifications cannot replace the original exception. Callbacks do not roll back completed layers. File, parse, and final tag errors raise directly; these do not create directive events outside a running step.

Customize a build

Pass on_step= to receive a BuildStepEvent for each parsed directive. It includes duration, cache-hit status, image IDs, and any error. Keep callbacks short; a callback exception can stop the build. The async builder also accepts an awaitable callback. Use parser=, session_factory=, or execute_directive() for custom behavior. See Customize the SDK and Dockerfile builders.

Upload cache lifetime and scope

COPY and ADD keep reusable upload records in the same Store as layer history. Tree kinds separate these records. Namespaces and layer hashes include the endpoint, project, and credential scope, so shared storage does not reuse another server’s UUIDs. Local paths and source URLs are retained through the public file-source registry. See Reuse uploads and retain their sources for enumeration and invalidation. The default upload lifetime is 90 days. Cache hits and URL responses with status 304 do not extend it. Override create_context() and set context.upload_cache_ttl to choose a shorter lifetime for your server. Expiry causes a new upload when the directive runs; it does not invalidate an existing image layer. A 304 response without a live cached upload raises DockerBuildError. Local COPY uses a temporary snapshot for its content hash and upload. Async COPY limits concurrent snapshots and uploads through context.upload_concurrency (default four). URL ADD streams into a temporary file before upload and closes the response body, including conditional responses. These paths bound memory usage; temporary disk space must hold the files currently being prepared.

Assemble the sandbox yourself

The factory resolves a connection, creates a client and memory store, connects file transfer to a base session, and wraps it in a lazy session. See Advanced: assemble a sandbox yourself for complete Sync/Async programs that assemble these components and close them in the correct order.

Advanced: assemble a sandbox yourself