Guide

Build your first Docker Sandbox kit

  • Intermediate
  • 20 min
  • Published
  • Verified

What you'll learn

  • Understand the difference between a workload kit and a mixin kit
  • Create the files required for a sandbox kit
  • Validate a kit with docker buildx and read a validation failure
  • Run a kit from a local directory with sbx run --kit
  • Inspect a kit's staged sources inside a running sandbox

The terminals below are simulated. Nothing runs on your machine, and each one starts from the state the guide has reached at that point.

Every time you start a sandbox for a new task, you set it up the same way: copy in the helper script your team uses, allow the one extra host it talks to, and paste a paragraph telling the agent when to use it. The next sandbox starts empty and you do it all again. Your teammates do the same, slightly differently.

A kit packages that setup once. It is an ordinary OCI image whose layers carry the files and whose descriptor declares what the sandbox must grant: network rules, credentials, setup hooks, and instructions for the agent. Docker Sandboxes reads the descriptor and applies it when it creates the sandbox.

In this guide you’ll write a small kit from scratch, check it with the same validator that runs when you publish, and run it in a sandbox straight from a local directory.

What you’ll build

The kit is called req-echo. When an agent debugs an HTTP client, it needs to see what the client actually sent. httpbin.org answers any request to /anything by echoing it back. The kit adds three things to a sandbox:

  • One file: a req-echo script on the PATH that sends a request to httpbin.org and prints what the server received.
  • One network rule: permission to reach httpbin.org, and nothing else.
  • Agent context: a short note telling an agent what req-echo is for and when to use it.

You’ll prove each one exists in a sandbox that has the kit, after first seeing that a sandbox without it has none of them.

Before you begin

You need:

  • sbx v0.45 or later. Kits v3 need at least v0.45. This guide was written against v0.47.0. See Install Docker Sandboxes.
  • Docker with Buildx. sbx builds local kits with your Docker engine, and you’ll use docker buildx build to validate the kit yourself.
  • Network access to Docker Hub, where the kit build frontend and the workload you’ll compose with are published.

If you are new to sandboxes, start with Run your first sandbox.

See what is missing

A kit always runs on top of a workload: the kit that supplies the base environment and the launch command. Every sandbox has exactly one. This guide uses Docker’s published v3 shell workload, docker/sbx-kit-shell:1.0.0, which starts a plain bash shell with git, curl, and the usual command-line tools, and no agent.

Start a sandbox from the shell workload alone:

Host machine

sbx run docker/sbx-kit-shell:1.0.0 --name req-echo-before
SANDBOX
  Name:       req-echo-before
  Agent:      sbx-kit-shell
  Kit:        docker/sbx-kit-shell:1.0.0
  Workspace:  ~/my-app (rw)
  Image:      docker.io/docker/sbx-kit-shell@sha256:367c9a4b3fd550f777e1d57cc53550afb4a1385055d79111acf6f996272ff6de
  CPU:        16
  Memory:     32 GiB

IMAGE
  ✓ image — ready

  ✓ Created sandbox req-echo-before

  ○ sbx-kit-shell — starting in sandbox req-echo-before…
      workspace /home/moby/my-app

You land in a shell inside the sandbox. Check for the script, then try to reach httpbin.org directly:

Inside req-echo-before

req-echo
bash: req-echo: command not found
curl -sS https://httpbin.org/anything
Approval required for httpbin.org:443.

Review and respond with:
  sbx policy approval ls

Neither works. There is no script, and the sandbox’s network proxy refuses a host nothing has allowed. (If you chose the allow-all network policy when you set up sbx, the curl succeeds. The kit’s rule still matters for anyone running it under the deny or balanced policy.)

Type exit to leave the sandbox.

Create the kit

A kit lives in its own directory. The descriptor is named after the directory, and supporting files sit beside it. Create this layout:

req-echo/
├── req-echo.yaml
└── req-echo-context.md

Write the agent context

Agent context is plain Markdown that the agent reads. Create req-echo/req-echo-context.md:

# req-echo

`req-echo <path> [curl options]` sends a request to
`https://httpbin.org/anything/<path>` and prints the request exactly as the
server received it: method, headers, query arguments, and body.

Use it when debugging HTTP client code, to confirm what is actually being
sent before assuming the server is at fault. httpbin.org is a public service:
never send real credentials or customer data to it.

Write the descriptor

Create req-echo/req-echo.yaml:

# syntax=docker/sandbox-kit:3
schemaVersion: "3"
kind: mixin
displayName: req-echo
description: Echo an HTTP request through httpbin.org to see what a client sent

