- Introduction
- Prerequisites
- Deployment (New VPC)
- Deployment (Existing VPC)
- Using E2B CLI
- E2B SDK Cookbook
- Snapshot Retention
- Troubleshooting
- Resource Cleanup
- License
E2B on AWS provides a secure, scalable, and customizable environment for running AI agent sandboxes in your own AWS account. This project addresses the growing need for organizations to maintain control over their AI infrastructure while leveraging the power of E2B's sandbox technology for AI agent development, testing, and deployment.
If you encounter any issues, please submit a PR directly. Special thanks to all contributors involved in the project transformation.
The code layer (packages/, spec/, scripts/, tests/, firecracker/) is a
byte-for-byte copy of
e2b-dev/infra at
225f963,
with two exceptions: three packages this deployment does not build and so does
not vendor (dashboard-api, local-dev, nomad-nodepool-apm — none of them
appear in go.work), and upstream's .env* samples, which .gitignore
excludes. A sync replaces that layer wholesale rather than merging into it. The
deployment layer (CloudFormation, infra-iac/, nomad/) is specific to this
repository.
| Requirement | Description |
|---|---|
| AWS Account | With appropriate permissions |
| Domain Name | A domain you own (Cloudflare recommended) |
| Grafana Account | (Optional) For monitoring and logging |
| Posthog Account | (Optional) For analytics |
🔒 Production Security Checklist
Before deploying to production, verify these critical security and reliability settings are enabled:
DB_INSTANCE_BACKUP_ENABLEDRDS_AUTOMATIC_MINOR_VERSION_UPGRADE_ENABLEDRDS_ENHANCED_MONITORING_ENABLEDRDS_INSTANCE_LOGGING_ENABLEDRDS_MULTI_AZ_SUPPORTS3_BUCKET_LOGGING_ENABLEDIMDSv2 enforced- Instance Metadata Service v2 is required on all EC2 instances (HttpTokens: required)
To deploy into an existing VPC instead, see Deployment (Existing VPC).
- Clone this repository
- Open AWS CloudFormation console and create a new stack
- Upload the
e2b-setup-env.ymlfile - Configure the following parameters:
| Parameter | Description |
|---|---|
| Stack Name | Must be lowercase (e.g., e2b-infra) |
| VPC Configuration | New VPC environment configuration |
| Environment | dev or prod (prod has stricter resource protection) |
| Architecture | x64 or AWS Graviton |
| Domain | A domain you own (e.g., example.com) |
| EC2 Key Pair | Existing key pair for SSH access |
| AllowRemoteSSHIPs | IP range for SSH access (defaults to private networks) |
| Database Settings | RDS password: 8-30 characters with letters and numbers |
- Complete all required fields and launch the stack
Note: See AWS Graviton Technical Guide for Graviton best practices.
- Navigate to Amazon Certificate Manager (ACM)
- Find your domain certificate and note the required CNAME record
- Add the CNAME record to your DNS settings (Cloudflare DNS)
- Wait for domain validation (typically 5 minutes)
# Option A: SSH with your key pair
ssh -i your-key.pem ubuntu@<instance-ip>
# Option B: AWS Session Manager from the EC2 consoleThe whole bootstrap writes to a single log, /tmp/e2b.log — the toolchain
install, every deployment step, and the output of each step:
sudo su root
tail -f /tmp/e2b.logIf you left AutoDeploy=true (the default), the chain is already running and
there is nothing to start. Follow it in the log above.
If you set AutoDeploy=false, the stack installed the toolchain and cloned
this repository but ran nothing. One command does the rest:
cd /opt/infra/sample-e2b-on-aws
sudo bash deploy-all.shIt runs the same steps, in the same order, that AutoDeploy=true would:
init → packer → terraform → init-db → build → prepare → deploy →
create-template. Each step that succeeds writes /opt/.e2b-step-<name>.done,
so if one fails you can fix the cause and re-run the script — it resumes at the
step that broke instead of repeating the work before it.
sudo bash deploy-all.sh --list # steps, and which are already done
sudo bash deploy-all.sh --skip-template # stop after deploy, no test template
sudo bash deploy-all.sh --only terraform # re-run one step, ignoring its marker
sudo bash deploy-all.sh --force # clear all markers and start over
sudo bash deploy-all.sh --helpNote: the full chain took about 45 minutes on an
x86_64devstack.build(compiling and pushing the service images, ~19 min) andpacker(the AMI, ~14 min) dominate; everything else is minutes.
- Wildcard DNS: Add a
*CNAME record pointing to the Application Load Balancer (ALB) DNS name - Nomad Dashboard: Navigate to
https://nomad.<your-domain> - Retrieve Token: Run
cat /opt/config.propertiesto get the Nomad management token
📊 Logging & Monitoring (Optional)
The logging and monitoring stack consists of three components deployed via nomad/deploy.sh --all:
| Component | Type | Purpose | Port |
|---|---|---|---|
| OTel Collector | system (all nodes) | Collects metrics, traces, and application logs via OTLP | 4317 (gRPC), 4318 (HTTP) |
| Logs Collector (Vector) | system (all nodes) | Collects sandbox user logs, routes to Loki | 30006 |
| Loki | service (api node) | Log storage and query engine, backed by S3 | 3100 |
Architecture:
┌─────────────────────────────┐ ┌──────────────────────┐
│ Go Services (api/orch/proxy)│ │ Sandbox envd │
│ OTel SDK → gRPC :4317 │ │ HTTP → :30006 │
└──────────┬──────────────────┘ └──────────┬───────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ OTel Collector │ │ Vector (logs-coll) │
│ Metrics/Traces/Logs │ │ Sandbox user logs │
│ → Customer endpoint │ │ → Loki │
└──────────────────────┘ └──────────┬───────────┘
▼
┌──────────────────────┐
│ Loki │
│ Storage: S3 bucket │
└──────────────────────┘
Sandbox user logs are the one part of this stack that needs no external backend:
Vector ships them to a Loki that runs in the cluster and stores in S3. Two jobs,
Loki first because Vector's sink resolves loki.service.consul:
source nomad/nomad.sh # exports NOMAD_ADDR / NOMAD_TOKEN
bash nomad/deploy.sh loki
bash nomad/deploy.sh logs-collectorlogs-collector is a system job, so it lands one allocation per node — the api,
build and sandbox nodes all ship logs. Storage is the Loki bucket CloudFormation
created, {stack-name}-loki-{account-id}; retention is Loki's default, not S3
lifecycle, so set a lifecycle rule on that bucket if the volume matters to you.
Deploy this even if you have no OTel backend. Without logs-collector the
services keep POSTing to localhost:30006, nothing is listening, and the log line
is dropped — which is how a template build failure once surfaced only as
Build failed: An internal error occurred with the real cause
(stat /fc-versions/...: no such file or directory) discarded.
Loki has no UI of its own. It listens on the private address of the node its allocation landed on, so find that node and query the HTTP API from the bastion:
source nomad/nomad.sh
ALLOC=$(nomad job status loki | sed -n '/^Allocations/,$p' | awk 'NR==3{print $1}')
NODE=$(nomad alloc status -json "$ALLOC" | jq -r .NodeID)
LOKI=$(nomad node status -json "$NODE" | jq -r .HTTPAddr | cut -d: -f1)
# What labels exist yet
curl -s "http://$LOKI:3100/loki/api/v1/labels" | jq -c .data
# ["buildID","category","envID","sandboxID","service","source","teamID"]
# Everything one sandbox emitted in the last 10 minutes
curl -s --get "http://$LOKI:3100/loki/api/v1/query_range" \
--data-urlencode 'query={sandboxID="<sandbox-id>"}' \
--data-urlencode "start=$(( $(date +%s) - 600 ))000000000" \
--data-urlencode 'limit=50' | jq -r '.data.result[]?.values[]?[1]'Each line is a JSON object carrying the command, pid, exit status, teamID and
envID, so {service="envd"} gives every sandbox's activity and
{sandboxID="..."} narrows it to one. An empty label list means no logs have
arrived yet — either logs-collector is not deployed or no sandbox has run.
Consul DNS is not resolvable from the bastion, which is why the address is looked up through Nomad rather than by using
loki.service.consuldirectly. Inside the cluster that name works, and it is what Vector and the API use.
Point the cluster at your own OTLP/HTTP backend — Grafana Cloud, Datadog, Honeycomb, New Relic, or a collector you run. Three steps, and the only thing you have to know is the endpoint:
# 1. Write the endpoint into config.properties
cat << EOF >> /opt/config.properties
# Customer OTel endpoint. http:// for plaintext, https:// for TLS.
otel_customer_endpoint=https://your-otel-backend:4318
EOF
# 2. Re-render the deploy HCLs so envsubst injects it
bash nomad/prepare.sh
# 3. Deploy the monitoring components
bash nomad/deploy.sh --allIf your backend needs authentication — every hosted one does — add the header it expects. One header covers the common backends:
cat << EOF >> /opt/config.properties
otel_customer_header_name=Authorization
otel_customer_header_value=Basic <base64 of instanceID:token>
EOF| Backend | otel_customer_header_name |
otel_customer_header_value |
|---|---|---|
| Grafana Cloud | Authorization |
Basic <base64(instanceID:token)> |
| Datadog | DD-API-KEY |
your API key |
| Honeycomb | x-honeycomb-team |
your ingest key |
| New Relic | api-key |
your licence key |
| Self-hosted, no auth | (leave both unset) |
Both keys are optional and independent of the endpoint: leave them out and the exporter sends no headers, which is what an unauthenticated collector wants.
Important:
otel_customer_endpointmust be in/opt/config.propertiesbeforenomad/prepare.shruns.prepare.shrendersorigin/*.hcl→deploy/*-deploy.hclwithenvsubst; with no endpoint the exporter gets an empty one and the otel-collector job fails to start — deliberately, since forwarding nowhere is a misconfiguration rather than a default.
No backend yet? Sandbox user logs do not need one:
logs-collectorwrites them to the in-cluster Loki, sobash nomad/deploy.sh logs-collectoralone gives you searchable sandbox logs through Loki's HTTP API. Only the OTel pipeline (metrics, traces, service logs) requires an endpoint to send to.
| Data Type | Source | Pipeline | Storage |
|---|---|---|---|
| Metrics (application) | Go services OTel SDK | → OTel Collector → Customer endpoint | External |
| Metrics (infrastructure) | Nomad /v1/metrics |
→ OTel Collector (Prometheus scrape) → Customer endpoint | External |
| Traces | Go services OTel SDK | → OTel Collector → Customer endpoint | External |
| Application Logs | Go services zap logger | → OTel Collector (OTLP log bridge) → Customer endpoint | External |
| Sandbox User Logs | envd → orchestrator | → Vector (:30006) → Loki (:3100) | S3 (Loki bucket) |
Note: Sandbox user logs always go through Vector → Loki → S3, independent of the OTel pipeline. The Loki S3 bucket is created by CloudFormation (
{stack-name}-loki-{account-id}); Terraform only grants the nodes access to it.
Create a template:
# Create from e2bdev/code-interpreter (default)
bash packages/create_template.sh
# Create from a Dockerfile
bash packages/create_template.sh --docker-file <Docker_File_Path>
# Example: Desktop
bash packages/create_template.sh --docker-file test_use_case/Dockerfile/e2b.Dockerfile.Desktop
# Example: BrowserUse
bash packages/create_template.sh --docker-file test_use_case/Dockerfile/e2b.Dockerfile.BrowserUse
# Example: S3FS
bash packages/create_template.sh --docker-file test_use_case/Dockerfile/e2b.Dockerfile.s3fs
# Example: Code Interpreter (customized)
bash packages/create_template.sh --docker-file test_use_case/Dockerfile/e2b.Dockerfile.code_interpreter
# Create from an ECR image in your own account
bash packages/create_template.sh --ecr-image <ECR_IMAGE_URI>Create a sandbox:
# Get e2b_API value from: cat ../infra-iac/db/config.json
curl -X POST \
https://api.<e2bdomain>/sandboxes \
-H "X-API-Key: <e2b_API>" \
-H 'Content-Type: application/json' \
-d '{
"templateID": "<template_ID>",
"timeout": 3600,
"autoPause": true,
"metadata": { "purpose": "test" }
}'If you already have a VPC with subnets configured, use the e2b-setup-env-existing-vpc.yml template instead.
- Open AWS CloudFormation console and create a new stack
- Upload the
e2b-setup-env-existing-vpc.ymlfile - Configure the following parameters:
| Parameter | Description |
|---|---|
| Stack Name | Must be lowercase (e.g., e2b-infra) |
ExistingVpcId |
Your existing VPC ID |
ExistingPrivateSubnet1Id / 2Id |
Private subnet IDs (two AZs) |
ExistingPublicSubnet1Id / 2Id |
Public subnet IDs (two AZs) |
PublicAccess |
public or private access mode |
| Architecture, Domain, Key Pair, DB | Same as standard deployment |
- The template automatically discovers your VPC CIDR block via a Lambda function
Domain validation, bastion access, DNS setup, monitoring, and testing follow the same process as the standard deployment starting from Step 2.
Note: The existing VPC template uses Aurora Serverless PostgreSQL and Redis Serverless.
# Installation Guide: https://e2b.dev/docs/cli
# macOS
brew install e2b
# Export environment variables
# (query teamApiKey from /opt/config.properties)
#
# E2B_ACCESS_TOKEN is not set here: upstream dropped the access_tokens table, so
# this deployment no longer issues an sk_e2b_ token. CLI commands that take a
# team API key work; the ones that authenticate as a user need an auth provider,
# which this deployment does not run.
export E2B_API_KEY=xxx
export E2B_DOMAIN="<e2bdomain>"
# Common commands
e2b sandbox list # List all sandboxes
e2b sandbox connect <sandbox-id> # Connect to a sandbox
e2b sandbox kill <sandbox-id> # Kill a sandbox
e2b sandbox kill --all # Kill all sandboxes| Client | Version | Install |
|---|---|---|
| E2B CLI | 2.16.1 |
npm i -g @e2b/cli (or brew install e2b) |
Python SDK e2b |
2.46.0 |
pip install e2b-code-interpreter pulls it in |
Python SDK e2b-code-interpreter |
2.9.2 |
pip install e2b-code-interpreter |
envd (in-sandbox agent, server side) |
0.7.0 |
built by the deploy chain, not installed by you |
Verified against the code layer at upstream 225f963. envd is reported by the
API on every create (envdVersion) and by e2b sandbox list, so it is the
quickest way to confirm which build a sandbox actually came from.
Only E2B_API_KEY and E2B_DOMAIN are needed — e2b sandbox list and
e2b template list were both checked with no E2B_ACCESS_TOKEN set, which is
what this deployment can offer now that upstream dropped user access tokens.
Two scripts in tools/ exercise the API against a running deployment:
python3 tools/api-smoke-test.py # auth, templates, sandbox lifecycle
python3 tools/api-load-test.py --sandboxes 40 # concurrency: burst, fan-out, churn
# add --replicas <ip:port>,<ip:port> to also assert the API replicas share stateBoth read the credentials the deploy chain wrote, so there is nothing to export first, and both clean up every sandbox they create.
git clone https://github.com/e2b-dev/e2b-cookbook.git
cd e2b-cookbook/examples/hello-world-python
poetry install
# Edit .env and set E2B_API_KEY
vim .env
poetry run startPausing a sandbox writes a snapshot into the templates bucket, next to the
template builds it was layered on, and nothing upstream ever deletes it — not
even DELETE /sandboxes/{id}, which only hides the database row. The
snapshot-retention Nomad job (nomad/origin/snapshot-retention.hcl, built
from tools/snapshot-retention) runs daily at 03:00 UTC and applies one policy:
- A paused sandbox whose last pause is older than 90 days is soft-deleted:
it disappears from
e2b sandbox listand cannot be resumed, exactly as if it had been deleted through the API. - 7 days later its objects are deleted from the bucket and its build rows from the database — but only after the job has confirmed, from the headers of every live template and snapshot, that nothing still reads those blocks. A fork's checkpoint, a snapshot template, or a template built from one keeps its ancestors alive. Template objects are never touched.
- Snapshots deleted through the API follow the same 90-day / 7-day rule.
Why a job rather than an S3 lifecycle rule: snapshots are diffs. A newer snapshot's header points at blocks of older snapshots and of the template, and a running sandbox reads them lazily. Expiring objects by age would corrupt them.
The job ships as a dry run: it logs every MARK and PURGE it would make
and changes nothing. Review a run, then enable it:
# Trigger a run now and read its log
nomad job periodic force snapshot-retention
nomad job status snapshot-retention # child job -> allocation id
nomad alloc logs <alloc-id>
# Enable deletion
echo "RETENTION_APPLY=true" >> /opt/config.properties
bash nomad/prepare.sh
bash nomad/deploy.sh snapshot-retentionOnly one run executes at a time: a manual run started while the nightly one is
active exits at once with another snapshot-retention run holds the lock.
Within the 7-day window a marked sandbox can be brought back by clearing the
soft delete on its snapshot env (the env in the MARK log line). The extra
conditions keep a mistyped id from resurrecting a template someone deleted on
purpose:
UPDATE envs SET deleted_at = NULL
WHERE id = '<env-id>' AND source = 'snapshot' AND deleted_at IS NOT NULL;The same statement covers the one edge case the job does not handle itself: a sandbox that was running at the moment it was marked stays hidden after its next pause. Its objects are safe — that pause keeps them for another 90 days — it just needs the soft delete cleared to show up again. These envs are the ones that gained a build after they were soft-deleted, so they can be listed without the log:
SELECT e.id, s.sandbox_id, e.deleted_at
FROM envs e JOIN snapshots s ON s.env_id = e.id
WHERE e.source = 'snapshot' AND e.deleted_at IS NOT NULL
AND EXISTS (SELECT 1 FROM env_build_assignments a JOIN env_builds b ON b.id = a.build_id
WHERE a.env_id = e.id AND GREATEST(b.created_at, a.created_at) > e.deleted_at);RETENTION_DAYS and PURGE_DELAY_DAYS live in the job spec. The delay must
exceed the longest sandbox lifetime (tiers.max_length_hours); the job refuses
to run otherwise.
The tool is compiled against the vendored code layer and pinned to the newest
database migration it was verified with (verifiedMigration in
tools/snapshot-retention/schema.go). After an upstream sync that adds
migrations, go test for the tool fails until the constant is bumped, and a
deployed binary that meets a newer schema runs as a dry run and exits non-zero.
Treat a failing build or test of tools/snapshot-retention as a failed sync.
No nodes were eligible for evaluation
Check node status and constraints in the Nomad dashboard.
Driver Failure: Failed to pull from ECR
Error: pull access denied ... Your authorization token has expired
Solution: Execute aws ecr get-login-password --region us-east-1 to get a new ECR token and update the HCL file.
For other unresolved issues, contact support.
When you need to delete the E2B environment, follow these steps:
1. Terraform Resource Cleanup
cd ~/infra-iac/terraform/
terraform destroyNote: S3 Buckets must be manually emptied first. ALBs may require manual deletion through the AWS console.
2. CloudFormation Stack Cleanup
- Disable RDS deletion protection through the RDS console first
- Then delete the CloudFormation stack
3. Manual Verification
After automated cleanup, verify in the AWS console that all resources are removed:
| Service | Check |
|---|---|
| EC2 | Instances, Security Groups, Load Balancers |
| S3 | Buckets |
| RDS | Database instances |
| ECR | Container repositories |
See CONTRIBUTING for more information.
This project is licensed under the Apache-2.0 License.