Build a Sandbox Template from a GHCR Custom Image
FC Agent Sandbox can build a Sandbox template directly by pulling an image from a container registry. When your business needs to preinstall a specific language runtime, system dependencies, a private SDK, or a data science toolchain, packaging everything into a container image and building the template from that image is more controllable and reproducible than installing dependencies one by one at runtime.
This document uses a private image on GitHub Container Registry (GHCR) as an example, and shows how to build a template from ghcr.io/<owner>/<image>:<tag>, create a Sandbox, and verify the run in the US (Silicon Valley) region (us-west-1). GHCR is a registry that requires authentication, so you must provide pull credentials during the build.
Use cases
A team packages a unified Python/Node.js runtime and common dependencies into an image, and multiple Agents share the same base template.
Analysis tasks need heavy dependencies such as
pandas,numpy, and charting libraries preinstalled once at build time instead of at every run.A private SDK or internal tool is published only to a private GHCR repository and cannot be pulled publicly.
You need to align image versions with template versions so builds are traceable and can be rolled back.
Prerequisites
FC Agent Sandbox is enabled, and you have the
E2B_API_KEYfor the target region.The target region is US (Silicon Valley), with the following endpoints:
api_url:https://api.us-west-1.e2b.fc.aliyuncs.comdomain:us-west-1.e2b.fc.aliyuncs.com
An image is ready on GHCR, for example
ghcr.io/<owner>/python:3.10.GitHub credentials for pulling the image have been created (see "Registry credentials" below).
Install the
e2bSDK:
pip install e2b==2.31.0Registry credentials
Pulling an image from a private GHCR repository requires a username and password. The credential here is a GitHub Personal Access Token (classic), not your account login password:
Username: your GitHub username (the owner that the image belongs to).
Password: the value of the Personal Access Token (classic) you created (in the form
ghp_xxxxxxxx).
Token scope requirements: When creating a Personal Access Token (classic), select the
read:packagesandwrite:packagespackagesscopes.read:packagesis required for pulling the image during the build, and selectingwrite:packagesautomatically includesread:packages. Missing these scopes causes the image pull to fail during the build (auth 401 / 403; see the GitHub documentation).
Creation path: GitHub → Settings → Developer settings → Personal access tokens → Tokens (classic) → Generate new token (classic), and select read:packages and write:packages under Select scopes.
The build request passes credentials through the following two HTTP headers, injected via the SDK's headers parameter:
| Header | Meaning | Value |
X-E2B-Template-Source-Username | Registry username | GitHub username |
X-E2B-Template-Source-Password | Registry password | Personal Access Token (classic) |
Credentials are sensitive. Inject them via environment variables or a secrets manager. Do not hardcode them in code or commit them to a repository.
Recommended workflow
Split "custom image → template → Sandbox" into four steps:
Declare the image source with
Template().from_image(<image>), and pass the registry credentials viaheaders.Call
Template.buildto build the template, specifyingcpu_count,memory_mb, and other specs, and obtain thetemplate_id.Create a Sandbox with the
template_id, wait for the data plane to be ready, then run a command as a smoke test.Destroy the Sandbox after verification to avoid holding resources.
Building the template is a one-time action: the template_id built from an image can be reused by many subsequent Sandbox.create calls. In production, separate build and run into two stages instead of rebuilding on every run.
Example code
The following example builds a template from the private GHCR image ghcr.io/<owner>/python:3.10, creates a Sandbox, runs a short Python snippet to print a verification marker, and finally destroys the Sandbox. The username, password, and E2B_API_KEY are all read from environment variables.
import os
import time
import uuid
from e2b import Sandbox, Template, default_build_logger
API_URL = "https://api.us-west-1.e2b.fc.aliyuncs.com"
DOMAIN = "us-west-1.e2b.fc.aliyuncs.com"
SOURCE_IMAGE = "ghcr.io/<owner>/python:3.10"
SANDBOX_MARKER = "sandbox-ok"
READY_TIMEOUT_SECONDS = 90
READY_INTERVAL_SECONDS = 3
def required_env(name: str) -> str:
value = os.environ.get(name, "").strip()
if not value:
raise RuntimeError(f"Missing environment variable: {name}")
return value
def main() -> int:
# USERNAME=GitHub username; PASSWORD=Personal Access Token (classic)
username = required_env("USERNAME")
password = required_env("PASSWORD")
api_key = required_env("E2B_API_KEY")
name = f"custom-python-{int(time.time())}-{uuid.uuid4().hex[:8]}"
headers = {
"X-E2B-Template-Source-Username": username,
"X-E2B-Template-Source-Password": password,
}
# 1. Build the template from the private GHCR image
build = Template.build(
Template().from_image(SOURCE_IMAGE),
name=name,
cpu_count=2,
memory_mb=2048,
skip_cache=False,
on_build_logs=default_build_logger(),
headers=headers,
api_key=api_key,
api_url=API_URL,
domain=DOMAIN,
)
print(f"template_id={build.template_id}")
# 2. Create a Sandbox with the template_id
sandbox = Sandbox.create(
template=build.template_id,
timeout=900,
api_key=api_key,
api_url=API_URL,
domain=DOMAIN,
)
print(f"sandbox_id={sandbox.sandbox_id}")
try:
# 3. Wait for the data plane to be ready, then run a smoke test
last_error = None
deadline = time.monotonic() + READY_TIMEOUT_SECONDS
while time.monotonic() < deadline:
try:
result = sandbox.commands.run(
f"python3 - <<'PY'\nprint('{SANDBOX_MARKER}')\nPY",
timeout=60,
)
break
except Exception as err:
last_error = err
time.sleep(READY_INTERVAL_SECONDS)
else:
raise RuntimeError(f"Timed out waiting for the data plane: {last_error}") from last_error
if result.exit_code != 0 or SANDBOX_MARKER not in result.stdout:
raise RuntimeError(f"Verification failed: exit={result.exit_code}, stdout={result.stdout}")
print(f"stdout={result.stdout.strip()}")
finally:
# 4. Destroy the Sandbox
sandbox.kill()
return 0
if __name__ == "__main__":
raise SystemExit(main())How to run (keep credentials in environment variables):
export USERNAME="<github-username>"
export PASSWORD="ghp_xxxxxxxxxxxxxxxxxxxx" # Personal Access Token (classic)
export E2B_API_KEY="<your-e2b-api-key>"
python3 build_from_ghcr.pyOn success, the output prints the template build logs, template_id, sandbox_id, and the verification marker sandbox-ok in order, then destroys the Sandbox.
Production recommendations
Credential management: Inject the GitHub token and
E2B_API_KEYvia environment variables or a secrets manager. Never write them into code or commit them to Git. Follow the principle of least privilege for token scopes, and rotate tokens regularly.Separate build and run: Building a template is a one-time action. Persist and reuse the resulting
template_id; do not rebuild before everySandbox.create.Version your images: Use explicit tags (avoid relying on
latest) so templates and images stay aligned, traceable, and rollback-friendly.Build cache: During iteration, keep
skip_cache=Falseto reuse the cache and speed up builds; set it toTruewhen you need to force a rebuild.Wait for the data plane: The data plane may need a short initialization delay after a Sandbox is created. Add retry/timeout logic for command execution (like the wait loop in the example) instead of running commands immediately without retries.
Resource specs: Size
cpu_countandmemory_mbbased on the workload inside the image; scale up for heavy dependencies or large data processing.Destroy promptly: Call
sandbox.kill()when the task finishes (in afinallyblock) to avoid a Sandbox holding resources on error paths.