build: |
  # syntax=docker/dockerfile:1
  FROM scratch
  COPY --chmod=0755 <<'EOF' /usr/local/bin/req-echo
  #!/bin/sh
  # Usage: req-echo <path> [curl options...]
  # Sends a request to https://httpbin.org/anything/<path> and prints the
  # request exactly as the server received it.
  path="${1:-}"
  [ "$#" -gt 0 ] && shift
  exec curl -sS "https://httpbin.org/anything/${path}" "$@"
  EOF

capabilities:
  - type: com.docker.sandbox/network-policy@1
    config:
      runtime:
        allow:
          - httpbin.org

  - type: com.docker.sandbox/agent-context@1
    config:
      contentFile: ./req-echo-context.md

That is the whole kit. Here is what each part does.

# syntax=docker/sandbox-kit:3 must be the first line. It’s more than a comment to BuildKit: it selects the build frontend, which reads the descriptor, builds the content, validates both, and publishes them as one image. BuildKit pulls the frontend from Docker Hub on first use, so there is nothing to install.

schemaVersion: "3" pins the descriptor grammar to version 3.

kind: mixin says this kit adds to a workload rather than being one. A mixin’s layers land on top of the workload’s filesystem, and the workload keeps its own launch command, user, and working directory.

displayName and description are labels for people. Notice what is missing: there is no name field, no image reference, no entrypoint. A kit’s identity is the reference you run it by, such as ./req-echo today or moby/req-echo:1.0.0 once published, and runtime settings live in the image config where images already keep them.

build: is the content recipe: ordinary Dockerfile text, inline. It starts FROM scratch and copies in a single file, so the mixin’s layer holds exactly that script and nothing else. The script needs curl, which every Docker Sandboxes workload provides as part of its baseline, along with bash, git, a CA store, and the agent user. For anything bigger than a few lines, the recipe moves to a companion req-echo.dockerfile instead; see Add tools and runtimes to a sandbox.

capabilities is the list of everything the kit needs but can’t put in its own image. Each entry is a typed, versioned request, such as com.docker.sandbox/network-policy@1, with its settings under config. The @1 versions that capability’s contract, independently of the descriptor grammar and of sbx releases.

  • The network policy entry asks for one runtime allow rule: httpbin.org. The proxy is deny-by-default, so this rule is the only reason the script can work. runtime is the agent’s steady state; a separate install phase covers setup hooks, which this kit doesn’t have.
  • The agent context entry points at the Markdown file. At build time the frontend copies that file into the image and rewrites contentFile to point at the copy, so the published kit never depends on your directory.

Validate the kit

sbx builds a local kit on demand, but building it yourself first gives you faster feedback while you edit. From the directory that contains req-echo/, pass the descriptor to -f and the kit directory as the build context:

Host machine

docker buildx build -f req-echo/req-echo.yaml -t req-echo:dev req-echo/
[+] Building 2.1s (16/16) FINISHED                         docker:desktop-linux
 => [internal] load build definition from req-echo.yaml                    0.0s
 => => transferring dockerfile: 825B                                       0.0s
 => resolve image config for docker-image://docker.io/docker/sandbox-kit:  0.8s
 => docker-image://docker.io/docker/sandbox-kit:3@sha256:e6a397771e865625  0.9s
 => [internal] load kit descriptor req-echo.yaml · sandbox-kit 3.0.0-m.8   0.0s
 => => transferring dockerfile: 825B                                       0.0s
 => [internal] load guidance req-echo-context.md                           0.0s
 => => transferring context: 457B                                          0.0s
 => [kit] inline build block                                               0.0s
 => [internal] preparing inline document                                   0.0s
 => [1/1] COPY --chmod=0755 <<EOF /usr/local/bin/req-echo                  0.0s
 => [kit] stage guidance /usr/share/sandbox/kit/req-echo/req-echo-context  0.0s
 => [kit] stage sources /usr/share/sandbox/kit/req-echo                    0.0s
 => exporting to image                                                     0.0s
 => => naming to docker.io/library/req-echo:dev                            0.0s
 => => unpacking to docker.io/library/req-echo:dev                         0.0s

The build succeeded, which means the descriptor passed validation. The frontend checks every field and every well-known capability’s config before it builds anything, and the same checks run when you publish. A kit built this way cannot be published malformed.

Break it on purpose

Validation is strict: an unrecognized field anywhere in the descriptor is an error, not something to skip. To see why that matters, misspell allow as alow in req-echo.yaml:

