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
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-echoscript on thePATHthat 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-echois 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:
sbxv0.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.
sbxbuilds local kits with your Docker engine, and you’ll usedocker buildx buildto 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-appYou 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.mdWrite 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.mdThat 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.runtimeis the agent’s steady state; a separateinstallphase 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
contentFileto 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.orgThen 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-appA 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.yamlis the published descriptor, the version the frontend validated and attached to the image.kit.dockerfileis the content recipe. You wrote it inline underbuild:; it’s staged here either way.req-echo-context.mdis 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}" "$@"
EOFThat 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-echoRun 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, andcurlin the microVM, writes theAGENTS.mdprofile, and allowsdeb.debian.organddhi.ioon the proxy. - req-echo adds the
req-echoscript andreq-echo-context.mdto the microVM, and allowshttpbin.orgon the proxy. - Claude Code mixin adds the
claudeagent and itsCLAUDE.mdprofile, allows its API hosts, and asks for theanthropiccredential service, which the proxy injects as anx-api-keyheader forapi.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:
- Add tools and runtimes to a sandbox — companion Dockerfiles, multi-stage builds, and lifecycle hooks for setup that has to happen inside the sandbox.
- Give your agents reusable skills — package team workflows as agent skills and context.
- Connect to services without exposing credentials — declare a credential the proxy injects for your kit’s service.
- Make your Sandbox kits configurable — expose arguments so one kit fits several projects.
- Publish and share your Docker Sandbox kits — push the kit to Docker Hub, sign it, and build it in CI.
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).
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.
Learning path
Build and customize Docker Sandbox environments
Step 1 of 6
Table of contents