# DockerCliExecBackend - Sandboxed Command Execution
`DockerCliExecBackend` is an `ExecBackend` implementation (module `spring-ai-agent-utils-docker-cli`) that runs model-authored shell commands inside a Docker container instead of on the host JVM. It drives Docker through the `docker` CLI — no Java Docker client dependency — so contexts, credential helpers and non-standard daemon sockets are resolved exactly as in your terminal, and any CLI-compatible runtime works (Docker Desktop, Colima, podman via `podman-docker`).
```xml
org.springaicommunity
spring-ai-agent-utils-docker-cli
```
## The full sandbox recipe
The headline use case: one managed container per agent/session, a bind-mounted host directory as the workspace, and the whole toolset wired from two objects:
```java
Path hostWorkspace = Files.createTempDirectory("agent-session");
try (DockerCliExecBackend backend = DockerCliExecBackend.builder()
.image("eclipse-temurin:17-jdk-alpine") // needs a POSIX shell + sleep
.mount(hostWorkspace, "/workspace") // the agent workspace
.build()) {
Workspace workspace = backend.workspace(); // host root + /workspace display mapping
// Shell commands execute inside the container
ShellTools shell = ShellTools.builder().execBackend(backend).build();
// File/search tools operate host-side through the mount, confined to it
FileSystemTools files = FileSystemTools.builder().workspace(workspace).build();
GrepTool grep = GrepTool.builder().workspace(workspace).build();
GlobTool glob = GlobTool.builder().workspace(workspace).build();
// Skill base directories and environment prompts show container paths
ToolCallback skills = SkillsTool.builder()
.addSkillsDirectory(hostWorkspace.resolve("skills").toString())
.workspace(workspace)
.build();
String envInfo = AgentEnvironment.info(workspace);
String gitStatus = AgentEnvironment.gitStatus(backend); // git runs in the container
}
```
Every path the model sees is a container path; every command the model writes runs in the container; every file the model touches stays inside the mount. No tool code changes.
## Two modes
**Managed container** — `image(...)` creates a long-lived container (kept alive with a `sleep` entrypoint) and owns its lifecycle:
```java
DockerCliExecBackend backend = DockerCliExecBackend.builder()
.image("alpine:3.20")
.mount(hostDir, "/workspace") // optional; enables workspace()
.containerWorkingDirectory("/workspace") // default: the mount target
.environment(Map.of("LANG", "C")) // env vars for every command
.shellCommand("/bin/bash", "-c") // default: /bin/sh -c
.dockerCommand("podman") // default: docker
.build();
...
backend.close(); // docker rm -f
```
`close()` removes the container. A JVM shutdown hook does the same if `close()` never runs, and managed containers carry the label `org.springaicommunity.agent.exec-backend=docker-cli`, so stragglers from crashed JVMs can be swept with:
```bash
docker rm -f $(docker ps -aq --filter label=org.springaicommunity.agent.exec-backend=docker-cli)
```
**Attached container** — `containerId(...)` executes in an existing running container whose lifecycle you own (Kubernetes sidecar, compose service, testcontainer). `close()` is a no-op; `mount(...)` is not available (an existing container's mounts are fixed), so `workspace()` requires managed mode.
## Semantics and caveats
- **Exit codes and streams** — `docker exec` propagates the container command's exit code and separate stdout/stderr, so `ExecResult` looks exactly as with `LocalExecBackend`. Docker-level failures (container not running, daemon down) surface as exit codes 125–127 with the CLI error on stderr.
- **Timeout and kill reach into the container.** Killing the client `docker exec` process does not kill the in-container process (a Docker limitation), so every command is wrapped to record its in-container PID under `/tmp`; timeouts and `KillShell` signal that PID (TERM, then KILL after a grace period) inside the container. Processes the command itself detaches into the background may survive — same caveat as the local backend.
- **Background shells** (`BashOutput`/`KillShell`) get handle ids in the `docker_` namespace and stream incremental output with the same cursor semantics as local shells.
- **Image requirements** — a POSIX shell at the configured `shellCommand` path and a `sleep` binary (managed mode keep-alive); `alpine`, `busybox`, `debian`, `eclipse-temurin` images all qualify, distroless images do not. `build()` fails fast with a clear message when the managed container cannot start or dies immediately. The in-container kill mechanism also needs a writable `/tmp` for its PID files — on a read-only rootfs commands still run, but timeout/kill degrade to destroying only the client `docker exec` process.
- **Runtime requirement** — the `docker` CLI must be on the JVM's PATH. If the JVM itself runs in a container, mount the Docker socket and install the CLI, or attach to a pre-created container instead.
## See also
- [Workspace & Exec SPI](WorkspaceAndExecSPI.md) — overview of the two seams this backend implements
- [ShellTools](ShellTools.md#execution-backend--working-directory) — the `ExecBackend` seam this plugs into
- [AgentEnvironment](AgentEnvironment.md) — workspace/backend-aware environment prompts
- [FileSystemTools](FileSystemTools.md) / [GrepTool](GrepTool.md) / [GlobTool](GlobTool.md) — workspace confinement for the host-side file tools