# Custom images
URL: https://metorial.com/docs/build/sandboxes/custom-images

Build a machine image from your own Dockerfile.

---

A custom image boots machines with your tools, packages, and files already in place. You build it from a Dockerfile, and Metorial makes it available in every region.

<PageIntro>
  <Learn>
    * How to build an image in the dashboard
    * How images, versions, and builds relate
    * How to build from the CLI or SDK
  </Learn>

  <Reading title="Before you start">
    * A Dockerfile that builds locally with `docker build`
    * [Run your first machine](/docs/build/sandboxes/quickstart)
  </Reading>
</PageIntro>

## How images work [#how-images-work]

An image is a named container for versions. Every build creates a new numbered version of the image, and new machines boot from the latest version that built successfully. A machine's `image.version_id` shows which version it runs.

An image stays `pending` until its first build succeeds. After that it's `active`, and you can create machines from it.

## Build an image in the dashboard [#build-an-image-in-the-dashboard]

<Steps>
  <Step title="Open machine images">
    In the dashboard, go to **Developer**, then **Sandbox**, then **Machine Images**.

        <img alt="The machine images list in the dashboard" src="__img0" />
  </Step>

  <Step title="Create the image">
    Click **Create Image** and fill in the form:

    * **Name**: a name you'll recognize in lists.
    * **Build Context**: a single Dockerfile, or a `.zip`, `.tar`, `.tar.gz`, or `.tgz` archive up to 50 MB. An archive must contain the Dockerfile and every file it copies.
    * **Dockerfile Path** and **Context Path**: where the Dockerfile and the build context sit inside the archive. They default to `Dockerfile` and `.`.

    Under **Advanced**, you can set a build **Target** stage, up to 64 **Build Arguments**, a **Timeout** (30 minutes by default), and the **Machine Disk Size**, which is the root disk size of machines that boot from this version (16 GiB by default).

        <img alt="The Create Image dialog with the advanced options expanded" src="__img1" />

    A build records the names of its build arguments but never their values.
  </Step>

  <Step title="Watch the build">
    The image opens on its **Versions** tab. Each version shows its build status and the regions it's available in.

        <img alt="The Versions tab showing version 1 succeeded in two regions" src="__img2" />

    Open a version to follow the build log live.

        <img alt="A version page with build attributes and the build log" src="__img3" />
  </Step>

  <Step title="Create a machine">
    When the build succeeds, click **Create Machine** on the image or version page.
  </Step>
</Steps>

To ship changes, click **New Version** on the image page and upload a new build context. **Rebuild** on a version page builds the same context again as a new version.

## Build with the CLI [#build-with-the-cli]

<Steps>
  <Step title="Upload the build context">
    Pass a directory and the CLI zips it for you:

    ```bash
    metorial-admin files upload ./my-image --purpose sandbox_image_build_context
    ```

    Save the file ID from the output as `FILE`.
  </Step>

  <Step title="Create the image and start a build">
    ```bash
    metorial-admin sandbox images create --name python-data-agent
    metorial-admin sandbox images builds create $IMAGE --file-id $FILE
    ```

    `builds create` also takes `--dockerfile-path`, `--context-path`, `--target`, `--build-args '{"PYTHON_VERSION": "3.12"}'`, `--timeout-seconds`, and `--disk-size-bytes`.
  </Step>

  <Step title="Follow the build">
    ```bash
    metorial-admin sandbox image-builds get $BUILD
    metorial-admin sandbox image-builds logs $BUILD
    ```

    Stop a build with `image-builds cancel $BUILD`. List an image's versions with `images versions list $IMAGE`.
  </Step>
</Steps>

## Build with the SDK [#build-with-the-sdk]

Start builds from code to rebuild an image when your dependencies change. Upload the build context with the CLI first, then pass its file ID.

<CodeBlockTabs defaultValue="TypeScript" groupId="language" mode="code">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="TypeScript">
      TypeScript
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Python">
      Python
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="TypeScript">
    ```typescript
    let build = await metorial.sandbox.images.builds.create(imageId, {
      fileId: 'your_file_id',
      buildArgs: { PYTHON_VERSION: '3.12' }
    });

    let done = ['succeeded', 'failed', 'cancelled', 'timed_out'];
    while (!done.includes(build.status)) {
      await new Promise(resolve => setTimeout(resolve, 5000));
      build = await metorial.sandbox.imageBuilds.get(build.id);
    }

    console.log(build.status, build.versionNumber);
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Python">
    ```python
    import time

    build = metorial.sandbox.images.builds.create(
        image_id,
        file_id="your_file_id",
        build_args={"PYTHON_VERSION": "3.12"},
    )

    done = {"succeeded", "failed", "cancelled", "timed_out"}
    while build.status not in done:
        time.sleep(5)
        build = metorial.sandbox.image_builds.get(build.id)

    print(build.status, build.version_number)
    ```
  </CodeBlockTab>
</CodeBlockTabs>

When a build fails, its `failure` field holds a code and a message, and the build log shows the Docker output.