Build a Sandbox Template from a GHCR Custom Image

Updated at:

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_KEY for the target region.

  • The target region is US (Silicon Valley), with the following endpoints:

    • api_url: https://api.us-west-1.e2b.fc.aliyuncs.com

    • domain: 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 e2b SDK:

pip install e2b==2.31.0

Registry 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:packages and write:packagespackages scopes. read:packages is required for pulling the image during the build, and selecting write:packages automatically includes read:packages. Missing these scopes causes the image pull to fail during the build (auth 401 / 403; see the GitHub documentation).

Creation path: GitHub → SettingsDeveloper settingsPersonal access tokensTokens (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:

HeaderMeaningValue
X-E2B-Template-Source-UsernameRegistry usernameGitHub username
X-E2B-Template-Source-PasswordRegistry passwordPersonal 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:

  1. Declare the image source with Template().from_image(<image>), and pass the registry credentials via headers.

  2. Call Template.build to build the template, specifying cpu_count, memory_mb, and other specs, and obtain the template_id.

  3. Create a Sandbox with the template_id, wait for the data plane to be ready, then run a command as a smoke test.

  4. 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.py

On 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_KEY via 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 every Sandbox.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=False to reuse the cache and speed up builds; set it to True when 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_count and memory_mb based 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 a finally block) to avoid a Sandbox holding resources on error paths.