# Running AI agents with customized templates using docker sandbox

> Source: <https://andrewlock.net/running-ai-agents-with-customized-templates-in-docker-sandbox/>
> Published: 2026-04-14 10:00:00+00:00

This post follows on directly from [my previous post](/running-ai-agents-safely-in-a-microvm-using-docker-sandbox/), in which I describe how to run AI agents safely using the docker sandbox tool, `sbx`

. In this post I describe how to create custom templates, so that your sandboxes start with additional tools. I show both how to add tools to the default template, and how to start with a different docker image and layer-on the docker sandbox tooling later.

An initial caveat to this post: I've been somewhat struggling to get my personal projects working in docker sandboxes, with .NET just hanging indefinitely during builds. This seems to be specific to my projects, as a hello world build is fine, but a word of caution: your mileage may vary.

[Running agents safely in a docker sandbox](#running-agents-safely-in-a-docker-sandbox)

As I described in [my previous post](/running-ai-agents-safely-in-a-microvm-using-docker-sandbox/), working with AI agents in their default mode can mean an infuriating number of tool calls, that interrupt your flow, and generally slow you down:

However, ignoring these tool calls using the ["bypass permissions" mode](https://code.claude.com/docs/en/permission-modes#switch-permission-modes) (AKA YOLO/dangerous mode) can be, well, *dangerous*. There's plenty of examples of AI agents going rogue; do you want to risk it? [Docker Sandboxes](https://docs.docker.com/ai/sandboxes/) provide one solution.

Docker sandboxes run in microVMs, which are isolated from the host machine. The only folder the sandbox can access is the working directory you give access to, and all network traffic goes through a network proxy, which can either block traffic, or it can inject credentials such that the coding agent never sees them directly.

I've only used docker sandboxes for a short while, but I've found they work relatively well for my purposes. However, one limitation is that [some of the projects](https://github.com/datadog/dd-trace-dotnet) I'm working on have a bunch of requirements for tooling, which always needs to be installed in the sandbox. Doing that every time is a bit of a pain. Luckily, there's a solution: custom templates.

[Creating a custom Claude Code template](#creating-a-custom-claude-code-template)

[The Docker sandbox documentation](https://docs.docker.com/ai/sandboxes/agents/custom-environments/) describes how to create a custom template, based on one of the default templates. I'm going to use the Claud Code examples in this post, but there's different templates for each of [the supported agents](https://docs.docker.com/ai/sandboxes/agents/custom-environments/). For each supported image there are also 2 variants: one that includes a Docker Engine, and one that doesn't. e.g.

`claude-code`

—includes a variety of dev tools.`claude-code-docker`

—includes the same as above, but also has Docker Engine.

There's also a

`claude-code-minimal`

template which is similar to`claude-code`

, but includes fewer tools, so you don't have npm, python, or golang, for example.

To create a custom template, you need to have Docker Desktop installed as you're basically building an OCI image ([ effectively a docker image, kinda, sorta](https://github.com/opencontainers/image-spec)). That's despite the fact that docker sandboxes

*don't*run as docker containers, but rather as microVMs.

The following example, [based on the documentation](https://docs.docker.com/ai/sandboxes/agents/custom-environments/#build-a-custom-template) shows how to start from the default template, how to install package manager dependencies, and how to install other tools, using `dotnet`

as an example:

```
FROM docker/sandbox-templates:claude-code-docker

# Switch to root to run package manager installs (.NET dependencies)
USER root
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
    ca-certificates \
    libc6 \
    libgcc-s1 \
    libgssapi-krb5-2 \
    libicu76 \
    libssl3t64 \
    libstdc++6 \
    tzdata \
    zlib1g

# Most tools should be installed at user-level, using the agent user
USER agent
RUN curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --channel 10.0 --no-path
ENV DOTNET_ROOT=/home/agent/.dotnet \
    PATH=$PATH:/home/agent/.dotnet:/home/agent/.dotnet/tools
```

This shows several important things:

- The base
`docker/sandbox-templates`

images are based on Ubuntu, so use`apt-get`

for managing packages. - The base images include two users,
`root`

and`agent`

.- System-level package installations must be made using the
`root`

user. - Tools that install into the home directory must be installed using the
`agent`

user.

- System-level package installations must be made using the

You can build the package using familiar `docker build`

commands, but you *must* push it straight to an OCI registry ([Docker Hub](https://hub.docker.com/) works!). You can't just build it locally as the docker sandbox doesn't share the image store with your local Docker host.

```
docker build -t my-org/my-template:v1 --push .
```

Once you've pushed the image to an OCI registry you can use it locally in a sandbox by using the `--template`

or `-t`

argument when calling `sbx run`

:

```
sbx run -t docker.io/my-org/my-template:v1 claude
```

This will pull (and cache) the template you specify, and you'll have the extra tools immediately available in your sandbox. Note that you must include the `docker.io`

(Docker Hub) or other prefix when specifying the template (which differs from when you're running "normal" docker commands).

I've created

[some sandboxes for .NET], similar to the above, and pushed them to dockerhub. You can see[the definition of the images]here. Feel free to use them if you wish!

Basing your custom templates on the standard default templates works well when you just want to make some extra tools available to your sandbox, but what if you fundamentally want to use a different base image? That's a bit trickier…

[What if you need to change the base image?](#what-if-you-need-to-change-the-base-image-)

The "supported" approach to these custom templates is shown in the previous section: you start with the `docker/sandbox-templates`

and then install the extra tools on that base image. Currently, those images are based on ubuntu 25.10, which is a nice current base image. But what if you *need* to use an older image for running tests. This is the case for the [Datadog .NET SDK](https://github.com/DataDog/dd-trace-dotnet) where we build using old distro versions to ensure we can support customers running with early glibc versions.

This proves a little tricky, as it's not officially supported. On the one hand, to emulate the work the base images do, *mostly* there's just a few crucial configurations you need to add, such as setting `NO_PROXY`

, creating an `agent`

user, and installing the `claude`

CLI. However, the `docker/sandbox-templates`

images contain a lot more than that. Unfortunately, the contents of these images aren't readily available on GitHub, for example.

Luckily, you *can* see the contents of each layer [on Docker Hub](https://hub.docker.com/layers/docker/sandbox-templates/claude-code-minimal/images/sha256-39c0713656fb1f8531df04fbd6cf8d5a64e6002c5331e47ded1d7d3250ff2230). It's a little bit messed up due to how buildkit renders it, but it *is* understandable. Based on each of those layers, I was able to effectively reverse-engineer the layering the `docker/sandbox-templates:claude-code-docker`

image *on top* of a different base image.

This is all very hacky, could change at any time, and comes with no guarantees that it works for you 😅

The following shows a dockerfile that aims to perform all the steps the default `docker/sandbox-templates`

to, but based on an arbitrary base image. There's quite a lot in here, but in summary:

- It configures various environment variables.
- Installs various basic tools (curl, certificates) and sets up various keyrings.
- Configures the
`agent`

user. - Sets up a
`CLAUDE_ENV_FILE`

temporary session file. - Installs a variety of tools (npm, golang, python, make etc).
- Installs Claude Code.

All in all, it looks a bit like this:

```
FROM dd-trace-dotnet/debian-tester AS base

# Grab stuff from the original sandbox
ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global
ENV PATH=/home/agent/.local/bin:/usr/local/share/npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ENV NO_PROXY=localhost,127.0.0.1,::1,172.17.0.0/16
ENV no_proxy=localhost,127.0.0.1,::1,172.17.0.0/16

WORKDIR /home/agent/workspace
RUN apt-get update \
    && apt-get install -yy --no-install-recommends \
    ca-certificates \
    curl \
    gnupg \
    && install -m 0755 -d /etc/apt/keyrings \
    && curl -fsSL https://download.docker.com/linux/debian/gpg | \
    gpg --dearmor -o /etc/apt/keyrings/docker.gpg \
    && chmod a+r /etc/apt/keyrings/docker.gpg \
    && echo \
    "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \
    $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
    tee /etc/apt/sources.list.d/docker.list > /dev/null

# Remove base image user
# Create non-root user
# Configure sudoers
# Create sandbox config
# Set up npm global package folder under /usr/local/share
RUN userdel ubuntu || true \
    && useradd --create-home --uid 1000 --shell /bin/bash agent \
    && groupadd -f docker \
    && usermod -aG sudo agent \
    && usermod -aG docker agent \
    && mkdir /etc/sudoers.d \
    && chmod 0755 /etc/sudoers.d \
    && echo "agent ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/agent \
    && echo "Defaults:%sudo env_keep += \"http_proxy https_proxy no_proxy HTTP_PROXY HTTPS_PROXY NO_PROXY SSL_CERT_FILE NODE_EXTRA_CA_CERTS REQUESTS_CA_BUNDLE JAVA_TOOL_OPTIONS\"" > /etc/sudoers.d/proxyconfig \
    && mkdir -p /home/agent/.docker/sandbox/locks \
    && chown -R agent:agent /home/agent \
    && mkdir -p /usr/local/share/npm-global \
    && chown -R agent:agent /usr/local/share/npm-global

RUN touch /etc/sandbox-persistent.sh && chmod 644 /etc/sandbox-persistent.sh && chown agent:agent /etc/sandbox-persistent.sh
ENV BASH_ENV=/etc/sandbox-persistent.sh

# Source the sandbox persistent environment file
# Export BASH_ENV so non-interactive child shells also source the persistent env
RUN echo 'if [ -f /etc/sandbox-persistent.sh ]; then . /etc/sandbox-persistent.sh; fi; export BASH_ENV=/etc/sandbox-persistent.sh' \
    | tee /etc/profile.d/sandbox-persistent.sh /tmp/sandbox-bashrc-prepend /home/agent/.bashrc > /dev/null \
    && chmod 644 /etc/profile.d/sandbox-persistent.sh \
    && cat /tmp/sandbox-bashrc-prepend /etc/bash.bashrc > /tmp/new-bashrc \
    && mv /tmp/new-bashrc /etc/bash.bashrc \
    && chmod 644 /etc/bash.bashrc \
    && rm /tmp/sandbox-bashrc-prepend
    && chmod 644 /home/agent/.bashrc \
    && chown agent:agent /home/agent/.bashrc

USER root

# Setup Github keys
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
    | tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
    && chmod a+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
    && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
    | tee /etc/apt/sources.list.d/github-cli.list > /dev/null

# Install all the tools available in the claude-code-docker image
RUN apt-get update \
    && apt-get install -yy --no-install-recommends \
    dnsutils \
    docker-buildx-plugin \
    docker-ce-cli \
    docker-compose-plugin \
    git \
    jq \
    less \
    lsof \
    make \
    procps \
    psmisc \
    ripgrep \
    rsync \
    socat \
    sudo \
    unzip \
    gh \
    bc \
    default-jdk-headless \
    golang \
    man-db \
    nodejs \
    npm \
    python3 \
    python3-pip \
    containerd.io docker-ce \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

LABEL com.docker.sandboxes.start-docker=true

USER agent

FROM base AS claude

# Install Claude Code
RUN curl -fsSL https://claude.ai/install.sh | bash

ENV CLAUDE_ENV_FILE=/etc/sandbox-persistent.sh
CMD ["claude", "--dangerously-skip-permissions"]
```

If you don't want *all* the extra tools like npm, python and golang, you can instead base it on the `claude-code-minimal`

image instead. In that case, the final tool install step looks a bit like this instead:

```
RUN apt-get update \
    && apt-get install -yy --no-install-recommends \
        bubblewrap \
        dnsutils \
        docker-buildx-plugin \
        docker-ce-cli \
        docker-compose-plugin \
        git \
        gh \
        jq \
        less \
        lsof \
        make \
        procps \
        psmisc \
        ripgrep \
        rsync \
        socat \
        sudo \
        unzip \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*
```

Or, you know, install a mix of those tools. That's the advantage of this approach at least, you can install more of fewer tools, whatever you want! Whichever approach you like, you can again build and push the image to an OCI registry:

```
docker build --tag dd-trace-dotnet/sandbox --push .
```

You can then use the image in your `sbx`

sandbox, just as before, but this time you'll be running in a base image that has all of your prerequisites installed.

[Updating the version of Claude Code only](#updating-the-version-of-claude-code-only)

You might notice in the above Dockerfile that I put the Claude Code image in its own section of the multi-stage build:

```
FROM base AS claude

# Install Claude Code
RUN curl -fsSL https://claude.ai/install.sh | bash

ENV CLAUDE_ENV_FILE=/etc/sandbox-persistent.sh
CMD ["claude", "--dangerously-skip-permissions"]
```

That's not necessary, but I did it for a subtle reason. Claude Code updates a *lot*, but I didn't really want to update the *entire* image repeatedly for performance reasons. By moving the Claude Code install to its own final stage, I could rebuild *just* that stage, without having to rebuild the *entire* image, by using `--no-cache-filter`

:

```
docker build --tag dd-trace-dotnet/sandbox --push --no-cache-filter claude .
```

It's just a minor thing, but it means updating to the latest Claude Code version is a much quicker process.

I still need to test this image out properly, but I tried it out with a previous version and it was working pretty well for me. I'd be interested to know if anyone else has tried something similar, or if you have a better solution (short of just yolo/dangerous direct on the host!).

[Summary](#summary)

In this post I described how to create custom templates for Docker Sandboxes. First I showed the official approach, which layers tools on top of yhe default sandbox templates in `docker/sandbox-templates`

. This is the easiest approach, and works well if the specific base image doesn't matter too much to you. Then I showed how I reverse-engineered the sandbox templates to allow completely swapping out the base image. This was necessary for a project I was working on, where I specifically wanted to run agents in the same base image we use to build the project. this approach isn't supported, and I'm not 100% it's quite right, but it seems to do the job well enough!
