Sandboxes

In addition to the Function interface, Modal has a direct interface for defining containers at runtime and securely running arbitrary code inside them.

This can be useful if, for example, you want to:

  • Execute code generated by a language model.
  • Create isolated environments for running untrusted code.
  • Check out a git repository and run a command against it, like a test suite, or npm lint.
  • Run containers with arbitrary dependencies and setup scripts.

Each individual job is called a Sandbox and can be created using the Sandbox.create constructor:

import modal

app = modal.App.lookup("my-app", create_if_missing=True)

sb = modal.Sandbox.create(app=app)

p = sb.exec("python", "-c", "print('hello')", timeout=3)
print(p.stdout.read())

p = sb.exec("bash", "-c", "for i in {1..10}; do date +%T; sleep 0.5; done", timeout=5)
for line in p.stdout:
    # Avoid double newlines by using end="".
    print(line, end="")

sb.terminate()

Note: you can run the above example as a script directly with python my_script.py. modal run is not needed here since there is no entrypoint.

Sandboxes require an App to be passed when spawned from outside of a Modal container. You may pass in a regular App object or look one up by name with App.lookup. The create_if_missing flag on App.lookup will create an App with the given name if it doesn’t exist.

Running a Sandbox with an entrypoint 

In most cases, Sandboxes are treated as a generic container that can run arbitrary commands. However, in some cases, you may want to run a single command or script as the entrypoint of the Sandbox. You can do this by passing string arguments to the Sandbox constructor:

sb = modal.Sandbox.create("python", "-m", "http.server", "8080", app=my_app, timeout=10)
for line in sb.stdout:
    print(line, end="")

This functionality is most useful for running long-lived services that you want to keep running in the background. See our Jupyter notebook example for a more concrete example of this.

Referencing Sandboxes from other code 

If you have a running Sandbox, you can retrieve it using the Sandbox.from_id method.

sb = modal.Sandbox.create(app=my_app)
sb_id = sb.object_id

# ... later in the program ...

sb2 = modal.Sandbox.from_id(sb_id)

p = sb2.exec("echo", "hello")
print(p.stdout.read())
sb2.terminate()

A common use case for this is keeping a pool of Sandboxes available for executing tasks as they come in. You can keep a list of object_ids of Sandboxes that are “open” and reuse them, closing over the object_id in whatever function is using them.

Parameters 

Sandboxes support nearly all configuration options found in regular modal.Functions. Refer to Sandbox.create for further documentation on Sandbox parametrization.

For example, Images and Volumes can be used just as with functions:

sb = modal.Sandbox.create(
    image=modal.Image.debian_slim().pip_install("pandas"),
    volumes={"/data": modal.Volume.from_name("my-volume")},
    workdir="/repo",
    app=my_app,
)

Using custom images 

Sandboxes support custom images just as Functions do. However, while you’ll typically invoke a Modal Function with the modal run cli, you typically spawn a Sandbox with a simple python call. As such, you need to manually enable output streaming to see your image build logs:

image = modal.Image.debian_slim().pip_install("pandas", "numpy")

with modal.enable_output():
    sb = modal.Sandbox.create(image=image, app=my_app)

Dynamically defined environments 

Note that any valid Image or Mount can be used with a Sandbox, even if those images or mounts have not previously been defined. This also means that Images and Mounts can be built from requirements at runtime. For example, you could use a language model to write some code and define your image, and then spawn a Sandbox with it. Check out devlooper for a concrete example of this.

Environment variables 

You can set environment variables using inline secrets:

secret = modal.Secret.from_dict({"MY_SECRET": "hello"})

sb = modal.Sandbox.create(
    secrets=[secret],
    app=my_app,
)
p = sb.exec("bash", "-c", "echo $MY_SECRET")
print(p.stdout.read())

Verbose logging 

You can see Sandbox execution logs using verbose=True. For example:

sb = modal.Sandbox.create(app=my_app, verbose=True)

p = sb.exec("python", "-c", "print('hello')")
print(p.stdout.read())

with sb.open("test.txt", "w") as f:
    f.write("Hello World\n")

shows Sandbox logs:

Sandbox exec started: python -c print('hello')
Opened file 'test.txt': fd-yErSQzGL9sig6WAjyNgTPR
Wrote to file: fd-yErSQzGL9sig6WAjyNgTPR
Closed file: fd-yErSQzGL9sig6WAjyNgTPR

Named Sandboxes 

You can assign a name to a Sandbox when creating it. Each name must be unique within an app - only one running Sandbox can use a given name at a time. Once a Sandbox completely stops running, its name becomes available for reuse. Some applications find Sandbox Names to be useful for ensuring that no more than one Sandbox is running per resource or project. If a Sandbox with the given name is already running, create() will raise a modal.exception.AlreadyExistsError.

sb1 = modal.Sandbox.create(app=my_app, name="my-name")
# this will raise a modal.exception.AlreadyExistsError
sb2 = modal.Sandbox.create(app=my_app, name="my-name")

A named Sandbox may be fetched using modal.Sandbox.from_name() but only if the Sandbox is currently running. If no running Sandbox is found, from_name() will raise a modal.exception.NotFoundError.

my_app = modal.App.lookup("my-app", create_if_missing=True)
sb1 = modal.Sandbox.create(app=my_app, name="my-name")
# returns the currently running Sandbox with the name "my-name"
sb2 = modal.Sandbox.from_name("my-app", "my-name")
assert sb1.object_id == sb2.object_id # sb1 and sb2 refer to the same Sandbox

Tagging 

Sandboxes can also be tagged with arbitrary key-value pairs. These tags can be used to filter results in Sandbox.list.

sandbox_v1_1 = modal.Sandbox.create("sleep", "10", app=my_app)
sandbox_v1_2 = modal.Sandbox.create("sleep", "20", app=my_app)

sandbox_v1_1.set_tags({"major_version": "1", "minor_version": "1"})
sandbox_v1_2.set_tags({"major_version": "1", "minor_version": "2"})

for sandbox in modal.Sandbox.list(app_id=my_app.app_id):  # All sandboxes.
    print(sandbox.object_id)

for sandbox in modal.Sandbox.list(
    app_id=my_app.app_id,
    tags={"major_version": "1"},
):  # Also all sandboxes.
    print(sandbox.object_id)

for sandbox in modal.Sandbox.list(
    app_id=app.app_id,
    tags={"major_version": "1", "minor_version": "2"},
):  # Just the latest sandbox.
    print(sandbox.object_id)