Code Execution Sandbox
The Artifact and Code Executor tools run model-written Python in a sandbox, never in the DocsGPT API or worker. Neither tool works until a sandbox is running, which is why neither is enabled by default. Read Document doesn’t need one: it parses files in the Celery worker.
DocsGPT supports two sandbox backends, chosen with SANDBOX_BACKEND:
| Backend | What runs the code | Isolation between sessions |
|---|---|---|
jupyter (default) | docsgpt-sandbox, a Jupyter Kernel Gateway container you run next to DocsGPT. Each session is a kernel process inside it. | Working directory only. |
daytona | Daytona Cloud , one sandbox VM per session. | A separate VM per session. |
One Jupyter runner is one trust domain. Every session runs as the same user in the same container and is separated only by its working directory, so code in one session can read another session’s files. The runner removes secrets from the kernel environment and requires a token on its control API, but it is not a boundary between users who don’t trust each other. For untrusted or multi-tenant use, use Daytona.
Docker Compose
The runner comes as an overlay for the Compose files in a DocsGPT checkout (deployment/docker-compose.yaml or deployment/docker-compose-hub.yaml). Compose builds the runner image from deployment/sandbox the first time. The standalone install (docsgpt up and the one-line installer) has no sandbox option. Use a checkout, Kubernetes, or Daytona instead.
-
Add a gateway token to the
.envfile in the repository root. The runner refuses to start without one, and Compose stops with an error when it is unset:echo "SANDBOX_GATEWAY_AUTH_TOKEN=$(openssl rand -hex 32)" >> .env -
Start the stack with the sandbox overlay, from the repository root:
docker compose --env-file .env \ -f deployment/docker-compose.yaml \ -f deployment/optional/docker-compose.optional.sandbox.yaml \ up -d--env-file .envmatters: without it Compose looks for.envindeployment/, the directory of the first file, and doesn’t find the token. Pass the same--env-fileand-flist to every later command (ps,logs,down). Compose needs the token to read the overlay, and a command without the overlay doesn’t see the runner. -
Enable the tools on an agent, or for every chat through
DEFAULT_CHAT_TOOLS(see Enable the tools).
The overlay sets these on backend and worker, so you don’t add them yourself:
SANDBOX_GATEWAY_URL=http://docsgpt-sandbox:8888SANDBOX_KERNEL_NAME=docsgpt-python, the kernel that strips secrets from the environmentSANDBOX_GATEWAY_AUTH_TOKEN, from your.env
The runner has no published port. It reaches the API and worker over an internal sandbox-net network and reaches the internet over its own sandbox-egress network. Its container is read-only, limited to 1024 processes, has a 256 MB /dev/shm for Chromium, and is capped by SANDBOX_MEMORY (default 4g) and SANDBOX_CPUS (default 1.0). The memory covers the warm session kernels (about 50-60 MB each when idle) and the LibreOffice and Chromium processes they start; if you set SANDBOX_MEMORY yourself from an older 1g default, raise it.
The runner has two in-memory mounts. /tmp holds the session workspaces and is mounted noexec, so nothing written there can be run. /sandbox-home holds the kernels’ home directory, where pip install puts packages at runtime, plus pip’s cache and the LibreOffice and Chromium profile directories. It allows exec, so a package with compiled code installed at runtime can load, and it keeps nosuid and nodev. It is owned by the sandbox user with mode 0700 and is capped at SANDBOX_HOME_SIZE (default 1g). Both mounts use RAM, so what they hold counts against SANDBOX_MEMORY.
If you run the image without /sandbox-home, for example from an older copy of the overlay or a plain docker run, the kernels fall back to a home under /tmp and the runner logs one line saying so. Everything works except packages with compiled code installed at runtime, which fail to load with failed to map segment from shared object. Add the mount from the current overlay to fix that.
Block private networks (egress overlay)
By default the runner can reach the internet, which lets code pip install packages or call public APIs. It can also reach your LAN, the Docker host and cloud metadata addresses such as 169.254.169.254. To cut that off, add the egress overlay. It removes the runner’s own route out and sends its traffic through a forward proxy that refuses private destinations:
docker compose --env-file .env \
-f deployment/docker-compose.yaml \
-f deployment/optional/docker-compose.optional.sandbox.yaml \
-f deployment/optional/docker-compose.optional.sandbox-egress.yaml \
up -dThe proxy image in docker-compose.optional.sandbox-egress.yaml (ghcr.io/example/egress-deny-private) is a placeholder. Replace it with a forward proxy of your own, such as Squid or tinyproxy, listening on port 8080 and denying 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16 and 127.0.0.0/8. Only code that honors HTTP_PROXY and HTTPS_PROXY gets out through it; other raw connections have no route at all.
Neither overlay stops the runner from reaching the API (backend:7091) and the worker, because they share sandbox-net and Compose can’t make that one-way. When you enable the sandbox:
- run DocsGPT with authentication (
AUTH_TYPEset; see Choose anAUTH_TYPE) so the API refuses unauthenticated requests from sandbox code, and - if you can, add host firewall rules that drop traffic from the runner to the API and worker. The header of
docker-compose.optional.sandbox-egress.yamlhas theiptablescommands.
Kubernetes
The runner is deployment/k8s/deployments/sandbox-deploy.yaml, with its NetworkPolicy in deployment/k8s/network-policies/sandbox-egress-policy.yaml. Neither is in kustomization.yaml, so kubectl apply -k doesn’t start them. To enable the sandbox, create the docsgpt-sandbox-gateway Secret, add the three SANDBOX_* variables to the docsgpt-api and docsgpt-worker containers, and apply both files. Kubernetes: Optional code-execution sandbox has the exact steps.
- The manifest runs the published
arc53/docsgpt-sandboximage, pinned to the same release asarc53/docsgpt. Change both tags together when you upgrade. - The NetworkPolicy allows public internet egress and DNS, and blocks private, link-local, carrier-grade NAT, loopback and cloud metadata ranges. On a cluster whose pod network uses those ranges, as most do, that also keeps sandbox code away from the API, worker, Postgres and Redis pods. Only the API and worker pods may connect to the runner, on port 8888. The policy takes effect only with a network plugin that enforces NetworkPolicy, such as Calico or Cilium.
- If your cluster runs NodeLocal DNSCache on an address other than
169.254.20.10, change that address in the policy, or DNS lookups from the runner fail. - The runner Service is
ClusterIP. Don’t expose it outside the cluster. - On nodes with the gVisor
runscRuntimeClass installed, uncommentruntimeClassName: gvisorin the manifest for kernel-level isolation.
pip install or a development machine
Run the published image on its own and point DocsGPT at it:
docker run -d --name docsgpt-sandbox -p 127.0.0.1:8888:8888 \
-e SANDBOX_GATEWAY_AUTH_TOKEN=<same-token-as-in-.env> \
arc53/docsgpt-sandbox:<version>Then set the token in DocsGPT’s .env. SANDBOX_GATEWAY_URL defaults to http://localhost:8888, and SANDBOX_KERNEL_NAME defaults to docsgpt-python, the kernel the image ships:
SANDBOX_GATEWAY_AUTH_TOKEN=<same-token>Use the same <version> as your DocsGPT release. develop tracks the main branch. A container started this way has no egress filtering and can reach the host and your LAN, so keep it to single-user and development setups. To run the gateway from a Python environment without Docker, see the runner notes .
Daytona Cloud
Set SANDBOX_BACKEND=daytona, DAYTONA_API_KEY, and optionally DAYTONA_TARGET for the region. Nothing runs next to DocsGPT. Daytona’s default image lacks the libraries that render presentations, documents, spreadsheets and PDFs, and the sandbox’s command-line tools, so build a snapshot once with scripts/build_daytona_snapshot.py, then set DAYTONA_SNAPSHOT to its name yourself: DAYTONA_SNAPSHOT=docsgpt-sandbox-py312-v3 for the script’s default. DocsGPT uses Daytona’s plain image until you do. See Daytona snapshot.
What the sandbox image contains
The runner image and the Daytona snapshot hold the same Python libraries, command-line tools (headless LibreOffice and Chromium, tesseract, poppler, ffmpeg, Node.js 24) and fonts. What the sandbox has installed lists them. To check an image, run its smoke test, which converts real files with each tool:
docker run --rm --read-only --tmpfs /tmp \
--tmpfs /sandbox-home:rw,exec,nosuid,nodev,size=1g,uid=10001,gid=10001,mode=0700 \
arc53/docsgpt-sandbox:<version> \
/opt/docsgpt/kernel-env.sh python /opt/docsgpt/smoke_test.py --pip--pip also installs two small packages at runtime, one with compiled code, and imports them. It needs internet access.
Licenses: the Python libraries, Node.js, Chromium and tesseract are permissive (MIT, BSD or Apache-2.0), LibreOffice is MPL-2.0, and the fonts are under the SIL Open Font License or the Bitstream Vera license. poppler-utils and ffmpeg are Debian’s GPL builds, run only as separate programs. Debian publishes their source, so if you redistribute the image, pass that offer on. PyMuPDF (AGPL) is not included.
Enable the tools
Turn on Code Executor and Artifact per agent in the agent’s tool picker. To offer them in every chat without an agent, add them to DEFAULT_CHAT_TOOLS in .env:
DEFAULT_CHAT_TOOLS=["memory","read_webpage","scheduler","code_executor","artifact_generator"]To check the setup, ask an agent with Code Executor to run print(1 + 1). An error beginning sandbox unavailable: means the API or worker couldn’t open a session on the runner. Check SANDBOX_GATEWAY_URL, that the tokens match, and the runner’s logs (docker compose ... logs docsgpt-sandbox, or kubectl logs deploy/docsgpt-sandbox).
Settings
| Setting | Default | Purpose |
|---|---|---|
SANDBOX_BACKEND | jupyter | jupyter for the self-hosted runner, daytona for Daytona Cloud. |
SANDBOX_GATEWAY_URL | http://localhost:8888 | Where the API and worker reach the runner. |
SANDBOX_GATEWAY_AUTH_TOKEN | unset | Shared token. Set the same value on the runner and on DocsGPT; the runner won’t start without it. |
SANDBOX_KERNEL_NAME | docsgpt-python | The kernel each session uses. docsgpt-python strips secrets from the kernel environment. The stock python3 kernel passes the gateway’s whole environment to code, so use it only with a bare development gateway that has no other kernel. |
SANDBOX_EXEC_TIMEOUT | 60 | Wall-clock limit in seconds for one run, unless the model asks for more. |
SANDBOX_EXEC_MAX_TIMEOUT | 1000 | The most the model may ask for on one run, for long jobs such as a video render or OCR of many pages. A larger request is cut to this. |
SANDBOX_MAX_TTL | 1200 | Seconds an idle session is kept before it is closed. Sessions stay open between runs until then. |
SANDBOX_MAX_SESSIONS | 32 | Live sessions per API or worker process. At the limit, the least recently used idle session is closed. |
SANDBOX_MEMORY, SANDBOX_CPUS | 4g, 1.0 | Resource caps for the Compose runner container. |
SANDBOX_HOME_SIZE | 1g | Size of the Compose runner’s /sandbox-home mount, the kernels’ home. It counts against SANDBOX_MEMORY as it fills. On Kubernetes, change the sizeLimit of the sandbox-home volume instead. |
SANDBOX_KERNEL_IDLE_TIMEOUT | 1800 | Set on the runner, not on DocsGPT. Seconds an idle kernel may live before the runner shuts it down, which catches kernels a restarted API or worker process left behind. Keep it above SANDBOX_MAX_TTL. A kernel that is running code is never shut down, so long runs don’t need a higher value. |
On the Jupyter runner every conversation that ran code keeps its kernel until the session has been idle for SANDBOX_MAX_TTL. An idle kernel takes about 50-60 MB, and more once code has loaded data, and its workspace files sit on the runner’s in-memory /tmp. Size SANDBOX_MEMORY for the conversations you expect to run code at the same time, or lower SANDBOX_MAX_TTL or SANDBOX_MAX_SESSIONS. On Daytona each session is its own sandbox, which Daytona also stops after DAYTONA_AUTO_STOP_INTERVAL idle minutes.
On Daytona, an API process and a worker can both use the same session’s sandbox: each finds it again by its session label. Each process tracks idle time on its own, so when its copy of a session expires it only drops its own handle. Every process records each use of a session in Redis, and the sandbox is deleted only when neither that record nor Daytona’s own activity time shows a use in the last SANDBOX_MAX_TTL seconds. If neither can be read, the sandbox is kept too. Daytona stops a kept sandbox after DAYTONA_AUTO_STOP_INTERVAL idle minutes and deletes it DAYTONA_AUTO_DELETE_INTERVAL minutes after that. When the model passes persist=false, the sandbox is still deleted right after the run.
Long runs
A run can take up to SANDBOX_EXEC_MAX_TIMEOUT seconds when the model asks for it. While it runs:
- the session counts as in use, so it is not closed for being idle, closed to make room for another session, or culled by the runner;
- a Daytona sandbox gets its activity refreshed when the run could outlast half of
DAYTONA_AUTO_STOP_INTERVAL, so Daytona doesn’t stop it mid-run; - the chat stream keeps sending keepalive comments every
SSE_KEEPALIVE_SECONDS, and the answer keeps its liveness heartbeat, so proxies don’t drop the connection and the cleanup task doesn’t mark the turn failed.
Each long run holds one of the API worker’s request threads and one kernel until it ends. If many users start long runs at the same time, raise SANDBOX_MEMORY and the number of API workers, or lower SANDBOX_EXEC_MAX_TIMEOUT.
When a run uses more memory than the runner allows, the kernel is killed. The model is told the run ran out of memory, with a hint to process the data in smaller pieces, and its next run starts a new session.
The rest, including the output and file size caps and the Daytona settings, are in the Settings Reference. The runner README covers the isolation model in more depth.