Images and the registry
Every workspace has its own private namespace in the caas.sh container registry, registry.caas.sh/<tenant-id>/. Push an image there with Docker, then run it in a sandbox, an MCP server or a workflow step. Only your workspace's API keys, and your workspace's own sandboxes, can pull it.
This guide covers logging in, naming, building and pushing, running an image by its digest, listing tags, pricing and deleting images. The CLI examples assume you've installed and configured caas (see The CLI).
Log in to the registry
The registry takes the same API keys as the API. Docker logs in with your workspace's tenant ID as the username and an API key as the password.
Create an API key
API keys are created only in the console:
Sign in to the console and open Settings.
Under Workspace, copy the Tenant ID.
Under API keys, enter a Key name, such as
docker on my laptop, and choose Create key.Copy the key. It starts with
ck_, and the console shows it only once.
Owners and operators can create keys; viewers can't. A new key can push and pull images.
Log in with Docker
export CAAS_TENANT=<tenant-id>
export CAAS_API_KEY=<api-key>
echo "$CAAS_API_KEY" | docker login registry.caas.sh --username "$CAAS_TENANT" --password-stdinLog in with the CLI
If you've saved your tenant ID and key with caas configure, log Docker in with them:
caas registry loginIt runs docker login registry.caas.sh with your tenant ID as the username and your saved key as the password.
What a key can do
| Access | Needs | Lets the key |
|---|---|---|
registry:read | Any key from the console | Pull images and list tags |
registry:write | Any key from the console, while its creator is an owner or operator | Push and delete images, and pull them |
A key only ever reaches its own workspace's namespace. If the member who created a key becomes a viewer, the key can still pull but no longer push. A revoked key stops working within a minute. See The API for scopes and roles.
Name your images
An image in your registry is named registry.caas.sh/<tenant-id>/<name>:<tag>, for example registry.caas.sh/<tenant-id>/agent:1.2.0.
<tenant-id>is your workspace's Tenant ID. You can only push to and pull from your own.<name>is one to four parts separated by/, such asagentortools/python. Each part is lowercase letters and digits, which.,_,__or hyphens may join, as inmy-agentornode_24.tools.<tenant-id>/<name>is at most 255 characters.<tag>is up to 128 letters, digits,_,.and-, and doesn't start with.or-.
Build and push an image
Sandboxes run linux/amd64 images. Build for that platform, even on an Arm Mac, then push:
docker build --platform linux/amd64 -t registry.caas.sh/$CAAS_TENANT/agent:1.2.0 .
docker push registry.caas.sh/$CAAS_TENANT/agent:1.2.0docker push ends with the image's digest:
1.2.0: digest: sha256:4f1c…e9a2 size: 1784A multi-platform image works too, as long as it includes linux/amd64.
To look up a tag's digest later, ask the registry for its manifest's headers:
curl -sI -u "$CAAS_TENANT:$CAAS_API_KEY" \
https://registry.caas.sh/v2/$CAAS_TENANT/agent/manifests/1.2.0 | grep -i docker-content-digestTo pull an image to your own machine, log in, then use docker pull as usual:
docker pull registry.caas.sh/$CAAS_TENANT/agent:1.2.0What the image needs
/bin/sh,base64,tarandgzip. caas.sh runs your commands, moves files and takes snapshots inside the image's container, with these tools.Node, for a stdio MCP server. A server that speaks MCP over stdio runs behind caas.sh's bridge, which runs on Node. A server that serves MCP over HTTP doesn't need it.
Layers under about 100 MB.
docker pushuploads each layer in one request, and a layer over about 100 MB can exceed the registry's upload limit. Split large installs across severalRUNinstructions to keep each layer smaller.
Run your image
Sandboxes, MCP servers and workflow steps run an image pinned to its digest, registry.caas.sh/<tenant-id>/<name>@sha256:…, so what runs is exactly the image you pushed, even if you push the tag again later. The CLI looks tags up for you; the console and the API take the digest.
Before anything is created, caas.sh checks that the digest exists in your namespace.
Sizes
A sandbox or MCP server on your own image needs size M (1/2 vCPU, 4 GiB of memory, 8 GB of disk) or larger, or a custom size with at least 4 GiB of memory and 8 GB of disk. XS and S are refused with sandboxes on your own image need size M or larger. See Sizes.
A workflow machine's size is rounded up to at least 4 GiB of memory and 8 GB of disk when it runs your own image.
In a sandbox
The command you give replaces the image's entrypoint and command, and a sandbox on your own image needs one. It runs in /workspace, which is mounted into the image's container.
In the console:
Open Sandboxes and choose New sandbox.
Under 1. Choose an image, pick Your image.
In Image, enter the digest reference,
registry.caas.sh/<tenant-id>/agent@sha256:….In Command, enter what the sandbox runs, such as
node agent.js.Under 2. Configure, pick size M or larger, and choose Create sandbox.
With the CLI, --image takes a tag or a digest:
caas run --image agent:1.2.0 -- node agent.js
caas run --image agent@sha256:<digest> --size l -- node agent.jsYou can write the image as NAME[:TAG] or NAME@sha256:DIGEST, optionally under registry.caas.sh/ or registry.caas.sh/<tenant-id>/. Without a tag, the CLI uses latest. It looks the tag up in the registry, which needs registry:read, and creates the sandbox with the digest. Images from other registries or other workspaces are refused with push the image to registry.caas.sh/<tenant-id>/NAME first.
With the API, put the digest reference in the source:
curl https://api.caas.sh/v1/tenants/$CAAS_TENANT/sandboxes \
-H "Authorization: Bearer $CAAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"kind": "agent",
"size": "m",
"source": {
"type": "image",
"reference": "registry.caas.sh/<tenant-id>/agent@sha256:<digest>",
"command": ["node", "agent.js"]
}
}'The API doesn't take tags. A reference in another workspace's namespace gets 400 with image must belong to this tenant, and a digest the registry doesn't hold gets 400 with image digest not found in registry.caas.sh. Sandboxes covers the other fields.
In an MCP server
caas mcp deploy --image server:1.2.3 deploys an MCP server from your image. Without --command, the image's own entrypoint and command start the server. MCP servers covers stdio and HTTP servers on your own image.
In a workflow step
Set image at the top of the workflow file for every step, under a named sandbox, or on a step that has its own sandbox. A step's image replaces the file's:
workflow: smoke-test
image: registry.caas.sh/agent:1.2.0
steps:
- id: agent-version
name: Check the agent image
run: node --version
- id: python-version
name: Check the Python image
image: registry.caas.sh/tools/python:3.12
run: python3 --versionIn a workflow file, image takes registry.caas.sh/NAME:TAG or registry.caas.sh/<tenant-id>/NAME:TAG, or either pinned with @sha256:…. Without a tag, the CLI uses latest.
CLI:
caas workflows run smoke-test.yamlpins each tag to its digest before it starts the run.Console: New run only accepts images pinned to a digest,
registry.caas.sh/<tenant-id>/NAME@sha256:….API: the API doesn't look tags up. Map each image the file names in any other form than
registry.caas.sh/<tenant-id>/NAME@sha256:…to that digest reference inimages(see The API).
A resumed run uses the same pinned images. See Workflows.
Private pulls
Your images never need to be public, and your sandboxes never need your API key to pull them:
When a sandbox, an MCP server or a workflow step starts on your image, caas.sh pulls it from
registry.caas.shfor you, with a short-lived credential that can only pull that image. You don't log in from inside the sandbox.A sandbox can always reach
registry.caas.sh. You don't add it to the sandbox's allowed outbound hosts.Traffic between your sandboxes and
registry.caas.shdoesn't bill as egress. Pulls count only as registry pulls (see Pricing).Nobody outside your workspace can pull your images: the registry serves a key only its own workspace's namespace, and refuses requests without a key.
List tags
The registry answers the standard tags list for one image name:
curl -s -u "$CAAS_TENANT:$CAAS_API_KEY" \
https://registry.caas.sh/v2/$CAAS_TENANT/agent/tags/list{ "name": "<tenant-id>/agent", "tags": ["1.1.0", "1.2.0", "latest"] }It returns up to 50 tags at a time; add ?n=100 for more. When there are more, the response's Link header holds the URL of the next page. Each request bills as one list.
The registry doesn't list your workspace's image names: it refuses /v2/_catalog. Keep track of the names you push.
Pricing
| Meter | Price | What counts |
|---|---|---|
| Registry storage | $0.025 / GB-month | Everything stored in your namespace: manifests, configs and layers, measured every hour |
| Registry pulls | $0.01 / 1,000 pulls | Each image manifest fetched, by Docker or by your sandboxes. A multi-platform image counts two: its index and the platform's manifest. HEAD requests and layer downloads are free. |
| Registry lists | $0.005 / 1,000 lists | Each tags listing |
A GB is 1,000,000,000 bytes, and a month is 30 days. A 2 GB image kept for a month costs $0.05. A sandbox on your own image pulls it each time it starts, and each of those pulls bills to your workspace. Registry usage draws from the workspace's balance like everything else; see Billing and usage.
Delete images
Docker can't delete images from a registry, so send the registry's DELETE requests with curl and a key that has registry:write. Each answers 202.
A sandbox pulls its image every time it starts, and a resumed workflow run pulls the images it started with. Don't delete an image that one of them still uses.
Delete a tag
curl -X DELETE -u "$CAAS_TENANT:$CAAS_API_KEY" \
https://registry.caas.sh/v2/$CAAS_TENANT/agent/manifests/1.1.0This removes only the tag. The image stays, under its digest and any other tags.
Delete an image
curl -X DELETE -u "$CAAS_TENANT:$CAAS_API_KEY" \
https://registry.caas.sh/v2/$CAAS_TENANT/agent/manifests/sha256:<digest>This removes the image's manifest and every tag that points to it.
Free the storage it used
Deleting a manifest doesn't delete its layers, which keep counting toward registry storage. To free them:
Before you delete the image, read its manifest and note the
config.digestand eachlayers[].digest. If it's an index (a multi-platform image), it lists other manifests undermanifests: read each of those too, and delete them along with the index.shellcurl -s -u "$CAAS_TENANT:$CAAS_API_KEY" \ https://registry.caas.sh/v2/$CAAS_TENANT/agent/manifests/sha256:<digest>Delete the image, as above.
Delete each layer and config that no other image in your namespace uses:
shellcurl -X DELETE -u "$CAAS_TENANT:$CAAS_API_KEY" \ https://registry.caas.sh/v2/$CAAS_TENANT/agent/blobs/sha256:<layer-digest>
Docker can share a layer between image names when it pushes, so an image under another name may use it too. Deleting a layer that another image still uses breaks that image.
Troubleshooting
| What you see | What to do |
|---|---|
docker login fails with unauthorized | Use your Tenant ID as the username and an API key from the same workspace as the password. Check that the key isn't revoked. |
docker push is refused | Push to registry.caas.sh/<tenant-id>/… with your own Tenant ID, with a key whose creator is still an owner or operator. |
| A large layer fails to upload | Keep each layer under about 100 MB. |
429 with TOOMANYREQUESTS | One address or one key sent more than about 1,200 requests in a minute. Wait a minute, then retry. |
image tag not found from the CLI | Push the tag first, or check its spelling. |
image digest not found in registry.caas.sh | The digest is wrong, or the image was deleted. Copy it again from docker push. |
image must belong to this tenant | The reference names another workspace's namespace. Push the image to yours. |
sandboxes on your own image need size M or larger | Pick M or larger, or a custom size with at least 4 GiB and 8 GB. |