capabilities:
  - type: com.docker.sandbox/network-policy@1
    config:
      runtime:
        alow:
          - httpbin.org

Then build again:

Host machine

docker buildx build -f req-echo/req-echo.yaml -t req-echo:dev req-echo/
[+] Building 0.2s (4/4) FINISHED                           docker:desktop-linux
 => [internal] load build definition from req-echo.yaml                    0.0s
 => => transferring dockerfile: 824B                                       0.0s
 => resolve image config for docker-image://docker.io/docker/sandbox-kit:  0.1s
 => [internal] load kit descriptor req-echo.yaml · sandbox-kit 3.0.0-m.8   0.0s
 => => transferring dockerfile: 824B                                       0.0s
req-echo.yaml:23
--------------------
  22 |         config:
  23 | >>>       runtime:
  24 | >>>         alow:
  25 | >>>           - httpbin.org
  26 |
--------------------
ERROR: failed to build: failed to solve: capabilities[0].config: capabilities[0]: capability com.docker.sandbox/network-policy@1: json: unknown field "alow"

The build fails, and the error names the capability and the field. A lenient reader would have ignored alow, and you would have shipped a kit whose network rule silently didn’t exist. You’d find out later as a mysterious blocked request instead of now as an error. Kits fail closed: a missing permission must show up as an error, never as behavior.

Change alow back to allow and rebuild until the build succeeds again. Running the kit with sbx uses the same frontend, so a descriptor that fails here also fails in sbx run.

Run the kit in a sandbox

Point --kit at the directory. sbx builds the kit, caches the result by source hash, composes it onto the workload, and creates the sandbox. There is no registry and no push involved:

Host machine

sbx run docker/sbx-kit-shell:1.0.0 --name req-echo-test --kit ./req-echo
KITS
  ○ ./req-echo — building…
  ✓ ./req-echo — built (0s)
      sbx-kit-src:req-echo-04f14458a673

SANDBOX
  Name:       req-echo-test
  Agent:      sbx-kit-shell
  Kits:       docker/sbx-kit-shell:1.0.0
              ./req-echo
  Workspace:  ~/my-app (rw)
  Image:      docker.io/docker/sbx-kit-shell@sha256:367c9a4b3fd550f777e1d57cc53550afb4a1385055d79111acf6f996272ff6de
  CPU:        16
  Memory:     32 GiB

IMAGE
  ✓ image — ready

AGENT
    assembling kit image from 2 kits
    pulling kit image /home/moby/my-app/req-echo
  ✓ assembled kit image (5 layers)

  ✓ Created sandbox req-echo-test

  ○ sbx-kit-shell — starting in sandbox req-echo-test…
      workspace /home/moby/my-app

A sandbox records the kits it was created with. If you edit the kit later, create a sandbox with a new name to try the change; reopening req-echo-test keeps the version it started with.

Verify the result

You’re in a shell inside req-echo-test. Run the script, asking for an orders path with a query string:

Inside req-echo-test

req-echo 'orders?status=open'
{
  "args": {
    "status": "open"
  },
  "data": "",
  "files": {},
  "form": {},
  "headers": {
    "Accept": "*/*",
    "Host": "httpbin.org",
    "User-Agent": "curl/8.14.1"
  },
  "json": null,
  "method": "GET",
  "url": "https://httpbin.org/anything/orders?status=open"
}

Two of the three pieces are proven at once: the script is on the PATH, and the request reached httpbin.org. Run it with extra curl options, such as -X POST -H 'Content-Type: application/json' -d '{"sku":"MOBY-1"}', and the echo shows the method, header, and parsed body exactly as they were sent. That is the job an agent will use it for.

Now check that the rule is as narrow as the descriptor says. The kit allowed httpbin.org, not every echo service:

Inside req-echo-test

curl -sS https://postman-echo.com/get
Approval required for postman-echo.com:443.

Review and respond with:
  sbx policy approval ls

The kit widened the sandbox’s network access by exactly one host. That is the trust property worth noticing: what a kit is allowed to reach is written down in the kit, where you and anyone reviewing it can read it.

Look inside the kit

Every kit is self-describing. The frontend stages a kit’s sources into its own image under /usr/share/sandbox/kit/<stem>/, where <stem> is the descriptor’s filename without .yaml. List what is staged in this sandbox:

Inside req-echo-test

ls /usr/share/sandbox/kit/
req-echo  shell
ls /usr/share/sandbox/kit/req-echo/
kit.dockerfile  kit.yaml  req-echo-context.md

