Blender in Docker¶
Two images put the whole thing on a machine with a GPU: Blender without an interface, with the add-on installed, and the MCP server in front of it, speaking HTTP. A client connects to one HTTPS address with one token — nothing to install on the client's side, no SSH key, no tunnel. The projects, renders and snapshots live in a Docker volume beside Blender, and the server has pages to look through them and clean them.
| Image | What it holds |
|---|---|
ghcr.io/orldev/s3l-blender:<blender>-<add-on> |
Ubuntu 24.04, Blender (5.2.3, checked by SHA-256), the Snail Bridge add-on; starts blender -b -c snail_bridge |
ghcr.io/orldev/s3l-blender-mcp:<version> |
the server on ASP.NET, in HTTP mode on port 8080, with its pages |
The Blender tag names both versions because the add-on is inside it: the pair in compose.yml is the pair
blender_diagnose reports as current.
client (Claude Code, any folder)
│ HTTPS + bearer token
▼
reverse proxy (Traefik) ── blender.example.com
│ private address, port 8810
▼
s3l-blender-mcp ── /mcp, /raw/…, the pages, /healthz
│ internal network, no way out, the bridge token
▼
s3l-blender ── Blender 5.2 -b + Snail Bridge on 9876, the GPU
└─ volume data: files/<project>/, jobs/, batches/, snapshots/
What the machine needs¶
- An NVIDIA GPU and its driver.
- Docker with GPU support: on Linux the NVIDIA Container Toolkit, on Windows Docker Desktop with the WSL 2 backend.
- A reverse proxy that ends TLS, and a name that points at the machine.
OptiX and Docker Desktop. Inside a container on Docker Desktop for Windows, OptiX cannot load: WSL hands the
container a stub libnvoptix.so.1 that loads the real library through DXCore and does not export the entry point Cycles
looks for (OptiX initialization failed with error code 7805). CUDA works — set BLENDER_BACKEND=CUDA. On a Linux host
NVIDIA_DRIVER_CAPABILITIES=all brings the OptiX library in and OPTIX works. EEVEE renders in the container too, on
the CPU through Mesa, several times slower than Cycles on the GPU; use Cycles.
1. The folder¶
Everything the machine needs is one folder: compose.yml and .env.example from deploy/ in the repository, and two
secrets beside them.
s3l-blender/
├── compose.yml
├── .env # copied from .env.example
└── secrets/
├── bridge.token # between the server and Blender; stays on this machine
└── client.token # what clients present; a copy goes to each client
Each token is 64 hexadecimal characters:
openssl rand -hex 32 > secrets/bridge.token
openssl rand -hex 32 > secrets/client.token
On Windows without OpenSSL, in PowerShell:
foreach ($name in 'bridge', 'client') {
$bytes = New-Object byte[] 32; [Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes)
($bytes | ForEach-Object { $_.ToString('x2') }) -join '' | Set-Content -NoNewline -Encoding ascii "secrets\$name.token"
}
.env holds what differs between machines:
| Setting | Default | Meaning |
|---|---|---|
STACK_NAME |
s3l-blender |
The compose project, and with it the names of the volumes and the containers (<name> and <name>-mcp); see A second stack |
BLENDER_TAG |
latest |
The Blender image: latest, or a release as 5.2.3-X.Y.Z |
MCP_TAG |
latest |
The server image: latest, or a release as X.Y.Z; pin both to the same release |
BLENDER_BACKEND |
OPTIX |
The Cycles compute backend Blender enables at start: OPTIX, CUDA, NONE for the CPU; CUDA on Docker Desktop. A scene renders on it once its device is GPU |
PUBLIC_URL |
https://blender.example.com |
The address clients reach the server at; links to files are built on it |
PUBLISH_ADDRESS |
192.0.2.10 |
The address the server port is published on — one the proxy reaches, never the public one |
PUBLISH_PORT |
8810 |
That port |
PYTHON_ACCESS |
Fallback |
Allow, Fallback (a script only where no tool covers the job) or Off |
BLENDER_MEMORY |
24g |
What Blender may use before the kernel stops it; a scene that does not fit kills the container instead of swapping the machine to a standstill. blender_render_budget reads this limit from the container's cgroup and judges a scene against it, not against the memory of the host |
MCP_MEMORY |
1g |
The same for the server, which holds a chunk of a transfer at a time and nothing else large |
The stack keeps four volumes: Blender's data directory (projects, render jobs, batches, snapshots), its kernel cache,
the keys that sign the pages' sessions, and the server's own data directory — which is where the keys clients are
admitted by live, so clients add survives an update of the stack.
2. Images¶
A tag vX.Y.Z in the repository publishes both images to GHCR once the whole CI has passed: the server as X.Y.Z and
Blender as 5.2.3-X.Y.Z. The pair is then started and asked whether the server reaches Blender, the package goes to
NuGet, and only after that does latest move to this release, and only for one without a suffix. The images are
private. Log in once with a token that has read:packages:
docker login ghcr.io -u orldev
Or build them on the machine itself, from a checkout:
docker build -f deploy/blender/Dockerfile -t ghcr.io/orldev/s3l-blender:latest .
docker build -f deploy/mcp/Dockerfile --build-arg VERSION=0.1.0 -t ghcr.io/orldev/s3l-blender-mcp:latest .
VERSION is the version the server reports in blender_diagnose and on its status page; without it a build from a
checkout reports 0.0.0-local.
3. Start¶
docker compose up -d
docker compose ps
curl http://192.0.2.10:8810/healthz # {"status":"ok","blender":"up"}
s3l-blender turns healthy once the bridge listens, about half a minute after the start. The server asks Blender
nothing until a client does, so blender: down right after the start only means Blender is still starting.
4. The reverse proxy¶
With Traefik's file provider, a route next to the other ones — the file is deploy/traefik/s3l-blender.yml:
http:
routers:
s3l-blender:
entryPoints:
- https
service: s3l-blender-host
rule: Host(`blender.example.com`)
tls:
certResolver: letsEncrypt
services:
s3l-blender-host:
loadBalancer:
servers:
- url: http://192.0.2.10:8810/
passHostHeader: true
Traefik watches that directory, but not every file system tells it about a new file: on Docker Desktop for Windows a
file added to a bind-mounted Windows folder stays unnoticed until Traefik restarts (docker restart traefik, which
drops its other sites for a few seconds). Traefik v3 also stops reading a request body after 60 seconds by default; the
upload commands send a file in parts of 16 MB for that reason, each well inside the minute even on a slow uplink, so the
default can stay. Another proxy needs the same three things: TLS, the host name, and the published port behind it.
5. Connect Claude Code¶
The token goes into the client's MCP entry. In user scope the tools are there in every folder the client opens:
claude mcp add --transport http --scope user blender https://blender.example.com/mcp \
--header "Authorization: Bearer $(cat ~/.s3l-mcp-blender/remote/docker.token)"
Copy secrets/client.token from the machine to ~/.s3l-mcp-blender/remote/docker.token on the client, readable by you
alone (chmod 600). blender_diagnose then reports transport.kind: http with the public address and the link minutes,
the add-on as current and Blender's machine as linux.
6. A session¶
The server beside Blender sees none of the client's folders, so a project is a folder in the volume named after the folder the client works in, and files travel by commands the client runs itself:
blender_open_projectwith the name of the folder you work in opens or createsfiles/<name>/<name>.blend, withrenders/andtextures/beside it.blender_uploadwith aremotePathin the project —<name>/textures— returns a command that uploads through a link valid for this path only, for 15 minutes; without aremotePath, or with an absolute one, it refuses. The agent runs the command in its own shell, from the folder it works in: it packs a folder into a temporary tar, sends the file in parts of 16 MB withddandcurl, and the last part carries the SHA-256 of the whole, which the server checks before the file takes its name. 160 MB over a 2 MB/s uplink took 97 seconds this way.- Build and render as usual;
blender_set_cycleswithbackend: OPTIX(orCUDA) puts the scene on the GPU, and renders go into therenderspathblender_open_projectnamed. blender_downloadof<name>/rendersreturnscurl | tarthat unpacks the renders intorenders/of the client's folder, keeping files that are already there unlessoverwrite. The reply says how many files and bytes will arrive. The server checks every file against the SHA-256 the add-on computed as it sends it, and the command fails when one differs; a single file lands under a partial name first and takes its own only when whole.
7. The pages¶
The server's own address opens its pages — https://blender.example.com/. Sign in with the client token; the
session lasts twelve hours and is renewed while the pages are in use.
| Page | Shows |
|---|---|
| Status | refreshed every ten seconds: whether Blender answers, the add-on build, what Blender is busy with, the open file and unsaved changes, the GPU, render jobs, the size of each area of the volume and the free disk, the server |
| Files | the projects: folders and files with sizes; an image or a small text file opens as a preview; a file downloads, a folder comes as a zip |
| Jobs, Batches | background renders and batches with their state and progress; a queued or running job can be cancelled, and everything that ended can be deleted in one go |
| Snapshots | snapshots with their notes; each downloads as its .blend |
Deleting asks first and cannot be undone. The add-on refuses to delete the file open in Blender, a folder holding it, and a job or batch still queued or running, and the page says so.
8. Running it¶
| To | Command |
|---|---|
| see the state | docker compose ps |
| read the logs | docker compose logs -f blender or mcp |
| restart | docker compose restart |
| stop | docker compose stop — the scene open in Blender is lost unless saved |
| update | set the tags in .env, then docker compose pull && docker compose up -d |
| remove the containers | docker compose down — the volumes, and with them the projects, stay |
| remove everything | docker compose down -v — deletes every project, render and snapshot, the compiled GPU kernels and the keys that keep browsers signed in |
| back up the projects | docker run --rm -v s3l-blender_data:/data -v "$PWD":/backup ubuntu tar czf /backup/data.tgz -C /data . |
Docker Desktop on Windows starts with the user's session: after a reboot the containers come back once someone signs in to Windows.
Docker over SSH on Windows. An SSH session logged in with a key has no access to the Windows Credential Manager,
so docker pull and docker build fail with A specified logon session does not exist. Give that session a Docker
configuration without a credential helper — an empty config.json in a folder of its own named by DOCKER_CONFIG, and
a PATH holding a copy of docker.exe without docker-credential-wincred beside it.
A second stack on the same machine¶
One Blender holds one scene, and every key the server admits works in it: a second client sees what the first has open, reads its files, its journal and its render jobs, and with the Python scope reads anything Blender's process can. Keys divide what a client may do, not what it may see. Two people who must not see each other's work need a stack each.
A second stack is a second folder: a copy of compose.yml, its own .env and its own secrets/. In .env it takes a
STACK_NAME, a PUBLISH_PORT and a PUBLIC_URL of its own:
STACK_NAME=s3l-blender-own
PUBLIC_URL=https://blender-own.example.com
PUBLISH_PORT=8811
BLENDER_MEMORY=16g
The name gives it volumes of its own — s3l-blender-own_data and the rest — so nothing either Blender writes reaches the
other, and new tokens in secrets/ keep each server's clients its own. The proxy needs a second route, to the second
port. The two share the machine: the GPU and its memory, and the memory of the host, so give BLENDER_MEMORY values
that fit together, with room for the system. Both run the same images, so an update pulls once and is then brought up
in each folder.
Security¶
- The client token opens
/mcp, the files and the pages; keep it out of repositories. - The bridge token never leaves the machine, and Blender's network is internal: nothing but the server reaches it, and Blender reaches nothing outside.
- A link to a file names one path, one method and an expiry, signed with a key derived from the client token; rotating the token revokes every link.
- Files are served sandboxed (
Content-Security-Policy: sandbox), so an SVG or HTML file from the volume runs no script in the pages' origin. Forms carry antiforgery tokens, and the session cookie isHttpOnly,SameSite=StrictandSecurebehind anhttpsaddress. PYTHON_ACCESS=Fallbackkeepsblender_pythonfor what no tool covers, and reads apythonstep ofrun_toolsorblender_batchthe same way;Offrefuses Python altogether, the operators that run scripts included.