Docs

Quickstart

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

Click here to create an API token.

Create a project

Click here to 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

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,
});

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',
    },
  },
});

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();

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(''));

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.

© 2026 Northflank Ltd. All rights reserved.

northflank.com / Terms / Privacy / feedback@northflank.com