There is one directory per kit in the composition: the workload’s and yours. Yours holds three files:

  • kit.yaml is the published descriptor, the version the frontend validated and attached to the image.
  • kit.dockerfile is the content recipe. You wrote it inline under build:; it’s staged here either way.
  • req-echo-context.md is your agent context, copied in by the build.

Check that the published descriptor now points at the staged copy rather than at a path on your machine, and that the recipe travelled with the kit:

Inside req-echo-test

grep contentFile /usr/share/sandbox/kit/req-echo/kit.yaml
        contentFile: /usr/share/sandbox/kit/req-echo/req-echo-context.md
cat /usr/share/sandbox/kit/req-echo/kit.dockerfile
# syntax=docker/dockerfile:1
FROM scratch
COPY --chmod=0755 <<'EOF' /usr/local/bin/req-echo
#!/bin/sh
# Usage: req-echo <path> [curl options...]
# Sends a request to https://httpbin.org/anything/<path> and prints the
# request exactly as the server received it.
path="${1:-}"
[ "$#" -gt 0 ] && shift
exec curl -sS "https://httpbin.org/anything/${path}" "$@"
EOF

That is the third piece: the agent context is in the sandbox. The runtime also adds a per-kit index that points at req-echo-context.md to the instruction file of the composition. Here that is the shell workload’s AGENTS.md, beside your workspace, though no agent reads it. When an agent kit is part of the composition, the index goes in that agent’s file instead, such as Claude Code’s CLAUDE.md. Stacking more mixins adds index entries, not pages of always-loaded text.

To see an agent pick it up, add Docker’s Claude Code mixin to the same composition. This needs an Anthropic credential stored on your host with sbx secret set anthropic:

$ sbx run docker/sbx-kit-shell:1.0.0 --name req-echo-claude \
    --kit docker/sbx-kit-claude-mixin:2.1.292 \
    --kit ./req-echo

Run claude in that sandbox and ask what tools it has for debugging HTTP requests.

Understand what happened

Workloads and mixins

You composed two kits, and they played different roles:

Kind Its layers are Per sandbox
workload A complete root filesystem, plus the command the sandbox runs Exactly one
mixin An overlay that lands on the workload’s filesystem, or nothing Zero or more

You started with a mixin because it is the smaller job. A workload must carry a whole root filesystem built on a base that provides the sandbox baseline (bash, the agent user, git, a CA store), and it owns the launch command. A mixin carries only what it adds, which is why req-echo could be a single file on top of FROM scratch. To build a workload, see Build an agent workload.

Try composing the kits from this guide. Notice that a single kit reaches more than one part of the sandbox at once:

  • Shell workload puts bash, git, and curl in the microVM, writes the AGENTS.md profile, and allows deb.debian.org and dhi.io on the proxy.
  • req-echo adds the req-echo script and req-echo-context.md to the microVM, and allows httpbin.org on the proxy.
  • Claude Code mixin adds the claude agent and its CLAUDE.md profile, allows its API hosts, and asks for the anthropic credential service, which the proxy injects as an x-api-key header for api.anthropic.com.

Network allow rules from every kit in the composition are combined, so the mixin you wrote didn’t need to know which workload it would land on. That is the reason to keep a tool’s access rules with the tool: they follow it into whatever sandbox uses it.

One artifact, one digest

When you publish this kit, it becomes one OCI image. The layers carry the content, and the descriptor rides in a manifest annotation on the same image. There is no second file to keep in sync: pinning the image digest pins the files, the network rule, and the context together. A registry that knows nothing about kits stores one correctly, and docker pull works on it like any other image.

Because the descriptor is the full list of what the kit asks for, a new version that asks for more, such as another host or a credential, shows up as a change in the descriptor you can review, not as a surprise at run time.

Clean up

Remove the two sandboxes from your host:

Host machine

sbx rm req-echo-before -f
Removed sandbox req-echo-before
sbx rm req-echo-test -f
Removed sandbox req-echo-test

Keep the req-echo/ directory if you want to build on it.

Where to go next

You now have a working kit and the local loop for changing it: edit the descriptor, validate with docker buildx build, and run it with sbx run <workload> --kit ./req-echo under a new sandbox name. From here:

For the full grammar, see the Kit v3 specification and its capability pages. For more complete kits to study, see the examples, starting with motd (a single-file mixin) and hello (the smallest workload).

Learning paths

Part of a learning path

This works on its own and is also included in the paths below. Open a path to see the complete route.