AI Dev Tools
·4 min read·tutorial

How to Run Docker Containers Inside Vercel Sandbox for Secure Code Execution

Learn how to build, deploy, and run docker containers inside vercel sandbox to execute untrusted code securely with TypeScript.

Executing untrusted user code or running heavy, dynamic workloads inside a serverless environment has historically been a recipe for disaster. Serverless runtimes are highly constrained, ephemeral, and lack the isolation needed to prevent malicious actors from abusing your system.

By leveraging Vercel Sandbox (powered by E2B's secure microVM infrastructure), you can run fully isolated custom Docker containers inside a secure sandbox directly from your Vercel deployments.

Here is the end result: a serverless API endpoint that boots a custom, isolated Docker container with a pre-configured Python data science stack, executes arbitrary code generated by an LLM, extracts the generated visualization, and tears down the environment—all within 2 seconds.

typescript
import { Sandbox } from '@e2b/code-interpreter';
 
// Create a sandbox instance using our custom Docker-based template
const sandbox = await Sandbox.create({
  template: 'data-science-runner',
  apiKey: process.env.E2B_API_KEY,
});
 
// Run untrusted code in an isolated Docker container
const execution = await sandbox.runCode(`
import pandas as pd
import matplotlib.pyplot as plt
 
df = pd.DataFrame({'x': [1, 2, 3], 'y': [10, 20, 15]})
plt.plot(df['x'], df['y'])
plt.savefig('chart.png')
`);
 
// Retrieve the file generated inside the container
const fileBytes = await sandbox.downloadFile('chart.png');
await sandbox.close();

The Architecture: How to Run Docker Containers Inside Vercel Sandbox Safely

To run arbitrary code safely, you cannot rely on Node.js process isolation or basic containerization. You need a virtualization boundary. When you run docker containers inside vercel sandbox, your code runs inside a microVM (micro Virtual Machine) managed by Firecracker.

The overall architecture separates your stateless Vercel Serverless Function from the stateful, isolated execution environment:

The Vercel Function acts as the orchestrator. It makes lightweight API calls to provision the microVM, sends the execution payload, streams stdout/stderr back to the client, and fetches any generated files. The untrusted code never touches your Vercel environment's filesystem or environment variables.


Step 1: Define Your Custom Docker Environment

To run a custom environment inside your sandbox, you must define a custom template. This is done using a standard Dockerfile combined with an E2B configuration file.

Create a new directory for your template:

bash
mkdir -p sandboxes/data-science-runner
cd sandboxes/data-science-runner

Now, create a Dockerfile that defines the exact environment you want your sandboxed code to execute within. Keep this image as lean as possible to minimize container boot times.

dockerfile
# Use a lightweight Debian-based Python image
FROM python:3.11-slim-bookworm
 
# Install system dependencies required for data visualization and compilation
RUN apt-get update && apt-get install -y \
    curl \
    git \
    libpng-dev \
    libfreetype6-dev \
    && rm -rf /var/lib/apt/lists/*
 
# Install specific Python libraries for our execution environment
RUN pip install --no-cache-dir \
    pandas \
    numpy \
    matplotlib \
    seaborn \
    scipy
 
# The sandbox agent requires a working directory
WORKDIR /workspace
 
# Copy custom entrypoints or configurations if necessary
COPY . .

Next, initialize the sandbox configuration file (e2b.toml):

toml
# e2b.toml
team_id = "your-team-id"
template_id = "data-science-runner"
template_name = "data-science-runner"
dockerfile = "Dockerfile"

Build and push this template to the Vercel Sandbox registry using the CLI:

bash
npx @e2b/cli template build

This command packages your local Dockerfile, sends it to the secure build pipeline, compiles it into a custom microVM snapshot, and registers it under your account.


Step 2: Write the Secure Execution Engine in TypeScript

Now that your custom Docker template is registered, you need to write the orchestration layer inside your Vercel project.

First, install the required SDK:

bash
npm install @e2b/code-interpreter

In high-throughput systems, managing sandbox lifecycles efficiently is critical. If you are handling multiple execution requests concurrently, you should optimize your async flows to handle timeouts and cleanup properly. For complex concurrency patterns, refer to our guide on advanced TypeScript async/await patterns for high throughput.

Here is a robust, production-ready execution engine written in TypeScript:

typescript
// lib/sandbox-runner.ts
import { Sandbox } from '@e2b/code-interpreter';
 
interface ExecutionResult {
  success: boolean;
  stdout: string[];
  stderr: string[];
  chartUrl?: string;
  error?: string;
}
 
export async function executePythonSandbox(
  userCode: string,
  timeoutMs: number = 15000
): Promise<ExecutionResult> {
  let sandbox: Sandbox | null = null;
 
  // Enforce absolute timeout to prevent hanging serverless functions
  const timeoutPromise = new Promise<never>((_, reject) =>
    setTimeout(() => reject(new Error('Execution timed out')), timeoutMs)
  );
 
  try {
    const executionPromise = (async () => {
      // Initialize the sandbox with our registered Docker template
      sandbox = await Sandbox.create({
        template: 'data-science-runner',
        apiKey: process.env.E2B_API_KEY,
      });
 
      const stdout: string[] = [];
      const stderr: string[] = [];
 
      // Run code and listen to real-time streams
      const execution = await sandbox.runCode(userCode, {
        onStdout: (msg) => stdout.push(msg.line),
        onStderr: (msg) => stderr.push(msg.line),
      });
 
      if (execution.error) {
        return {
          success: false,
          stdout,
          stderr,
          error: execution.error.value,
        };
      }
 
      // Check if the script generated our expected chart artifact
      let chartUrl: string | undefined = undefined;
      try {
        const files = await sandbox.files.list('/');
        const hasChart = files.some(f => f.name === 'chart.png');
 
        if (hasChart) {
          const fileBuffer = await sandbox.downloadFile('chart.png');
          const base64 = Buffer.from(fileBuffer).toString('base64');
          chartUrl = `data:image/png;base64,${base64}`;
        }
      } catch (fileErr) {
        console.warn('Failed to check or download execution artifacts:', fileErr);
      }
 
      return {
        success: true,
        stdout,
        stderr,
        chartUrl,
      };
    })();
 
    // Race the sandbox execution against our safety timeout
    return await Promise.race([executionPromise, timeoutPromise]);
 
  } catch (error: any) {
    return {
      success: false,
      stdout: [],
      stderr: [],
      error: error.message || 'Unknown execution error',
    };
  } finally {
    // Crucial: Always close the sandbox to prevent microVM leaks and billing charges
    if (sandbox) {
      await (sandbox as Sandbox).close().catch((err) => {
        console.error('Failed to close sandbox cleanly:', err);
      });
    }
  }
}

Step 3: Implement the Vercel Serverless Route Handler

With the runner logic defined, wrap it inside a Next.js or Vercel Route Handler. This endpoint will receive untrusted code from your frontend or LLM orchestrator, execute it inside the custom Docker sandbox, and return the structured output.

typescript
// app/api/execute/route.ts
import { NextResponse } from 'next/server';
import { executePythonSandbox } from '@/lib/sandbox-runner';
 
export const maxDuration = 30; // Extend Vercel function execution limit if needed
export const dynamic = 'force-dynamic';
 
export async function POST(request: Request) {
  try {
    const { code } = await request.json();
 
    if (!code || typeof code !== 'string') {
      return NextResponse.json(
        { error: 'Invalid or missing code payload' },
        { status: 400 }
      );
    }
 
    // Basic safety guardrail: block obvious local system calls
    // Note: The sandbox is isolated, but filtering prevents spamming the microVM
    if (code.includes('import os') && code.includes('os.environ')) {
      return NextResponse.json(
        { error: 'Accessing environment variables is prohibited.' },
        { status: 403 }
      );
    }
 
    const result = await executePythonSandbox(code);
 
    if (!result.success) {
      return NextResponse.json(
        { 
          error: 'Code execution failed', 
          details: result.error,
          stdout: result.stdout,
          stderr: result.stderr 
        },
        { status: 422 }
      );
    }
 
    return NextResponse.json({
      success: true,
      stdout: result.stdout,
      stderr: result.stderr,
      chart: result.chartUrl,
    });
 
  } catch (error: any) {
    return NextResponse.json(
      { error: 'Internal Server Error', details: error.message },
      { status: 500 }
    );
  }
}

Step 4: Integrating with LLM Tool Calling (AI Agents)

Running Docker containers inside Vercel Sandbox is highly powerful when paired with AI agents. By exposing the sandbox execution environment to an agent as a tool, you allow LLMs like Claude or GPT-4o to write and execute code on the fly to solve mathematical, analytical, or visual tasks.

If you are building complex agentic systems, check out our deep dives on building a production-ready MCP agent framework in TypeScript and how to configure Claude managed agents with Vercel Sandbox.

Here is how you can expose the runner as a structured tool using the Vercel AI SDK:

typescript
import { tool } from 'ai';
import { z } from 'zod';
import { executePythonSandbox } from '@/lib/sandbox-runner';
 
export const pythonInterpreterTool = tool({
  description: 'Execute Python code in a secure Docker sandbox containing pandas, numpy, and matplotlib. Use this to analyze data, run calculations, and generate charts.',
  parameters: z.object({
    code: z.string().describe('The Python code to execute. Save any charts as chart.png to retrieve them.'),
  }),
  execute: async ({ code }) => {
    const result = await executePythonSandbox(code);
    return {
      success: result.success,
      output: result.stdout.join('\n'),
      errors: result.stderr.join('\n'),
      chartUrl: result.chartUrl,
    };
  },
});

Production Gotchas & Best Practices

While Vercel Sandbox handles the underlying virtualization securely, running Docker containers in a serverless loop introduces unique operational challenges.

1. Cold Starts and MicroVM Warm-up Times

Creating a new sandbox instance via Sandbox.create() takes anywhere from 500ms to 1.5 seconds depending on the region and the size of your custom Docker image.

  • Mitigation: Keep your custom Dockerfiles minimal. Do not install heavy dependencies like CUDA or large machine learning models unless strictly necessary. Pre-compile as much as possible during the template build phase.

2. Zombie Sandboxes and Memory Leaks

If your Vercel Function encounters an unhandled exception or times out before invoking sandbox.close(), the microVM will remain active until its default idle timeout expires (usually 5-10 minutes). This can rapidly exhaust your concurrency limits and blow up your usage bill.

  • Mitigation: Wrap all sandbox code in strict try...finally blocks. Ensure that sandbox.close() is executed regardless of whether the code succeeded or failed.

3. Ephemeral Disk Space

The default microVM storage allocation is limited (typically around 2GB to 5GB). If your sandboxed code downloads large datasets, creates massive database files, or generates high-resolution media, the filesystem will run out of space, resulting in silent crashes.

  • Mitigation: Add disk cleanup steps directly into your python scripts or enforce file size limits inside your sandbox wrapper before downloading files back to your API layer.

4. Network Isolation

By default, these sandboxes have outbound internet access. If you are running untrusted code, malicious users can use your sandbox as a proxy to launch DDoS attacks, scan internal networks, or scrape protected endpoints.

  • Mitigation: If your executions do not require external packages at runtime, configure your sandbox template to disable network access entirely in the e2b.toml configuration by setting network_isolated = true.
ShareTweet

Related Posts