Sandbox Sidecars
Introduction
Sandbox Sidecars let you run additional containers alongside your primary 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 and other Sidecars.
- Can communicate over an internal bridge network with the main sandbox and other Sidecars.
- Can be created, terminated, and replaced dynamically during the Sandbox’s lifetime.
- Supports executing commands just like the main Sandbox.
Usage
Creating a Sidecar container
The main Sandbox is resolvable as main, and each Sidecar 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()import { ModalClient } from "modal";
const modal = new ModalClient();
const app = await modal.apps.fromName("sidecar-example", {
createIfMissing: true,
});
const image = await modal.images.fromRegistry("python:3.13-slim").build(app);
const sb = await modal.sandboxes.create(app, image, {
command: ["sleep", "600"],
timeoutMs: 300 * 1000,
});
const sidecar = await sb.experimentalSidecars.create("web", image, {
command: ["python", "-m", "http.server", "8080"],
});
// Give the server a moment to start, then call it from the main sandbox.
const p = await sb.exec([
"python",
"-c",
"import time, urllib.request; time.sleep(1); print(urllib.request.urlopen('http://web:8080').status)",
]);
await p.wait();
console.log(await p.stdout.readText()); // "200"
await sb.terminate();package main
import (
"context"
"fmt"
"io"
"time"
modal "github.com/modal-labs/modal-client/go"
)
func main() {
ctx := context.Background()
mc, _ := modal.NewClient()
app, _ := mc.Apps.FromName(ctx, "sidecar-example", &modal.AppFromNameParams{
CreateIfMissing: true,
})
image, _ := mc.Images.FromRegistry("python:3.13-slim", nil).Build(ctx, app, nil)
sb, _ := mc.Sandboxes.Create(ctx, app, image, &modal.SandboxCreateParams{
Command: []string{"sleep", "600"},
Timeout: 5 * time.Minute,
})
defer sb.Terminate(ctx, nil)
sidecar, _ := sb.ExperimentalSidecars.Create(ctx, "web", image, &modal.SidecarCreateParams{
Command: []string{"python", "-m", "http.server", "8080"},
})
_ = sidecar
// Give the server a moment to start, then call it from the main sandbox.
p, _ := sb.Exec(ctx, []string{
"python", "-c",
"import time, urllib.request; time.sleep(1); print(urllib.request.urlopen('http://web:8080').status)",
}, nil)
stdout, _ := io.ReadAll(p.Stdout)
fmt.Println(string(stdout)) // "200"
}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")const containers = await sb.experimentalSidecars.list();
for (const container of containers) {
console.log(`${container.containerName}: ${container.containerId}`);
}
const sidecar = await sb.experimentalSidecars.get("web");containers, _ := sb.ExperimentalSidecars.List(ctx, nil)
for _, container := range containers {
fmt.Printf("%s: %s\n", container.ContainerName, container.ContainerID)
}
sidecar, _ := sb.ExperimentalSidecars.Get(ctx, "web", nil)
_ = sidecarResource configuration
Sidecars share the resource allocation (CPU and memory) of the main Sandbox container, and resources are configured only on the main Sandbox. When planning your resource allocation, make sure the main Sandbox is configured with enough CPU and memory for all containers (main + Sidecars) 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 viaImage.from_id(), or created from filesystem/directory snapshots. Lazy image building is not supported for sidecars. See also Separating Image builds from Sandbox creation. - No volume or mount support: Sidecar containers do not currently support attaching Volumes or Cloud Bucket Mounts.
- No snapshot support: Sidecar container state is not captured in Sandbox snapshots.
- Changes to /etc/hosts are not preserved:
/etc/hostsis 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.