Sandbox image (bring your own container)
A compute backend decides where a kernel runs (docker, podman, kubernetes, modal, coreweave). The sandbox image decides what runs inside it — the Python version, system libraries, and pre-installed packages your notebooks get.
You bring your own image. Set it once per deployment:
MARIMOHUB_COMPUTE_IMAGE=ghcr.io/orgname/marimo-sandbox:latestEvery container-based backend reads this same variable. (local runs uv on the host and needs no image; e2b uses an E2B template instead — see Compute.)
The repository image also publishes optional session surface targets:
| Target | Published tag suffix | Included tools |
|---|---|---|
vscode | -vscode | code-server and pinned extensions |
opencode | -opencode | OpenCode, ripgrep, jq, and marimo-pair |
tools | -tools | VS Code plus the OpenCode tools |
OpenCode is pinned to 1.18.17 with separate SHA-256 checksums for Linux x64 and arm64 builds.
Build requirement: All
images/marimo-sandboxtargets require thenotebook_bridgenamed build context. Older build commands without this context fail. See Build the maintained image.
Multiple images
To offer more than one image — say a lean default plus a GPU or heavy-ML variant — set MARIMOHUB_COMPUTE_IMAGE to a comma-separated list:
MARIMOHUB_COMPUTE_IMAGE=ghcr.io/orgname/marimo-sandbox:latest,ghcr.io/orgname/marimo-gpu:latest- The first image is the default. Every notebook uses it unless it chooses otherwise.
- Notebooks pick per-notebook. The create-notebook dialog and the notebook menu's Change base image action offer "Default" plus each listed image. Only a non-default choice is stored (in the notebook's metadata); "Default" always follows the first image, even if you later reorder the list.
- Changes apply on the next session start. A running kernel keeps the image it booted from; stop and restart the session to switch.
- Removing an image from the list is safe. A notebook still pointing at it falls back to the default at session start (with a server-side warning), and its stored choice can be updated to any currently-listed image.
- The full list is served to clients via
GET /api/v1/capabilities(sandbox_images);GET /api/v1/versionreports only the default.
For the e2b backend the same applies to MARIMOHUB_COMPUTE_E2B_TEMPLATE, which takes a comma-separated list of template ids.
With CoreWeave filesystem snapshots enabled, a notebook restoring from a snapshot boots the snapshot's image — a base-image change takes effect once no restore pointer exists (e.g. after the snapshot is dropped).
The contract
marimohub copies the notebook into the image and launches the kernel itself, reusing the image's pre-installed environment. With cwd /workspace:
uv sync --inexact --no-install-package marimo --no-compile-bytecode # add the notebook's deps (skipped when it declares none)
uv run --no-sync marimo --quiet edit notebook.py --headless --token --token-password-file /tmp/.marimohub-kernel-token --host 0.0.0.0 --port 2718The provisioner creates the password file after it writes other session credentials. The file is outside /workspace, so workspace snapshots cannot capture it. Custom images must use a marimo version that supports --token-password-file.
During the sync, --no-install-package marimo keeps the image's pinned marimo version even if the notebook declares another version.
Inline dependencies
For local and synced notebooks, marimohub installs PEP 723 dependencies after project sync, before the edit session, app session, or job starts. Apps and jobs use their selected source version.
[ -d "${UV_PROJECT_ENVIRONMENT:-.venv}" ] || uv venv "${UV_PROJECT_ENVIRONMENT:-.venv}"
uv export --script notebook.py --format requirements-txt --no-hashes --prune marimo -o "${UV_PROJECT_ENVIRONMENT:-.venv}/marimohub-script-requirements.txt"
uv pip install --python "${UV_PROJECT_ENVIRONMENT:-.venv}" -r "${UV_PROJECT_ENVIRONMENT:-.venv}/marimohub-script-requirements.txt"Setup failures stop startup with PYTHON_ENV_SETUP_FAILED. The export excludes marimo and packages used only by marimo, preserving the image's marimo version. Direct notebook dependencies remain in the export.
Project and inline requirements resolve separately. Inline pins can replace project versions and leave project requirements unsatisfied. Keep requirements compatible across both files. Headers remain unchanged and are not copied into pyproject.toml. Dependency changes apply at the next sandbox startup.
Installation reuses the image environment, without marimo --sandbox isolation. Inline requires-python does not select the kernel Python. Use an image with a compatible Python version.
Custom indexes must be available to both export and installation. Configure them for uv pip install through sandbox uv configuration, not only in the header.
Image requirements
So your image must provide:
uvonPATH— the launch command isuv sync+uv run ….- marimo + your common libraries pre-installed into the project environment (
UV_PROJECT_ENVIRONMENT, also activated asVIRTUAL_ENV), at a pinned marimo version. This is what makes the base case start instantly — no per-launch install. - A POSIX shell (
sh) + coreutils —base64,mkdir,rm,cat,true. marimohub transfers notebook files and probes reachability with these. git— only if notebooks use git checkouts, but cheap to include.- A writable working directory (
/workspace) and a writable parent forUV_PROJECT_ENVIRONMENT. uv can replace the whole environment to change Python versions. If the user cannot write to/workspace, setMARIMOHUB_COMPUTE_WORKDIRto a writable directory. - PyPI egress at runtime for notebooks that use libraries beyond the pre-installed base.
No marimo entrypoint or CMD is required — marimohub supplies the launch command. The repository images use a generated token in their quiet default CMD. For a standalone check, publish the port and read the token from the container:
cid=$(docker run -d -p 127.0.0.1:2718:2718 IMAGE)
token=$(docker exec "$cid" cat /tmp/.marimohub-kernel-token)
printf 'http://127.0.0.1:2718/?access_token=%s\n' "$token"Don't set
UV_COMPILE_BYTECODEin your image. Compile bytecode at build with the--compile-bytecodeflag (fast imports for the pre-installed base). The startup sync passes--no-compile-bytecodeto skip the ~5s of compiling freshly added deps on the launch path — and uv errors if both the env var and the flag are set.
Configure source builds
Source builds are allowed by default. UV_NO_BUILD controls both project sync and inline dependency installation.
To disable source builds, add this line after dependency installation in your sandbox Dockerfile:
ENV UV_NO_BUILD=trueUse ENV UV_NO_BUILD=false in a derived image to remove this restriction. False or unset values leave the decision to the notebook's uv configuration.
For the example E2B template, add this line to files/marimo.sh and rebuild the template:
export UV_NO_BUILD="${UV_NO_BUILD:-true}"This defaults to true and preserves an explicit false in the shell environment. The template's .setEnvs() affects build steps only.
Set the variable in the sandbox environment before dependency setup starts. The hub server's environment does not configure remote sandboxes. Image changes apply to new sandboxes, subject to the snapshot behavior described above.
Build exceptions depend on the uv version and command. See UV_NO_BUILD. This setting does not isolate notebook code or protect session credentials.
Why pre-install (not just cache)
uv sync builds a notebook's environment at startup. Even with a warm download cache, materializing marimo + its recommended extras (~70 packages) into a fresh venv takes a few seconds — paid on every kernel. Instead, pre-install marimo and your common libraries into the image's project environment. Then uv sync --inexact only needs to add a notebook's extra deps (and keeps the base), so the common case is a near-instant no-op.
Pin marimo to a specific version so it's consistent and never silently upgrades; bump it deliberately. Encode the Python + marimo version in the image tag (e.g. py3.13-marimo0.23.10) so each image is traceable.
Build your own
The examples/sandbox-image/ directory is a ready-to-fork starting point. marimo is pinned via the MARIMO_VERSION build arg and installed alongside the libraries in warm/pyproject.toml into /opt/marimohub/venv:
FROM python:3.13-slim
ARG MARIMO_VERSION=0.23.10
ENV MARIMO_VERSION=${MARIMO_VERSION}
COPY --from=ghcr.io/astral-sh/uv:0.10.9 /uv /uvx /usr/local/bin/
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates \
&& rm -rf /var/lib/apt/lists/*
ENV UV_PROJECT_ENVIRONMENT=/opt/marimohub/venv UV_LINK_MODE=copy
# Skip marimo's PyPI version ping, and continuously snapshot the notebook to
# __marimo__/notebook.html so the host captures it on teardown.
ENV MARIMO_SKIP_UPDATE_CHECK=1 _MARIMO_APP_OVERLOAD_AUTO_DOWNLOAD=[html]
RUN useradd -m appuser && mkdir -p /workspace && chown -R appuser:appuser /workspace
COPY warm/pyproject.toml /tmp/base/pyproject.toml
RUN cd /tmp/base && uv add --compile-bytecode "marimo[recommended]==${MARIMO_VERSION}" \
&& uv cache clean && rm -rf /tmp/base && chown -R appuser:appuser /opt/marimohub
ENV VIRTUAL_ENV=/opt/marimohub/venv PATH=/opt/marimohub/venv/bin:$PATH
USER appuser
WORKDIR /workspaceBuild with a version-stamped tag, push, and point MARIMOHUB_COMPUTE_IMAGE at it:
docker build --build-arg MARIMO_VERSION=0.23.10 \
-t ghcr.io/orgname/marimo-sandbox:py3.13-marimo0.23.10 examples/sandbox-image
docker push ghcr.io/orgname/marimo-sandbox:py3.13-marimo0.23.10To upgrade marimo, bump MARIMO_VERSION and rebuild. To change the pre-installed libraries, edit warm/pyproject.toml; add system libraries with extra apt-get install lines.
Private registries
| Backend | How to authenticate the pull |
|---|---|
| Kubernetes | Create an image pull secret; set MARIMOHUB_COMPUTE_KUBERNETES_IMAGE_PULL_SECRET to its name. |
| Docker | docker login on the server's Docker daemon — its credentials pull the image. |
| Podman | podman login for the server user or configured remote connection. |
| Modal | Configure registry credentials in Modal, or use a named image (modal://name[:tag]). |
| CoreWeave | Configure registry credentials in CoreWeave; pass the image reference. |
Private package indexes
Configure private Python package indexes with the package registry integrations, not with image ENV lines. The hub injects the uv index settings and credentials into each editor session and job before uv sync starts. Restricted viewer sandboxes receive no integrations, so private dependencies do not resolve there. Presets exist for AWS CodeArtifact, JFrog Artifactory, Azure Artifacts, and GitLab. For any other index (Nexus, devpi, Google Artifact Registry, an internal mirror), use the generic Python package index integration with the index's simple-API URL.
How injected values combine with other sources:
- If an Environment variables integration also sets
UV_INDEX, the hub merges the values: package registry entries first, then the environment-variable entries, without duplicates. - An Environment variables integration that sets
UV_DEFAULT_INDEXto a value different from the package registry's stops session creation. Use the registry's Default index option instead. - An
ENV UV_INDEXorENV UV_DEFAULT_INDEXin the sandbox image applies only while no integration sets that variable. An injected value replaces it.
uv sync runs once at session start. An index change applies to new sessions, not to running kernels.
Notebook URL bridge
Hub installs the bundled notebook bridge into the active marimo interpreter before the CLI starts. Both uv-sync-edit and uv-script-pins support installation. The installer uses uv pip install --no-deps --no-index and preserves the installed marimo version. Custom environment paths work through sys.executable.
The wheel and launcher live outside captured notebook files. An artifact identity prevents repeat installation. New environments and changed artifacts trigger installation again. Installation failures preserve the normal launch behavior.
No image change is required. The environment must permit local wheel installation. See the bridge package guide for timeout, fallback, and compatibility details.
Build the maintained image
The maintained images/marimo-sandbox image includes the notebook bridge wheel. Its default, base, release, vscode, opencode, and tools targets all require the notebook_bridge named build context. Builds without this context fail.
From the repository root, use:
docker build --build-context notebook_bridge=packages/notebook-bridge \
-t marimo-sandbox:local images/marimo-sandboxFor an optional target, add --target vscode, --target opencode, or --target tools. The publish workflow supplies this context automatically. At launch, the bridge checks its artifact identity and skips installation when the identity matches. Custom images and older images retain the offline installation fallback.
For a standalone image without bridge preinstallation, use examples/sandbox-image. That Dockerfile needs no named context. Hub installs the bridge at launch.