Make your Sandbox kits configurable
- Intermediate
- 20 min
- Published
- Verified
What you'll learn
- Declare kit arguments with defaults, required values, and enum or pattern constraints
- Create templated capability config and generated files
- Supply argument values with
--kit-arg, a handle prefix, an args file, or ansbxenvfile - Choose between build-phase and create-phase arguments
- Pin or re-expose a component's arguments in a kit set
Your platform team wrote a kit that connects agents to the company’s internal API. It works well, for the payments team, against staging. Then the search team asks for it, and someone else needs it pointed at production for a release-day investigation. The obvious fix is to copy the kit and edit the copy. Two months later there are five forks, each with a slightly different network rule, and nobody knows which one is current.
Kit arguments solve this. You publish one kit that declares the values that may vary, with defaults and constraints, and each sandbox supplies its own answers when it is created. The published kit never changes. Each sandbox gets its own expanded copy.
This guide starts with the short version: how to declare an argument, use it, and supply a value. The rest is a worked example that turns a kit with hardcoded values into one every team can use, and covers the options in more depth.
Kit arguments in brief
An argument is a named value that a kit declares and each sandbox answers when it’s created.
Declare it in the descriptor’s args block:
args:
environment:
default: staging
enum: [staging, production]
description: Which Acme API environment the agent may callUse it anywhere in the descriptor with ${{ kit.args.<name> }}, including
inside capability config:
capabilities:
- type: com.docker.sandbox/network-policy@1
config:
runtime:
allow:
- api.${{ kit.args.environment }}.acme.example.com:443The ${{ opener isn’t valid shell, so it never collides with $VAR or
${VAR} in a hook command. A reference to an argument the kit doesn’t declare,
such as ${{ kit.args.enviroment }}, is a validation error, not an empty
string.
Supply a value when you create the sandbox:
$ sbx create docker/sbx-kit-shell:1.0.0 --kit ./acme-api --kit-arg environment=productionLeave it out and the default applies. Values can also come from an args file,
an sbxenv file, or a kit set that pins them.
Constrain it with the argument’s other fields:
| Field | What it does |
|---|---|
default |
The value used when none is supplied. Can’t be combined with required. |
required: true |
Refuses to create the sandbox without a value. |
enum |
The exact set of accepted values. Can’t be combined with pattern. |
pattern |
A regular expression the whole value must match. |
env |
Also sets the value as this environment variable. Otherwise an argument stays private. |
buildArg |
Resolves the value when the kit image is built, and passes it to the Dockerfile. |
description |
Tells whoever supplies the value what it’s for. |
The published kit keeps its ${{ kit.args.* }} references. When a sandbox is
created, sbx checks the values against the constraints and expands them into
an effective descriptor for that sandbox alone. That’s how one kit serves many
sandboxes with different values. Argument values are plain text, so never use
one for a token or password. Declare a credential instead, as in
Connect to services without exposing credentials.
Worked example: one kit for every team
The rest of this guide takes a kit with hardcoded values and makes it configurable. You will:
- declare a required argument, a defaulted argument with an
enum, and apatternthat keeps values safe; - template those arguments into a network rule, a generated config file, and an environment variable;
- create sandboxes with different values and prove each one got its own configuration and policy;
- see a missing or invalid value refused before the sandbox exists;
- record the values for a project in an
sbxenvfile, and see how a kit set pins or re-exposes them.
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.- Familiarity with a kit descriptor. If you have never written one, start with Build your first Docker Sandbox kit.
The API in this guide, acme.example.com, is fictional. The kit mechanics
are the same for any real service.
The starting point: a kit with hardcoded values
Create a directory named acme-api and save this descriptor in it as
acme-api/acme-api.yaml:
# syntax=docker/sandbox-kit:3
schemaVersion: "3"
kind: mixin
displayName: Acme API access
description: Lets the agent call the Acme internal API
version: "1.0.0"
capabilities:
- type: com.docker.sandbox/network-policy@1
config:
runtime:
allow:
- api.staging.acme.example.com:443
- type: com.docker.sandbox/lifecycle@1
config:
files:
- path: /home/agent/.config/acme/config.json
mode: "0644"
content: |
{
"team": "payments",
"environment": "staging",
"baseUrl": "https://api.staging.acme.example.com"
}It is a declaration-only mixin: no Dockerfile, just two capability requests.
The network rule lets the agent reach the staging API. The lifecycle files
entry writes a config file the team’s scripts read before the agent starts.
Three values in this file belong to one team in one situation: the team name, the environment, and, because it is derived from the environment, the API host. Those are the values to turn into arguments.
Declare the arguments
Start by declaring the arguments. In acme-api/acme-api.yaml, add this args
block between version and capabilities:
args:
team:
required: true
pattern: "^[a-z][a-z0-9-]*$"
description: Team slug, as used in the Acme API (for example, payments)
environment:
default: staging
enum: [staging, production]
description: Which Acme API environment the agent may call
env: ACME_ENVEach part of the args block is doing a specific job:
teamisrequired: true. There is no sensible default team, so the kit refuses to guess. A sandbox cannot be created without a value.teamhas apattern. The value lands inside a JSON string in the config file. Kit expansion carries a value verbatim, so a value containing a"would produce broken JSON. The pattern, matched against the whole value, rules that out. Treat every constraint as a statement about where the value lands.environmenthas adefaultand anenum. Most people want staging and should not have to say so. Theenumis the exact accepted set, so a typo likeprodfails instead of producing a network rule for a host that does not exist.environmenthasenv: ACME_ENV. Arguments are private by default. A value reaches the running container’s environment only if you opt in withenv:.teamhas noenv:, so it appears in the config file and nowhere else.
Nothing in the kit uses these arguments yet. The capabilities still have the hardcoded values, which the next step replaces.
Use the arguments in the kit
Now replace each hardcoded value with a reference to an argument. In the
capabilities section of acme-api/acme-api.yaml, make these changes:
- In the network policy, replace
api.staging.acme.example.com:443withapi.${{ kit.args.environment }}.acme.example.com:443. - In the lifecycle file’s
content, replacepaymentswith${{ kit.args.team }}, and both occurrences ofstagingwith${{ kit.args.environment }}.
The complete acme-api/acme-api.yaml now looks like this:
# syntax=docker/sandbox-kit:3
schemaVersion: "3"
kind: mixin
displayName: Acme API access
description: Lets the agent call the Acme internal API
version: "1.0.0"
args:
team:
required: true
pattern: "^[a-z][a-z0-9-]*$"
description: Team slug, as used in the Acme API (for example, payments)
environment:
default: staging
enum: [staging, production]
description: Which Acme API environment the agent may call
env: ACME_ENV
capabilities:
- type: com.docker.sandbox/network-policy@1
config:
runtime:
allow:
- api.${{ kit.args.environment }}.acme.example.com:443
- type: com.docker.sandbox/lifecycle@1
config:
files:
- path: /home/agent/.config/acme/config.json
mode: "0644"
content: |
{
"team": "${{ kit.args.team }}",
"environment": "${{ kit.args.environment }}",
"baseUrl": "https://api.${{ kit.args.environment }}.acme.example.com"
}Create a sandbox without a value
Arguments that have no buildArg: are create-phase arguments: sbx
resolves them when it creates the sandbox. Use the local directory as a
mixin on Docker’s published v3 shell workload, and leave out the required
team:
Host machine
sbx create docker/sbx-kit-shell:1.0.0 --name acme-payments --kit ./acme-api
KITS
○ ./acme-api — building…
✓ ./acme-api — built (3s)
sbx-kit-src:acme-api-4b9e38e875ceerror: resolve kits: kit "./acme-api": kit arg "team" is required and no value was supplied
This failure is intended. The kit declared that team has no safe default,
so nothing was created. Supply the value with --kit-arg:
Host machine
sbx create docker/sbx-kit-shell:1.0.0 --name acme-payments --kit ./acme-api --kit-arg team=payments
KITS
○ ./acme-api — building…
✓ ./acme-api — built (1s)
sbx-kit-src:acme-api-6a75564307ed
SANDBOX
Name: acme-payments
Agent: sbx-kit-shell
Kits: docker/sbx-kit-shell:1.0.0
./acme-api
Workspace: none · no workspace bind mount
Image: docker.io/docker/sbx-kit-shell
CPU: 16
Memory: 32 GiB
IMAGE
✓ image — ready
AGENT
assembling kit image from 2 kits
pulling kit image /home/moby/acme-api/acme-api
✓ assembled kit image (3 layers)
setting 1 environment variable
writing 1 init file
✓ Created sandbox acme-payments
try: sbx run --name acme-paymentsYou supplied team but not environment, so environment took its default,
staging.
Verify what the arguments produced
Look at the generated config file:
Host machine · acme-payments
sbx exec acme-payments -- cat /home/agent/.config/acme/config.json
{
"team": "payments",
"environment": "staging",
"baseUrl": "https://api.staging.acme.example.com"
}sbx exec acme-payments -- printenv ACME_ENV
staging
sbx exec acme-payments -- sh -c 'printenv ACME_TEAM || echo "ACME_TEAM is not set"'
ACME_TEAM is not set
ACME_ENV is set because environment declares env: ACME_ENV. There is no
ACME_TEAM, and no other variable holding the team name. An argument without
env: stays out of the environment.
Now check that the argument reached the network policy. The staging API is allowed, and production is not:
Host machine · acme-payments
sbx exec acme-payments -- curl -s https://api.staging.acme.example.com/v1/health
{"status":"ok","environment":"staging"}sbx exec acme-payments -- curl -s https://api.production.acme.example.com/v1/health
Blocked by network policy: domain api.production.acme.example.com
One kit, one value, and the sandbox can reach exactly one environment.
Finally, look at the descriptor the sandbox recorded for the kit:
Host machine · acme-payments
sbx exec acme-payments -- grep acme.example.com /usr/share/sandbox/kit/acme-api/kit.yaml
- api.${{ kit.args.environment }}.acme.example.com:443
"baseUrl": "https://api.${{ kit.args.environment }}.acme.example.com"The references are still there. The kit itself is never rewritten. sbx
derives an effective descriptor for this one sandbox by expanding the
values, and that expanded form is what it enforces.
Create a second sandbox with different values
Now create a sandbox for a production investigation. Arguments are recorded when a sandbox is created, so a different configuration means a new sandbox with a new name.
First, try the common typo:
Host machine
sbx create docker/sbx-kit-shell:1.0.0 --name acme-payments-prod --kit ./acme-api --kit-arg team=payments --kit-arg environment=prod
KITS
○ ./acme-api — building…
✓ ./acme-api — built (1s)
sbx-kit-src:acme-api-eeac2c9a1aaeerror: resolve kits: kit "./acme-api": kit arg "environment": value "prod" is not one of [staging production]
The enum refused it before anything was created. Use the accepted value,
and this time prefix the argument with the kit’s handle, acme-api:
Host machine
sbx create docker/sbx-kit-shell:1.0.0 --name acme-payments-prod --kit ./acme-api --kit-arg team=payments --kit-arg acme-api.environment=production
KITS
○ ./acme-api — building…
✓ ./acme-api — built (1s)
sbx-kit-src:acme-api-3e2310800e3e
SANDBOX
Name: acme-payments-prod
Agent: sbx-kit-shell
Kits: docker/sbx-kit-shell:1.0.0
./acme-api
Workspace: none · no workspace bind mount
Image: docker.io/docker/sbx-kit-shell
CPU: 16
Memory: 32 GiB
IMAGE
✓ image — ready
AGENT
assembling kit image from 2 kits
pulling kit image /home/moby/acme-api/acme-api
✓ assembled kit image (3 layers)
setting 1 environment variable
writing 1 init file
✓ Created sandbox acme-payments-prod
try: sbx run --name acme-payments-prodAn unprefixed --kit-arg applies to every selected kit that declares that
argument. When you combine several kits, two of them may both declare an
argument named environment. The <handle>.<argument>=<value> form targets
one kit, and it overrides an unprefixed value. For a local directory, the
handle is the directory name. For a published image, it is the last part of
the repository name.
Confirm that this sandbox has its own configuration:
Host machine · acme-payments-prod
sbx exec acme-payments-prod -- cat /home/agent/.config/acme/config.json
{
"team": "payments",
"environment": "production",
"baseUrl": "https://api.production.acme.example.com"
}sbx exec acme-payments-prod -- curl -s https://api.production.acme.example.com/v1/health
{"status":"ok","environment":"production"}Both sandboxes came from the same kit directory. Neither knows the other exists.
Record the values for a project
Typing --kit-arg on every create works for one-offs. For values a project
always uses, record them where the project lives.
An args file. Put one name=value per line, with an optional handle
prefix, and pass it with --kit-args-file:
acme-api.team=payments
acme-api.environment=staging$ sbx create docker/sbx-kit-shell:1.0.0 --name acme-payments \
--kit ./acme-api --kit-args-file ./acme.argsA --kit-arg on the same command line overrides the file.
An sbxenv file. A
sandbox environment file
describes the whole environment a project consumes. An object entry under
kits takes the kit’s arguments under args:
schemaVersion: "1"
name: acme-payments
agent: sbx-kit-shell
kits:
- docker/sbx-kit-shell:1.0.0
- source: ./acme-api
args:
team: payments
environment: staging
workspace: .agent is the workload’s handle, and the reference itself goes under kits.
Commit this file next to the project, and sbx env run creates the same
sandbox for everyone on the team. The environment file can also declare its
own inputs and forward them to a kit, as in
environment: ${{ env.args.channel }}. See
Parameterize an environment.
This is the division of labour to aim for: the kit declares what can vary and what values are acceptable, and the environment file answers those questions for one project.
Choose the phase: build or create
Everything so far used create-phase arguments. An argument can instead
resolve when the kit image is built, by declaring buildArg:. Tool
versions are the usual case, because the version decides what is downloaded
into the image. This excerpt is adapted from the
GitHub CLI example in the spec:
args:
version:
default: "2.98.0"
pattern: '^[0-9]+\.[0-9]+\.[0-9]+$'
description: GitHub CLI release to install
buildArg: GH_VERSION
provides: ["gh@${{ kit.args.version }}"]The kit’s Dockerfile reads ARG GH_VERSION to download that release. When
you build, you pass the value by its kit-argument name, and the frontend
validates it against the pattern before the Dockerfile sees it as
GH_VERSION:
$ docker buildx build . -f gh.yaml --build-arg version=2.99.0 -t gh-kit:2.99.0The build expands every reference to a build-phase argument into the
published descriptor, so the published kit says gh@2.99.0. That value is
then fixed: changing it means building and publishing a new image.
Create phase (no buildArg:) |
Build phase (buildArg:) |
|
|---|---|---|
| Who answers | Whoever creates the sandbox | Whoever builds the image |
| Supplied with | --kit-arg, --kit-args-file, sbxenv |
docker buildx build --build-arg |
| Published descriptor | Keeps the ${{ kit.args.* }} reference |
Has the value baked in |
| Typical use | Team, environment, model, feature toggles | Tool versions, anything in a RUN |
| Reaches the container via | env:, if you opt in |
The image contents |
An argument resolves in exactly one phase, so env: and buildArg: are
mutually exclusive. To install a tool at a configurable version, see
Add tools and runtimes to a sandbox.
Pin or re-expose arguments in a kit set
A kit set publishes several kits as one reference. When the payments team
always uses the shell workload with acme-api, a set saves everyone the
command line. The set also decides which of its components’ arguments users
may still change.
A set’s components must be published references. This example assumes you
pushed acme-api as moby/acme-api:1.0.0. See
Publish and share your Docker Sandbox kits.
# syntax=docker/sandbox-kit:3
schemaVersion: "3"
kind: set
displayName: Acme payments environment
version: "1.0.0"
args:
environment:
default: staging
enum: [staging, production]
description: Which Acme API environment the agent may call
kits:
- ref: docker/sbx-kit-shell:1.0.0
- ref: moby/acme-api:1.0.0
args:
team: payments
environment: ${{ kit.args.environment }}The two component arguments get different treatment:
team: paymentsis pinned. A literal value answers the argument when the set is published. Users of the set cannot change it.environmentis re-exported. A value that is exactly one${{ kit.args.* }}reference passes the question through to the set’s ownenvironmentargument. Users answer it with--kit-arg environment=productionwhen they run the set.
A re-exported argument must promise at least what the component promised:
required where it was required, defaulted where it had a default, and its
enum restated or narrowed. That is why the set repeats default: staging
and the full enum. Without them, a user could pass a value that
acme-api would have refused, and nothing would be left to refuse it. A set
could narrow the choice to enum: [staging] to publish a staging-only
environment.
Understand what happened
A configurable kit is a contract with two halves:
- The kit states the questions and the acceptable answers. The argument
names, defaults, and
enumorpatternconstraints are part of the kit, and they travel with it when it is published and signed. Whoever runs the kit can choose only among values you allowed. - Each sandbox supplies its answers once, at creation.
sbxchecks the values, expands them across the whole descriptor, including capability config, and validates the result strictly. The effective descriptor is what it enforces and what it records, so recreating the sandbox reproduces it exactly.
Some details follow from this model:
- Expansion is structural. Values are substituted into the parsed
document, not into raw text. A value cannot escape the YAML string it lands
in and inject new fields. It can still break a format nested inside that
string, such as the JSON in the config file, so constrain values with a
patternorenum. - A value can widen access. A different
environmentvalue opens a different host. The effective descriptor is whatsbxjudges, so it treats access opened by a value like any other access a kit requests. - Not every file is a template.
${{ kit.args.* }}expands in descriptor strings: capability config, hook commands, and lifecyclefilescontent. It does not expand inside files the kit ships, such as anagent-context@1contentFile. Generate per-sandbox text with a lifecycle file instead. - Choosing other values means a new sandbox. A sandbox keeps the arguments it was created with. To use other values, create a sandbox with another name, as you did for production.
Clean up
Remove the two sandboxes:
Host machine
sbx rm acme-payments acme-payments-prod
✓ Removed sandbox acme-payments ✓ Removed sandbox acme-payments-prod
Where to go next
- Publish and share your Docker Sandbox kits
to push
acme-apito Docker Hub so a set or a teammate can reference it. - Add tools and runtimes to a sandbox
to put a build-phase
versionargument to work in a Dockerfile. - Connect to services without exposing credentials to add authentication to the Acme API without passing a token as an argument.
- The
argssection of the v3 specification for typed values, key collisions, and the full validation rules. - Configure component arguments in the Docker Docs for more on sets.
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 5 of 6
Table of contents