Sandboxes let you run untrusted code (LLM-generated code, user submissions, AI agents, CI/CD pipelines) in isolated environments. Each sandbox is a Linux environment running against its own kernel rather than the host's. That closes the gap ordinary containers leave open: containers share the host kernel, so a kernel-level escape reaches the host. You get true isolation at container speed, and sandboxes boot in under a second.
On Northflank, sandboxes run on Northflank cloud or on your own infrastructure. You create them, run commands, access files, and destroy them programmatically with the JavaScript or Python SDK.
Isolation comes from microVMs for CPU workloads and gVisor for GPU workloads on Northflank cloud. On your own cloud, you select the runtime through cluster defaults or workload tags.
This guide takes you from an API token to a running sandbox you can execute commands in.
Set up your environment
Start by creating an API token, a project, and an SDK client.
Generate an API token
Create a project
Create a project on Northflank Cloud or select a BYOC cluster and note the project ID. Every call in this guide needs it. See create a project for the full walkthrough.
Install an SDK
npm install @northflank/js-client
pip install northflank
Initialize the SDK
Create an ApiClient with your API token.
import {
ApiClient,
ApiClientInMemoryContextProvider,
} from "@northflank/js-client";
const contextProvider = new ApiClientInMemoryContextProvider();
await contextProvider.addContext({
name: "context",
token: process.env.NORTHFLANK_TOKEN,
});
const apiClient = new ApiClient(contextProvider, {
throwErrorOnHttpErrorCode: true,
});
import os
from northflank import ApiClient
client = ApiClient(api_token=os.environ["NF_API_TOKEN"])
Create a sandbox
Deploy a container image as a deployment service. A service is Northflank's unit for a running workload, and each sandbox is one, which is why the calls below are service calls. The sandboxId is used as both the service name and the identifier in later calls.
const sandboxId = `sandbox-${crypto.randomUUID().split('-')[4]}`;
await apiClient.create.service.deployment({
parameters: {
projectId: 'your-project-id',
},
data: {
name: sandboxId,
billing: {
// Sets CPU and memory. nf-compute-200 is 2 vCPU, 4GB RAM.
deploymentPlan: 'nf-compute-200',
},
deployment: {
instances: 1,
docker: {
// Base images exit immediately, so override the entrypoint with a
// long-running command (or tail -f /dev/null) to keep the
// container up to exec into.
// Accepts: default, customEntrypoint, customCommand,
// customEntrypointCustomCommand.
configType: 'customEntrypointCustomCommand',
customEntrypoint: '/bin/bash',
customCommand: "-c 'sleep infinity'",
},
external: {
// Any public or private container image.
imagePath: 'ubuntu:22.04',
},
storage: {
ephemeralStorage: {
storageSize: 2048,
},
},
},
runtimeEnvironment: {
// Injected into the container at runtime.
MY_VAR: 'hello-world',
},
},
});
import uuid
sandbox_id = f"sandbox-{uuid.uuid4().hex[:8]}"
client.create.service.deployment(
project_id="your-project-id",
data={
"name": sandbox_id,
# deploymentPlan sets CPU and memory. nf-compute-200 is 2 vCPU, 4GB RAM.
"billing": {"deploymentPlan": "nf-compute-200"},
"deployment": {
"instances": 1,
# Base images exit immediately, so override the entrypoint with a
# long-running command (or tail -f /dev/null) to keep the
# container up to exec into.
# Accepts: default, customEntrypoint, customCommand,
# customEntrypointCustomCommand.
"docker": {
"configType": "customEntrypointCustomCommand",
"customEntrypoint": "/bin/bash",
"customCommand": "-c 'sleep infinity'",
},
# Any public or private container image.
"external": {"imagePath": "ubuntu:22.04"},
"storage": {"ephemeralStorage": {"storageSize": 2048}},
},
# Injected into the container at runtime.
"runtimeEnvironment": {"MY_VAR": "hello-world"},
},
)
Start the sandbox
Scale the service to one instance to boot it, then poll until its deployment status reaches COMPLETED.
await apiClient.scale.service({
parameters: {
projectId: 'your-project-id',
serviceId: sandboxId,
},
data: {
instances: 1,
},
});
async function waitForReady() {
while (true) {
const svc = await apiClient.get.service({
parameters: {
projectId: 'your-project-id',
serviceId: sandboxId,
},
});
const status = svc.data?.status?.deployment?.status;
// COMPLETED means the deployment rolled out and the container is
// running. It does not mean the process has exited.
if (status === 'COMPLETED') return;
if (status === 'FAILED') throw new Error('Sandbox deployment failed');
await new Promise((r) => setTimeout(r, 1000));
}
}
await waitForReady();
client.scale.service(
project_id="your-project-id",
service_id=sandbox_id,
data={"instances": 1},
)
# Polls get.service until the deployment status is COMPLETED, then returns
# the service object. Raises TimeoutError if timeout_s elapses first, or
# RuntimeError if the deployment reports FAILED.
# COMPLETED means the deployment rolled out and the container is running.
# It does not mean the process has exited.
client.helpers.wait_for_service_ready(
project_id="your-project-id",
service_id=sandbox_id,
timeout_s=300,
poll_interval_s=1,
)
Sandboxes boot in under a second, so the wait normally returns on its first or second poll.
Run commands in the sandbox
Run commands inside the running sandbox.
const handle = await apiClient.exec.execServiceSession(
{
projectId: 'your-project-id',
serviceId: sandboxId,
},
{
// shell picks the interpreter. Use bash -c for pipes, redirects, or
// chained commands. Without it the command runs directly.
shell: 'bash -c',
command: "echo 'Hello from sandbox!' && ls /",
}
);
const stdoutChunks = [];
const stderrChunks = [];
// Attach listeners before awaiting the result, or early output is missed.
handle.stdOut.on('data', (data) => stdoutChunks.push(data.toString()));
handle.stdErr.on('data', (data) => stderrChunks.push(data.toString()));
const result = await handle.waitForCommandResult();
console.log('Exit code:', result.exitCode);
console.log('Stdout:', stdoutChunks.join(''));
console.log('Stderr:', stderrChunks.join(''));
# Blocks until the command exits, returning an ExecResult with
# exit_code, stdout, stderr and status. result.ok means exit_code == 0.
result = client.exec.run_service_command(
project_id="your-project-id",
service_id=sandbox_id,
command="echo 'Hello from sandbox!' && ls /",
# shell picks the interpreter. Use bash -c for pipes, redirects, or
# chained commands. Without it the command runs directly.
shell="/bin/bash -c",
)
print("Exit code:", result.exit_code)
print("Stdout:", result.stdout)
print("Stderr:", result.stderr)
Interactive sessions
When you need to write to the process's stdin and read its output as it streams, open a session instead. In Python, open_service_session is a context manager so the WebSocket always closes:
with client.exec.open_service_session(
project_id="your-project-id", service_id=sandbox_id, command="cat"
) as session:
session.resize(rows=40, columns=120)
session.send("hello\n")
# Read the output a send produces before closing stdin; calling
# send_eof immediately after a send can race the proxy.
for chunk in session: # chunk.stream is "stdout" or "stderr"
print(chunk.data, end="")
if "hello" in chunk.data:
break
session.send_eof() # close stdin
result = session.wait() # ExecResult once the process exits
See execute commands for streaming, TTY handling, and the equivalent CLI commands.
Next steps
Sandbox examples
Configure and manage sandboxes
Sandboxes on Northflank cloud
Resource plans, GPU plans, regions, and billing on managed infrastructure.
Sandboxes in your own cloud