> ## Documentation Index
> Fetch the complete documentation index at: https://www.agent37.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a custom image

> Write a Dockerfile, build the image on Agent37's infrastructure or import it from a registry, and run instances from the pinned template.

You can run an Agent37 instance from any Docker image. Register it as a [workspace template](/docs/agents-api/templates) in one of two ways:

| Your image                       | Register with                                       | What Agent37 needs                                                                                                |
| -------------------------------- | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| Not built yet, local, or private | A [cloud build](#build-it-in-the-cloud)             | A folder with the `Dockerfile` and the files it `COPY`s. Agent37 runs the build; no local Docker needed.          |
| In a registry, public or private | `image_ref`, plus `registry_auth` for a private one | A fully qualified reference; for a private registry, a username and password Agent37 uses once to copy the image. |

Your Dockerfile and files upload straight to the build, so a private codebase never needs a registry. A `FROM` on a private registry and a `RUN` step that needs a token both work through [build secrets](#build-secrets).

A complete example lives in [agent37-platform/custom-agent-image](https://github.com/agent37-platform/custom-agent-image): a working agent app (auth, chat, files, integrations) whose agents run an image it builds itself from one `Dockerfile` and one skill folder. Click **Use this template**, edit the `Dockerfile`, run `npm run release:agent`. Smaller ready-to-build recipes live in the [Agent37 Cookbook](https://github.com/agent37-platform/examples), like [hermes-vnc-desktop](https://github.com/agent37-platform/examples/tree/main/custom-images/hermes-vnc-desktop), a live desktop view of the agent's browser.

This page is the walkthrough; [Templates → build on the Hermes base image](/docs/agents-api/templates#build-on-the-hermes-base-image) is the reference for the contract.

<Note>
  When you register a template, Agent37 copies your image once into private storage and pins it by digest. That snapshot is what every instance runs, so registering takes up to a few minutes for a large image. Re-pushing the same tag later does nothing: publish a new tag and [update the template](/docs/agents-api/templates#update-a-template), or re-run the [cloud build](#build-it-in-the-cloud). Your image is stored privately and never republished.
</Note>

```text title="Paste this into your coding agent" wrap theme={null}
Read https://www.agent37.com/docs/llms-full.txt.
I want to give my agent my own tools with a custom Docker image, and my own skills on top.
Start the Dockerfile FROM ghcr.io/agent37-platform/hermes-base:latest, bake the tools into /usr/local/bin, publish it with npx agent37 templates build . --name my-custom-agent, point the agent at the managed model using AGENT37_LLM_PROXY_URL and AGENT37_MANAGED_TOKEN, create the instance with a budget, and install the skills into ~/.hermes/skills over exec once it is running.
Done when the agent uses one of my tools in a chat turn.
My key is in AGENT37_API_KEY.
```

## 1. Choose a starting image

To customize Hermes, build on [`ghcr.io/agent37-platform/hermes-base`](https://github.com/orgs/agent37-platform/packages/container/package/hermes-base). It includes Hermes, the gateway, Chromium, and the standard toolchain. This example adds a CLI:

```dockerfile theme={null}
FROM ghcr.io/agent37-platform/hermes-base:latest

USER root
RUN apt-get update && apt-get install -y --no-install-recommends your-cli \
 && rm -rf /var/lib/apt/lists/*
USER node
```

Bake binaries into `/usr/local/bin` and everything else into `/usr/local` or `/opt`, never `/home/node` or `/home/linuxbrew`, which are masked at runtime. Keep the base `ENTRYPOINT`. See [the full contract](/docs/agents-api/templates#build-on-the-hermes-base-image).

<Note>
  **Skills don't belong in the image.** Hermes reads them from `~/.hermes/skills`, which lives on the instance's persistent volume, so anything the image writes there is masked. Install them into a running instance instead, over [exec](/docs/agents-api/exec) or the [files API](/docs/agents-api/files):

  ```bash curl theme={null}
  curl -X POST https://api.agent37.com/v1/instances/<id>/exec \
    -H "Authorization: Bearer $AGENT37_API_KEY" -H "Content-Type: application/json" \
    -d '{ "command": "mkdir -p ~/.hermes/skills/my-skill && echo '"'"'<base64 of SKILL.md>'"'"' | base64 -d > ~/.hermes/skills/my-skill/SKILL.md" }'
  ```

  An app that provisions agents does this once, right after create. The skill then persists across restarts and updates.
</Note>

You can also use an existing image or start from any base. Its main process must keep running. If it serves HTTP, bind it to `0.0.0.0` and note its listening port; you will pass that as `default_port` when you register the template. An image with no HTTP service can omit `default_port` and be driven through [exec](/docs/agents-api/exec), but it still needs a long-running `ENTRYPOINT` or `CMD`.

<Tip>
  Your built image freezes its base at build time. `:latest` is convenient while getting started; pin a dated Hermes base tag for reproducible production rebuilds. Find published tags [on GHCR](https://github.com/orgs/agent37-platform/packages/container/package/hermes-base), or read the current tag from `agent37-hermes`'s `image_ref` on `GET /v1/templates`.
</Tip>

## 2. Verify it locally (optional)

You don't need Docker to publish; the [cloud build](#build-it-in-the-cloud) does the real build. If you have Docker, a local build is still the fastest way to test before publishing. Plain `docker build` stores the amd64 result in your local Docker, including on an Apple Silicon Mac:

```bash theme={null}
docker build --platform linux/amd64 -t my-agent:v1 .
docker image inspect my-agent:v1 --format '{{.Os}}/{{.Architecture}}'
# linux/amd64
```

Run the image locally if it is practical: registration validates the image, but only a running container proves that its entrypoint and service work. If you take the [registry path](#import-a-registry-image), this local `linux/amd64` build is also the artifact you push.

## 3. Register the image

You have two ways to get the image to Agent37. Either way the resulting image is capped at **8 GB decimal** (`8,000,000,000` bytes), and either way the platform copies it once into private storage at registration.

<Note>
  Registration is synchronous and can take a few minutes for a large image. The returned `image_digest` identifies the immutable private copy every instance runs. Re-pushing a mutable source tag does not change that copy.
</Note>

### Build it in the cloud

Use this path when the image isn't in a registry yet, or you would rather not run one. You upload a small **build context** (the `Dockerfile` and the files it `COPY`s), and Agent37 builds the image on its own infrastructure:

```bash theme={null}
export AGENT37_API_KEY=sk_live_...
npx agent37 templates build . --name my-custom-agent
```

What it does:

* Packs the directory (default `.`; it must have `Dockerfile` at its root) into a gzipped context, excluding `.git` and your `.dockerignore` patterns (plain patterns only; `!` negations are ignored). The context is capped at **1 GB**; it holds the Dockerfile's inputs, not the image, so it stays small.
* **Everything else in the folder ships with the context**, a stray `.env` or key file included, and a `COPY . .` bakes it into the image. Check the folder, or add a `.dockerignore`, before you build.
* Builds the image on Agent37's infrastructure and **streams the live build log** to your terminal. On failure the command exits non-zero with the failing step visible.
* Publishes the result as the workspace template. `--name` defaults to the folder name; `--default-port <port>` sets the template's default port. Re-building an existing name publishes a new template revision; **existing instances never change**.
* Ctrl-C does not cancel the build; it continues server-side and still publishes on success.

Builds are free, run up to three at once per workspace, and time out after 45 minutes. The built image is capped at 8 GB decimal, like any template image; if you need more, [contact support](mailto:support@agent37.com). A template published this way has an `image_digest` but no `image_ref`, because there is no public registry reference.

To script the same flow without the CLI, see the raw contract in
[Templates → build an image in the cloud](/docs/agents-api/templates#build-an-image-in-the-cloud).

### Build secrets

A `RUN` step that needs a credential, cloning a private repository or installing from a private package index, takes it as a build secret. The value is available at `/run/secrets/<id>` for that step only, is never written into the image, and is not stored by Agent37.

```dockerfile theme={null}
FROM ghcr.io/agent37-platform/hermes-base:latest
RUN --mount=type=secret,id=GITHUB_TOKEN \
    git clone "https://x-access-token:$(cat /run/secrets/GITHUB_TOKEN)@github.com/acme/private-repo" /opt/app
```

```bash theme={null}
export GITHUB_TOKEN=ghp_...
npx agent37 templates build . --name my-custom-agent --secret id=GITHUB_TOKEN
```

`--secret id=NAME` reads the value from the environment variable `NAME`; `id=NAME,env=VAR` reads a different variable and `id=NAME,src=PATH` reads a file, the same grammar as `docker build --secret`. Pass the flag once per secret.

A `FROM` on a private registry takes the login the same way, and it is not stored either:

```bash theme={null}
npx agent37 templates build . --name my-custom-agent \
  --registry-auth registry.acme.com=bot:$REGISTRY_PASSWORD
```

Without the CLI, both go in the body of the start call; see [the raw contract](/docs/agents-api/templates#3-start-the-build).

### Import a registry image

Push the image to any OCI registry, then register its fully qualified reference. For example:

```bash theme={null}
docker tag my-agent:v1 ghcr.io/you/my-agent:v1
docker push ghcr.io/you/my-agent:v1
```

On GHCR, either make the package public after the first push (**Packages** → **Package settings** → **Change visibility** → **Public**) or keep it private and register it with `registry_auth` (a personal access token with `read:packages` as the password). In CI, GitHub's `ubuntu-latest` runner is already `linux/amd64`, and the built-in `GITHUB_TOKEN` can push to `ghcr.io/<owner>/<repo>` with `permissions: packages: write`. Tag the image with the commit sha and register that, never `latest`.

Register a specific tag, not `latest`:

```bash curl theme={null}
curl -X POST https://api.agent37.com/v1/templates \
  -H "Authorization: Bearer $AGENT37_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "my-custom-agent", "image_ref": "ghcr.io/you/my-agent:v1" }'
```

For a private registry, add the login beside the reference:

```bash curl theme={null}
curl -X POST https://api.agent37.com/v1/templates \
  -H "Authorization: Bearer $AGENT37_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"name\": \"my-custom-agent\",
    \"image_ref\": \"registry.acme.com/my-agent:v1\",
    \"registry_auth\": { \"username\": \"bot\", \"password\": \"$REGISTRY_PASSWORD\" }
  }"
```

Agent37 needs access only while it copies the image, and does not keep the login. The template and its instances no longer depend on the source registry after registration. A future image update needs a new tag, sent with `registry_auth` again if the registry is private.

<Tip>
  If your own image serves HTTP on port `8000`, declare it: pass `--default-port 8000` on the build, or include `"default_port": 8000` beside `name` and `image_ref`. The bare instance URL then routes to that port, and instance creation probes it at boot. Omit it for `hermes-base`, whose gateway uses the `3737` fallback, or for a long-running private sandbox with no HTTP service.
</Tip>

## 4. Create an instance

Register once, then create instances from the template name:

```bash curl theme={null}
curl -X POST https://api.agent37.com/v1/instances \
  -H "Authorization: Bearer $AGENT37_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "template": "my-custom-agent" }'
```

The result is a standard Agent37 instance running your image: same [lifecycle](/docs/agents-api/instances), [exec](/docs/agents-api/exec), and routed URLs as any other. Confirm your CLI shipped, without needing a model:

```bash curl theme={null}
curl -X POST https://api.agent37.com/v1/instances/<id>/exec \
  -H "Authorization: Bearer $AGENT37_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "command": "your-cli --version" }'
```

<Note>
  If the instance comes up `failed` instead of `running`, read its [logs](/docs/agents-api/logs). An entrypoint that exits, a missing binary, or a service that never listens on `default_port` are the usual causes. `exec` cannot help until the container is running.
</Note>

## 5. Give it a model

`hermes-base` is clean: it boots with no LLM provider wired. That does not mean the instance has
no model available. Every instance, including one running an image you built from scratch, gets
a working OpenAI-compatible endpoint and a Composio MCP server in its environment, as
`AGENT37_MANAGED_TOKEN`, `AGENT37_LLM_PROXY_URL`, and `AGENT37_COMPOSIO_MCP_URL`. Pointing your
agent at those is the
shortest path to a running agent, costs no setup, and meters to the instance
[budget](/docs/agents-api/budgets). [Managed services in your image](/docs/agents-api/managed-services) is
the reference, and [pi-agent-image](https://github.com/agent37-platform/pi-agent-image) is a
complete image that does it in two config files with no credentials in either.

<Warning>
  **Moving an existing instance onto your image? Its config already holds a dead token.** An
  instance that previously ran a stock template (`agent37-hermes` and friends) has managed
  entries, the Composio one included, written into its persistent `~/.hermes/config.yaml` with
  the token as a literal. `AGENT37_MANAGED_TOKEN` is [reissued on every
  restart](/docs/agents-api/managed-services#wiring-an-agent-to-both), stock images rewrite the file
  each boot, and your image is now the one that has to: otherwise those entries start returning
  `401` at the first restart. Rewrite them from env on **every** boot, unconditionally:

  ```bash entrypoint.sh theme={null}
  python3 - <<'PY'
  import os, yaml
  path = os.path.expanduser("~/.hermes/config.yaml")
  token = os.environ.get("AGENT37_MANAGED_TOKEN")
  url = os.environ.get("AGENT37_COMPOSIO_MCP_URL")
  try:
      cfg = yaml.safe_load(open(path)) or {}
  except FileNotFoundError:
      cfg = {}
  if token and url and "composio" in cfg.get("mcp_servers", {}):
      cfg["mcp_servers"]["composio"] = {"url": url, "headers": {"Authorization": f"Bearer {token}"}}
      yaml.safe_dump(cfg, open(path, "w"))
  PY
  ```

  The same applies to any other place a literal token landed, such as a `custom_providers`
  entry pointing at `AGENT37_LLM_PROXY_URL`.
</Warning>

### Bring your own model instead

To run an instance on your own model, point Hermes at any OpenAI-compatible endpoint, your own proxy or a provider directly, by writing `~/.hermes/config.yaml` on the instance:

```yaml theme={null}
model:
  provider: "custom:MyProvider"
  default: "moonshotai/kimi-k2.7-code"        # the model id your endpoint serves
custom_providers:
  - name: "MyProvider"
    base_url: "https://your-llm-proxy.example.com/v1"   # must end in /v1
    api_key: "your-proxy-token"
    api_mode: "chat_completions"
    model: "moonshotai/kimi-k2.7-code"
```

Your endpoint must serve the two OpenAI-compatible routes Hermes uses: `GET /v1/models` (to resolve the model id) and `POST /v1/chat/completions` (the turn). Anything that speaks them works: a provider directly, or a small proxy of your own that forwards to one with your key.

Write the config over [exec](/docs/agents-api/exec) or the instance terminal; it lives on the persistent volume, so it survives restarts. Then [send a message](/docs/agents-api/chat) and the agent runs on your model.

Want this as a finished app rather than a config file? [Use your own model](/docs/agents-api/byo-model) is a forkable kit that does exactly this per agent: your key behind a proxy, a revocable token per instance, and per-agent spend caps.

<Tip>
  Want chat to work out of the box on Agent37's managed model instead? Build `FROM ghcr.io/agent37-platform/hermes:<tag>` ([tags on GHCR](https://github.com/orgs/agent37-platform/packages/container/package/hermes)), which wires the managed model, and pass a [budget](/docs/agents-api/budgets) on create.
</Tip>

## Keep it current

Your image freezes its base; rebuilding is how it picks up platform updates. Each template revision is an immutable snapshot, so a new build takes effect only after the template changes: re-run the [cloud build](#build-it-in-the-cloud) under the same name, or publish a **new registry tag** and [PATCH `image_ref`](/docs/agents-api/templates#update-a-template). Every successful build and every changed `image_ref` increments the template's automatic `revision`; a re-build counts even when its digest matches the previous image. PATCHing the same `image_ref` string deliberately reuses the existing snapshot and revision.

Existing instances keep their installed `template_revision`. [Update each instance](/docs/agents-api/instances#update) to recreate it from the template's current revision. Anything on the persistent volume (`~/.hermes/skills`, `~/.hermes/config.yaml`, the agent's files) survives that update untouched, so a changed skill needs the same [exec](/docs/agents-api/exec) write again, not a rebuild.

## Troubleshooting

| Error or symptom                  | What to check                                                                                                                                                                |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `build_failed`                    | A Dockerfile step failed. Read the build log: the CLI streams it, and `GET /v1/template-builds/{id}/logs` keeps it readable afterward; the failing step is at the end.       |
| `build_conflict`                  | Three builds run at a time per workspace. Wait for one to finish, then start again.                                                                                          |
| `build_timeout`                   | The build ran past the 45-minute limit. Trim the Dockerfile's slowest steps and retry.                                                                                       |
| `image_too_large`                 | The built or imported image is over 8 GB decimal. Remove build caches and unnecessary layers, or contact support.                                                            |
| `invalid_request` for `image_ref` | Use a fully qualified tag. For a private registry, pass `registry_auth` with a username and password that can pull the image; the message says which was missing or refused. |
| `image_ingest_failed`             | The private copy failed on Agent37's side. Retry once by re-running the build or the registration, then contact `support@agent37.com`.                                       |
| Instance status is `failed`       | Read [instance logs](/docs/agents-api/logs); confirm the main process stays alive and `default_port` listens when set.                                                            |
