Sandbox Sidecars

Introduction 

Sandbox Sidecars let you run additional containers alongside your main Sandbox container, on the same host. A sandbox and its sidecars are connected via an internal bridge network, allowing low latency communication between containers over TCP/UDP, making them ideal for:

  • Separating an agent harness from its execution environment, by running the agent in one container and its tool calls in another
  • Credentials injection, by running a proxy in a separate, trusted container from the primary application, and letting that proxy inject credentials or other secrets before passing on network calls to external services. See the secrets injection example for a working demonstration
  • Splitting out complex multi-service applications over separate containers, such as databases, caches or worker processes, similar to Docker Compose.

We’re still discovering all the ways that Sandbox Sidecars can be used - if you come up with another use case, please let us know!

Sidecars are managed through the sidecars interface on a Sandbox (_experimental_sidecars in Python, experimentalSidecars in JS/Go), which provides methods to create, list, get, and terminate Sidecar containers.

Each Sidecar container:

  • Runs its own image independently from the main Sandbox container.
  • Runs in a separate, sandboxed process isolated from the main Sandbox container and other Sidecar containers.
  • Can communicate over an internal bridge network with the main Sandbox container and other Sidecar containers.
  • Can be created, terminated, and replaced dynamically during the Sandbox’s lifetime.
  • Supports executing commands just like the main Sandbox container.

Usage 

Creating a Sidecar container 

The main Sandbox container is resolvable as main, and each Sidecar container is resolvable by the name you give it at creation time.

import modal

app = modal.App.lookup("sidecar-example", create_if_missing=True)
image = modal.Image.debian_slim().build(app)

sb = modal.Sandbox.create("sleep", "600", app=app, image=image, timeout=300)

sidecar = sb._experimental_sidecars.create(
    "python",
    "-m",
    "http.server",
    "8080",
    name="web",
    image=image,
)

# Give the server a moment to start, then call it from the main sandbox.
p = sb.exec(
    "python",
    "-c",
    "import time, urllib.request; time.sleep(1); print(urllib.request.urlopen('http://web:8080').status)",
)
p.wait()
print(p.stdout.read())  # "200"

sb.terminate()

Names are resolved using /etc/hosts which gets updated when a sidecar is created or terminated.

Listing and retrieving sidecars 

You can list all running Sidecar containers or retrieve a specific one by name:

containers = sb._experimental_sidecars.list()
for container in containers:
    print(f"{container.name}: {container.object_id}")

sidecar = sb._experimental_sidecars.get(name="web")

Routing HTTPS traffic through a Sidecar 

Sidecars can be used to inspect the outgoing HTTPS traffic from the main Sandbox container in a different context, for example to perform more advanced request filtering, inspecting requests for logging, or injecting secrets that the main Sandbox container should not have access to.

Normally, applications need to support explicit proxy configuration, such as respecting the HTTPS_PROXY environment variable, to route traffic through a Sidecar. To include HTTPS traffic (TCP on port 443) from proxy-unaware applications, you can set the experimental proxy_traffic_via_sidecar option that routes all outbound HTTPS traffic from the main Sandbox container through a Sidecar.

sb = modal.Sandbox.create(
    "sleep",
    "600",
    app=app,
    image=image,
    experimental_options={"proxy_traffic_via_sidecar": "my-proxy-sidecar"},
)

# Until this Sidecar is running, HTTPS from the main container is refused.
sb._experimental_sidecars.create("python", "/proxy.py", name="my-proxy-sidecar", image=proxy_image)

The Sidecar receives a raw TLS stream and must read the destination hostname from the ClientHello’s SNI. The original destination IP is not forwarded, so mechanisms such as SO_ORIGINAL_DST do not work. To read or rewrite HTTP requests, terminate TLS in the proxy using a certificate authority that the Sandbox trusts. See the Sidecar traffic routing example for a complete mitmproxy-based request filter.

Only TCP traffic to port 443 is relayed. This traffic is not subject to other egress controls on the Sandbox, such as outbound_cidr_allowlist or a Proxy. Non-relayed traffic is still subject to the egress controls of the Sandbox. The option cannot be combined with setting block_network or outbound_domain_allowlist on the Sandbox.

Filesystem snapshots 

You can snapshot a running Sidecar’s filesystem into a reusable Image. The resulting Image can be used anywhere an existing Image is accepted; the example below uses it to start another Sidecar. The snapshot is scoped to that Sidecar; it does not include the main Sandbox filesystem or other Sidecars.

sidecar.filesystem.write_text("ready", "/tmp/state")
snapshot = sidecar.snapshot_filesystem()

restored = sb._experimental_sidecars.create(
    "sleep", "600", name="restored", image=snapshot
)
assert restored.filesystem.read_text("/tmp/state") == "ready"

Resource configuration 

The main Sandbox container and the Sidecar containers share the resource allocation (CPU and memory) of the Sandbox, and resources are configured only on the Sandbox. When planning your resource allocation, make sure the Sandbox is configured with enough CPU and memory for all containers combined. Bursting is still possible, see the guide to Sandbox resources and pricing for more details.

For example, if you want to run a Sandbox with two Sidecars, and you expect the main container to use 1 CPU core and 512 MiB of memory, Sidecar A to use 0.5 CPU and 256 MiB, and Sidecar B to use 0.5 CPU and 256 MiB, you should set the Sandbox’s resources to at least 2 CPUs and 1024 MiB to accommodate all three containers.

The maximum number of Sidecars you can create is also determined by the main Sandbox’s resource reservation. Each container (including the main one) requires a minimum of 32 mCPU and 32 MiB of memory, so the limit is:

max containers = min(cpu_in_milli / 32, memory_in_mib / 32)

There is also a hard limit of 250 concurrent sidecar containers per sandbox, regardless of the resource reservation.

Limitations 

The main sandbox supports the same features as a regular sandbox, but some features are not yet supported for sidecars:

  • Pre-built images only: Sidecar images must be pre-built using image.build(), referenced by ID via Image.from_id() or name via Image.from_name(), or created from filesystem/directory snapshots. Lazy image building is not supported for sidecars. See also Separating Image builds from Sandbox creation.
  • No Cloud Bucket Mount support: Sidecar containers do not currently support attaching Cloud Bucket Mounts.
  • No memory snapshot support: A Sidecar’s filesystem can be snapshotted independently, but Sidecar memory state is not captured in Sandbox snapshots.
  • VM incompatibility: Sidecars are not compatible with VM Sandboxes.
  • Changes to /etc/hosts are not preserved: /etc/hosts is rewritten on sidecar create/terminate and user changes are not preserved.
  • Maximum of 250 concurrent sidecars: A sandbox can have at most 250 sidecar containers running at the same time.
  • No Proxy support: Traffic from a Sidecar does not exit through a Proxy.