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.
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:
mkdir -p sandboxes/data-science-runner
cd sandboxes/data-science-runnerNow, 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.
# 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):
# 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:
npx @e2b/cli template buildThis 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:
npm install @e2b/code-interpreterIn 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:
// 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.
// 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:
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...finallyblocks. Ensure thatsandbox.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.tomlconfiguration by settingnetwork_isolated = true.
Related Posts
How to Run Claude Managed Agents with Vercel Sandbox
Learn how to securely run Claude managed agents with Vercel Sandbox. Build a production-ready, sandboxed execution environment for LLM-generated code using TypeScript.
Secure NestJS Backend Development: Fixing What AI Generation Gets Wrong
Learn how to implement secure NestJS backend development by fixing common security vulnerabilities found in AI-generated code, including mass assignment, data leaks, and insecure defaults.
Secure MCP Server Implementation: Exposing SaaS APIs to AI Agents Safely
Learn how to build a secure mcp server implementation in TypeScript. Control authentication, restrict access, and enforce server-side guardrails for AI agents.