You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
None of this would exist without danny-avila and the LibreChat community. LibreChat is genuinely remarkable work — a unified, open-source AI interface that respects user privacy and gives you real control over your stack. The code interpreter integration, the agent framework, the JWT auth contract — all of it reflects careful, thoughtful engineering. Every time we looked at the code carefully enough, it was right. The gaps were in documentation, not implementation.
Thank you to Danny and everyone who contributed to LibreChat and to the ClickHouse/code-interpreter project.
Why we're posting this
If you're searching for evidence of a successful self-hosted deployment of ClickHouse/code-interpreter with LibreChat — this is it. No issues. Stateful sessions, multi-file upload, matplotlib charts, and pytest all working.
We couldn't find a single published account of anyone successfully doing this end to end. The repo has no deployment guide beyond docker compose up --build, the CODEAPI_JWT_* variables are undocumented publicly. This guide shares everything we learned.
Our setup: Ubuntu 24.04, no VM, /dev/kvm on the host. One wrinkle: containerd defaulted to /var/lib/containerd on our root filesystem, which filled up mid-build. We moved it to /home/containerd — the guide covers this. After the base deployment was verified, we did a second build to add pytest to the packages available in the sandbox.
Why it's so new: ClickHouse acquired LibreChat in late 2025. The paid LibreChat Code Interpreter API was discontinued and replaced with the open-source ClickHouse/code-interpreter. LibreChat v0.8.7 (June 23, 2026) was the first release to officially ship the self-hosted interpreter. The repo was public for less than two months when this was written. The community is still catching up.
This guide is designed to be pasted into an LLM and followed interactively — every step has an explicit gate so you know whether to proceed or stop.
Background: what you need to know before starting
The auth contract (read this first)
LibreChat v0.8.6+ (PR #13028) replaced x-api-key with minted EdDSA JWTs. The docs were not updated. You will see guides — including the current LibreChat docs page — that tell you to set LIBRECHAT_CODE_API_KEY. That variable is never read. LibreChat mints a short-lived Bearer token and sends it as Authorization: Bearer <token>.
ClickHouse/code-interpreter verifies these JWTs natively. If you set x-api-key anywhere, it will be rejected with "Bearer token is required".
// code-interpreter service/src/middleware/auth.tsif(legacyApiKeyHeader){returnres.status(401).json({error: 'Bearer token is required'});}
Both sides are correct. The documentation is wrong.
How packages work (KVM/baked mode)
In the default KVM compose path, packages are baked into the Docker image at build time — not installed at runtime. The file that controls this is docker/package-init.sh, which runs during docker build inside the package-builder stage.
build-packages.sh (repo root) — Kubernetes PVC mode only. Ignore it for local compose.
docker/package-init.sh — the right file. Edit this to add packages.
After editing, always touch docker/package-init.sh before rebuilding to bust Docker's layer cache — otherwise the build completes in seconds and your changes are silently absent.
Network egress from the sandbox
pip install at session time is not possible, and this is a deliberate architectural decision rather than an oversight.
The sandbox runs in its own network namespace (clone_newnet: true). All outbound traffic is routed through the egress gateway, which is deny-by-default and only allows pre-approved internal destinations — the tool call server and file server. PyPI is not an approved destination and there is no config knob to add it. The design intent is that sandbox code cannot make arbitrary network calls, which is a meaningful part of the security model.
The workaround — downloading wheels on the host and uploading them into the session — is fragile: Python 3.14 is new enough that many packages with C extensions don't have pre-built wheels yet, transitive dependencies must all be uploaded manually, and the install is session-scoped and doesn't survive to the next conversation. For anything you'll use regularly, bake it into the image.
A feature request for general configurable egress is tracked in issue #6, open as of this writing.
The localhost trap
LIBRECHAT_CODE_BASEURL must be http://host.docker.internal:3112/v1, nothttp://localhost:3112/v1. Inside a container, localhost resolves to the container itself. With the wrong value the stack appears healthy but every agent execution silently fails with "Code execution is temporarily unavailable." The setup-local-auth-env.js script writes localhost by default — the runbook corrects this immediately after.
Build times
Scenario
Time
Why
First ever build
~10 min
Python 3.14, nsjail, Rust launcher compile from source
After editing docker/package-init.sh
~9 min
Compilation cached; only uv package install reruns
After editing service code
Seconds
Sandbox layers cached; only changed layer rebuilds
After docker system prune (cache loss)
~10 min
Full rebuild
Build times are faster than often cited (~10 min, not 60–90) because uv handles package installation and each compilation stage caches independently in Docker's layer cache.
Agent persistence across rebuilds
Agents are stored in MongoDB (librechat_librechat-data named volume), which is independent of the code-interpreter stack. Rebuilding or restarting the code-interpreter containers does not affect your agents.
Always verify in the interpreter log, not the chat window
The model fabricates convincing execution output when execution fails. Always verify here:
cd~/code-interpreter && docker compose logs --tail=20 api
A real execution produces "Request received" and "Execution completed" log entries. If those aren't there, it didn't run.
The config version warning
LibreChat will log Outdated Config version: 1.2.8 / Latest version: 1.3.13 on startup. This is cosmetic — the stack works with 1.2.8. Update version: in librechat.yaml when ready; it doesn't affect code interpreter functionality.
⚠️Containerd disk trap: By default containerd writes to /var/lib/containerd on / even if Docker's data root is elsewhere. If / is small (< 20 GB free), the build will fail mid-way with "no space left on device". Check both filesystems:
df -h /
docker info | grep "Docker Root Dir"
df -h $(docker info --format '{{.DockerRootDir}}')
If containerd is filling /, move it before building:
sudo systemctl stop docker docker.socket containerd
sudo mv /var/lib/containerd /home/containerd # adjust path to where you have space
sudo mkdir -p /etc/containerd
sudo tee /etc/containerd/config.toml > /dev/null << 'EOF'version = 2root = "/home/containerd"EOF
sudo systemctl start containerd docker
Runbook
Step 1 — Pre-flight checks
ls -l /dev/kvm
docker info | grep "Docker Root Dir"
df -h $(docker info --format '{{.DockerRootDir}}')
docker ps -a --format '{{.Names}}'| grep -E '^(redis|minio|api|service-worker|sandbox-runner|file_server|egress_gateway|tool_call_server)$'
sudo ss -tlnp | grep -E ':(3112|13000|16379|19000|19001|3190|2000)'
Gates:/dev/kvm exists ✓ | 20+ GB free on Docker root ✓ | no container name collisions ✓ | no port collisions ✓
Step 2 — Clone both repos
cd~
git clone https://github.com/danny-avila/LibreChat.git
git clone https://github.com/ClickHouse/code-interpreter.git
ls ~/LibreChat/docker-compose.yml ~/code-interpreter/docker-compose.yaml
Write them into .env (replace <your-value> with each generated value):
sed -i 's/^CREDS_KEY=.*/CREDS_KEY=<your-value>/' .env
sed -i 's/^CREDS_IV=.*/CREDS_IV=<your-value>/' .env
sed -i 's/^JWT_SECRET=.*/JWT_SECRET=<your-value>/' .env
sed -i 's/^JWT_REFRESH_SECRET=.*/JWT_REFRESH_SECRET=<your-value>/' .env
echo"SESSION_SECRET=<your-value>">> .env
sed -i 's/^OPENAI_API_KEY=.*/OPENAI_API_KEY=<your-openai-key>/' .env
>**Your OpenAI key:** this is the API key foryour model provider (OpenAI or an OpenAI-compatible endpoint). It must bein`.env` — never paste a real key into a guide, chat, or document you intend to share. If you're using a Responses API-compatible model like GPT-5.6 Terra, set `useResponsesApi: true` in your `librechat.yaml` model spec (shown in Step 4).sed -i 's/^SEARCH=.*/SEARCH=false/' .envsed -i 's/^ALLOW_REGISTRATION=.*/ALLOW_REGISTRATION=true/' .envecho "FILE_PREVIEW_MAX_EXTRACT_BYTES=10485760" >> .env
Why all the CODEAPI vars in the override? LibreChat's compose has an environment allow-list and silently drops any var not explicitly declared. Without this override, all the JWT vars are stripped from the container even if they're in .env. OPENID_REUSE_TOKENS is written by setup-local-auth-env.js in Step 6 and must be declared here for the same reason.
Fix directory ownership (UID/GID mismatch causes MongoDB to crash on startup):
Updated LibreChat env: /home/<user>/LibreChat/.env
Updated CodeAPI env: /home/<user>/code-interpreter/.env
Provider: librechat-jwt
kid: lc-codeapi-local-2026-05
Generated a new local Ed25519 signing key for LibreChat.
Generated a new local Ed25519 execution-manifest keypair for CodeAPI.
If you get Bearer token is required — the CODEAPI vars didn't reach the container. Check the override file and re-run --force-recreate.
Step 10 — Agent execution in the UI
Creating the agent:
Go to http://localhost:3080
Click Agent Builder in the left sidebar
Click + Create New Agent
Give it a name (e.g. "toolman")
Set the Model — we used GPT-5.6 Luna (gpt-5.6-luna). The model must appear in your librechat.yaml model list to show up here.
To configure model parameters (including enabling the Responses API): click the model name. A Model Parameters panel opens with sliders and toggles. Enable Use Responses API here if your model requires it — we turned this on for GPT-5.6.
Under Tools, click + Add and select Run Code. This is required — the tool doesn't auto-enable from librechat.yaml capabilities alone.
Click Save
Click the green Select button
Important — the model dropdown disappears once an agent is selected. After clicking Select, the top of the chat window will read "Message <agent name>" instead of showing a model picker. This is expected — the agent has its own model baked in. If you still see "Select a model" in the composer, the agent isn't active.
Test execution:
Send: Execute this Python code and show me the output: print(2+2)
Then verify it actually ran — don't trust the chat window alone:
cd~/code-interpreter && docker compose logs --tail=10 api
Gate: log shows "Request received" and "Execution completed" ✓
If the agent generates a code block but doesn't execute it, click the Run Code button on the code block. Both the agent-auto-execute path and the Run Code button path go through the same interpreter.
In LibreChat: click the paperclip → Upload to Code Environment → select both files → send:
I've uploaded sales data for 2025 and 2026. Plot revenue for both years on the same chart, with months on the x-axis. Save it as chart.png and show it to me.
Verify in the log:
cd~/code-interpreter && docker compose logs --tail=30 api
Look for two upload events and "files":{"count":2} on the execution requests. "language":"bash" is expected — LibreChat routes Python through bash_tool by design.
Gate: chart renders inline; log shows two uploads and "Execution completed" with count:2 ✓
Note: a broken "Revenue chart" text link sometimes appears below the rendered chart. This is a duplicate embed from the model — the actual chart is above it. Verify in the log, not the chat window.
Step 13 — Pytest verification (if baked in)
Send the agent: can you run pytest
Expected: "pytest ran successfully, but no tests were found — Collected: 0 tests." This is correct: pytest ran; there are just no test files in the sandbox.
Gate:"Execution completed" in the log ✓
To run a real test: Write /mnt/data/test_math.py with a pytest test that asserts 2+2==4, then run pytest on it.
Troubleshooting
Symptom
Cause
Fix
Bearer token is required
CODEAPI vars not in container
Verify override file; run --force-recreate
unknown_kid or bad_signature
Key mismatch between sides
Re-run setup-local-auth-env.js
Code execution is temporarily unavailable
LibreChat can't reach the interpreter
Set LIBRECHAT_CODE_BASEURL to host.docker.internal, force-recreate
Build fails: no space left on device
Containerd writing to full /
Move containerd root (see system requirements)
KVM_ENABLED=false in sandbox log
/dev/kvm not accessible
Check device permissions; add user to kvm group
Agent generates code but doesn't run it
Model chose not to invoke the tool
Be explicit: "Execute this Python code and show me the output"
Outdated Config version in logs
librechat.yaml version field
Cosmetic — works fine. Update version: when ready.
Packages missing after rebuild
Docker cache reused old layer
Run touch docker/package-init.sh before rebuilding
Adding Python packages
Edit docker/package-init.sh — find the pip install block and add your package before openpyxl:
sed -i 's/ openpyxl \\/ yourpackage \\\n openpyxl \\/' \
~/code-interpreter/docker/package-init.sh
grep -n "yourpackage\|openpyxl"~/code-interpreter/docker/package-init.sh | head -5
Rebuild — touch is required to bust the Docker cache:
cd~/code-interpreter
docker compose down
touch docker/package-init.sh
docker compose up --build -d
Subsequent rebuilds take ~9 minutes. Python compilation is cached; only the uv package install reruns.
What we did next: adding pytest
After verifying the base deployment (Steps 1–13), we added pytest to docker/package-init.sh and rebuilt in ~9 minutes. With stateful sessions, an agent can write a test file in one message and run pytest against it in the next — which is the actual use case for having it baked in.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Working Self-Hosted Deployment: ClickHouse/code-interpreter + LibreChat v0.8.7
Validated: 2026-08-13
Status: Working end-to-end — stateful sessions, multi-file upload, matplotlib charts, and pytest all confirmed
Key repos:
A note of appreciation
None of this would exist without danny-avila and the LibreChat community. LibreChat is genuinely remarkable work — a unified, open-source AI interface that respects user privacy and gives you real control over your stack. The code interpreter integration, the agent framework, the JWT auth contract — all of it reflects careful, thoughtful engineering. Every time we looked at the code carefully enough, it was right. The gaps were in documentation, not implementation.
Thank you to Danny and everyone who contributed to LibreChat and to the
ClickHouse/code-interpreterproject.Why we're posting this
If you're searching for evidence of a successful self-hosted deployment of
ClickHouse/code-interpreterwith LibreChat — this is it. No issues. Stateful sessions, multi-file upload, matplotlib charts, and pytest all working.We couldn't find a single published account of anyone successfully doing this end to end. The repo has no deployment guide beyond
docker compose up --build, theCODEAPI_JWT_*variables are undocumented publicly. This guide shares everything we learned.Our setup: Ubuntu 24.04, no VM,
/dev/kvmon the host. One wrinkle: containerd defaulted to/var/lib/containerdon our root filesystem, which filled up mid-build. We moved it to/home/containerd— the guide covers this. After the base deployment was verified, we did a second build to addpytestto the packages available in the sandbox.Why it's so new: ClickHouse acquired LibreChat in late 2025. The paid LibreChat Code Interpreter API was discontinued and replaced with the open-source
ClickHouse/code-interpreter. LibreChat v0.8.7 (June 23, 2026) was the first release to officially ship the self-hosted interpreter. The repo was public for less than two months when this was written. The community is still catching up.This guide is designed to be pasted into an LLM and followed interactively — every step has an explicit gate so you know whether to proceed or stop.
Background: what you need to know before starting
The auth contract (read this first)
LibreChat v0.8.6+ (PR #13028) replaced
x-api-keywith minted EdDSA JWTs. The docs were not updated. You will see guides — including the current LibreChat docs page — that tell you to setLIBRECHAT_CODE_API_KEY. That variable is never read. LibreChat mints a short-lived Bearer token and sends it asAuthorization: Bearer <token>.ClickHouse/code-interpreterverifies these JWTs natively. If you setx-api-keyanywhere, it will be rejected with"Bearer token is required".Both sides are correct. The documentation is wrong.
How packages work (KVM/baked mode)
In the default KVM compose path, packages are baked into the Docker image at build time — not installed at runtime. The file that controls this is
docker/package-init.sh, which runs duringdocker buildinside thepackage-builderstage.build-packages.sh(repo root) — Kubernetes PVC mode only. Ignore it for local compose.docker/package-init.sh— the right file. Edit this to add packages.touch docker/package-init.shbefore rebuilding to bust Docker's layer cache — otherwise the build completes in seconds and your changes are silently absent.Network egress from the sandbox
pip installat session time is not possible, and this is a deliberate architectural decision rather than an oversight.The sandbox runs in its own network namespace (
clone_newnet: true). All outbound traffic is routed through the egress gateway, which is deny-by-default and only allows pre-approved internal destinations — the tool call server and file server. PyPI is not an approved destination and there is no config knob to add it. The design intent is that sandbox code cannot make arbitrary network calls, which is a meaningful part of the security model.The workaround — downloading wheels on the host and uploading them into the session — is fragile: Python 3.14 is new enough that many packages with C extensions don't have pre-built wheels yet, transitive dependencies must all be uploaded manually, and the install is session-scoped and doesn't survive to the next conversation. For anything you'll use regularly, bake it into the image.
A feature request for general configurable egress is tracked in issue #6, open as of this writing.
The
localhosttrapLIBRECHAT_CODE_BASEURLmust behttp://host.docker.internal:3112/v1, nothttp://localhost:3112/v1. Inside a container,localhostresolves to the container itself. With the wrong value the stack appears healthy but every agent execution silently fails with "Code execution is temporarily unavailable." Thesetup-local-auth-env.jsscript writeslocalhostby default — the runbook corrects this immediately after.Build times
docker/package-init.shdocker system prune(cache loss)Build times are faster than often cited (~10 min, not 60–90) because
uvhandles package installation and each compilation stage caches independently in Docker's layer cache.Agent persistence across rebuilds
Agents are stored in MongoDB (
librechat_librechat-datanamed volume), which is independent of the code-interpreter stack. Rebuilding or restarting the code-interpreter containers does not affect your agents.Always verify in the interpreter log, not the chat window
The model fabricates convincing execution output when execution fails. Always verify here:
A real execution produces
"Request received"and"Execution completed"log entries. If those aren't there, it didn't run.The config version warning
LibreChat will log
Outdated Config version: 1.2.8 / Latest version: 1.3.13on startup. This is cosmetic — the stack works with 1.2.8. Updateversion:inlibrechat.yamlwhen ready; it doesn't affect code interpreter functionality.System requirements
/dev/kvmmust exist —ls -l /dev/kvm/var/lib/containerdon/even if Docker's data root is elsewhere. If/is small (< 20 GB free), the build will fail mid-way with "no space left on device". Check both filesystems:If containerd is filling
/, move it before building:Runbook
Step 1 — Pre-flight checks
Gates:
/dev/kvmexists ✓ | 20+ GB free on Docker root ✓ | no container name collisions ✓ | no port collisions ✓Step 2 — Clone both repos
Gate: both files exist ✓
Step 3 — LibreChat base config
Generate secrets (run this and save the output):
Write them into
.env(replace<your-value>with each generated value):Verify:
grep -E '^(CREDS_KEY|CREDS_IV|JWT_SECRET|JWT_REFRESH_SECRET|SESSION_SECRET|SEARCH|ALLOW_REGISTRATION|OPENAI_API_KEY|UID|GID)=' .envGate: all 10 vars present and non-empty ✓
Step 4 — LibreChat config files
Write
librechat.yaml(adjust model names to match your API access):Write
docker-compose.override.yml:Fix directory ownership (UID/GID mismatch causes MongoDB to crash on startup):
Gate: both files exist ✓
Step 5 — Start LibreChat
Gate:
Server readiness checks passingin the api log ✓Go to
http://localhost:3080and create your account. Then close registration:sed -i 's/^ALLOW_REGISTRATION=.*/ALLOW_REGISTRATION=false/' .env docker compose up -d apiStep 6 — Generate JWT material
If node is missing:
sudo apt install -y nodejsExpected output:
Verify both sides match:
Gates:
CODEAPI_AUTH_PROVIDER=librechat-jwton both sides ✓ |kidvalue matches ✓ |x(public key) value matches in both JWK blobs ✓localhost, which doesn't work from inside a container:Gate:
LIBRECHAT_CODE_BASEURL=http://host.docker.internal:3112/v1✓Step 7 — Configure stateful sessions and production secrets
Gate: all three vars present and non-empty ✓
Step 8 — Build and start the code interpreter
First build takes ~10 minutes. Watch in a second terminal if you like:
When complete:
Gates: 8 containers up and healthy ✓ |
/v1/healthreturnsOK✓ | sandbox log showsBooting microVMandVMM seccomp filter applied✓KVM_ENABLED=false, nsjail is running against your host kernel with no VM boundary. Fix/dev/kvmaccess before proceeding.Step 9 — Auth handshake
Force-recreate the LibreChat api container to bake in the JWT env vars (plain
up -dis not enough — env is set at container creation):Gate: 9 CODEAPI vars listed ✓
Mint a real JWT and fire it at the interpreter:
Gate:
HTTP/1.1 200with"stdout":"4\n"✓If you get
Bearer token is required— the CODEAPI vars didn't reach the container. Check the override file and re-run--force-recreate.Step 10 — Agent execution in the UI
Creating the agent:
http://localhost:3080librechat.yamlmodel list to show up here.librechat.yamlcapabilities alone.Important — the model dropdown disappears once an agent is selected. After clicking Select, the top of the chat window will read "Message <agent name>" instead of showing a model picker. This is expected — the agent has its own model baked in. If you still see "Select a model" in the composer, the agent isn't active.
Test execution:
Send:
Execute this Python code and show me the output: print(2+2)Then verify it actually ran — don't trust the chat window alone:
Gate: log shows
"Request received"and"Execution completed"✓If the agent generates a code block but doesn't execute it, click the Run Code button on the code block. Both the agent-auto-execute path and the Run Code button path go through the same interpreter.
Step 11 — Stateful sessions
In a new conversation with the agent, send two separate messages:
Message 1:
Run this Python code: open('/mnt/data/probe.txt','w').write('one')Wait for it to complete, then:
Message 2:
Run this Python code: print(open('/mnt/data/probe.txt').read())Gate: Message 2 prints
one✓Step 12 — Multi-file upload and chart verification
Create two test CSVs on the host:
In LibreChat: click the paperclip → Upload to Code Environment → select both files → send:
Verify in the log:
Look for two upload events and
"files":{"count":2}on the execution requests."language":"bash"is expected — LibreChat routes Python throughbash_toolby design.Gate: chart renders inline; log shows two uploads and
"Execution completed"withcount:2✓Note: a broken "Revenue chart" text link sometimes appears below the rendered chart. This is a duplicate embed from the model — the actual chart is above it. Verify in the log, not the chat window.
Step 13 — Pytest verification (if baked in)
Send the agent:
can you run pytestExpected: "pytest ran successfully, but no tests were found — Collected: 0 tests." This is correct: pytest ran; there are just no test files in the sandbox.
Gate:
"Execution completed"in the log ✓To run a real test:
Write /mnt/data/test_math.py with a pytest test that asserts 2+2==4, then run pytest on it.Troubleshooting
Bearer token is required--force-recreateunknown_kidorbad_signaturesetup-local-auth-env.jsCode execution is temporarily unavailableLIBRECHAT_CODE_BASEURLtohost.docker.internal, force-recreateno space left on device/KVM_ENABLED=falsein sandbox log/dev/kvmnot accessiblekvmgroupOutdated Config versionin logslibrechat.yamlversion fieldversion:when ready.touch docker/package-init.shbefore rebuildingAdding Python packages
Edit
docker/package-init.sh— find the pip install block and add your package beforeopenpyxl:Rebuild —
touchis required to bust the Docker cache:Subsequent rebuilds take ~9 minutes. Python compilation is cached; only the uv package install reruns.
What we did next: adding pytest
After verifying the base deployment (Steps 1–13), we added
pytesttodocker/package-init.shand rebuilt in ~9 minutes. With stateful sessions, an agent can write a test file in one message and run pytest against it in the next — which is the actual use case for having it baked in.Key facts
mainbranch, 2026-08-13 — https://github.com/ClickHouse/code-interpreter/dev/kvmpresent, Docker root on/home/docker, containerd moved to/home/containerdCODEAPI_RUNTIME_SESSION_MODE=affinity+stateful_code_sessions: trueon the agentbash_tool)All reactions