diff --git a/studio/backend/main.py b/studio/backend/main.py
index 65f5e7fe90..7b116cf8d7 100644
--- a/studio/backend/main.py
+++ b/studio/backend/main.py
@@ -176,12 +176,15 @@ async def health_check():
platform_map = {"darwin": "mac", "win32": "windows", "linux": "linux"}
device_type = platform_map.get(sys.platform, sys.platform)
+ is_docker = os.path.exists("/.dockerenv")
+
return {
"status": "healthy",
"timestamp": datetime.now().isoformat(),
"service": "Unsloth UI Backend",
"device_type": device_type,
"chat_only": _hw_module.CHAT_ONLY,
+ "is_docker": is_docker,
}
diff --git a/studio/frontend/src/components/navbar.tsx b/studio/frontend/src/components/navbar.tsx
index 99909b80fa..4958ae2258 100644
--- a/studio/frontend/src/components/navbar.tsx
+++ b/studio/frontend/src/components/navbar.tsx
@@ -55,6 +55,7 @@ export function Navbar() {
const [shutdownOpen, setShutdownOpen] = useState(false);
const chatOnly = usePlatformStore((s) => s.isChatOnly());
+ const isDocker = usePlatformStore((s) => s.isDocker);
// Warn before closing the tab only when training is running (data loss risk).
// We store the handler in a ref so removeUnloadHandler() can clean it up
@@ -236,15 +237,17 @@ export function Navbar() {
Tour
-
+ {!isDocker && (
+
+ )}
{/* Right: mobile */}
@@ -331,17 +334,19 @@ export function Navbar() {
Start tour
) : null}
-
+ {!isDocker && (
+
+ )}
Theme
boolean;
}
@@ -24,6 +25,7 @@ interface PlatformState {
export const usePlatformStore = create()((_, get) => ({
deviceType: "linux",
chatOnly: false,
+ isDocker: false,
fetched: false,
isChatOnly: () => get().chatOnly,
}));
@@ -35,10 +37,11 @@ export async function fetchDeviceType(): Promise {
try {
const res = await fetch("/api/health");
if (res.ok) {
- const data = (await res.json()) as { device_type?: string; chat_only?: boolean };
+ const data = (await res.json()) as { device_type?: string; chat_only?: boolean; is_docker?: boolean };
const deviceType = data.device_type ?? "linux";
const chatOnly = data.chat_only ?? deviceType === "mac";
- usePlatformStore.setState({ deviceType, chatOnly, fetched: true });
+ const isDocker = data.is_docker ?? false;
+ usePlatformStore.setState({ deviceType, chatOnly, isDocker, fetched: true });
return deviceType;
}
} catch (err) {
diff --git a/studio/install_python_stack.py b/studio/install_python_stack.py
index e8ae22f470..6db18b5d9e 100644
--- a/studio/install_python_stack.py
+++ b/studio/install_python_stack.py
@@ -416,169 +416,157 @@ def install_python_stack() -> int:
base_total -= 1 # triton step is skipped on macOS
_TOTAL = (base_total - 1) if skip_base else base_total
- # 1. Try to use uv for faster installs (must happen before pip upgrade
- # because uv venvs don't include pip by default)
- USE_UV = _bootstrap_uv()
+ # # 1. Try to use uv for faster installs (must happen before pip upgrade
+ # # because uv venvs don't include pip by default)
+ # USE_UV = _bootstrap_uv()
- # 2. Ensure pip is available (uv venvs created by install.sh don't include pip)
- _progress("pip bootstrap")
- if USE_UV:
- run(
- "Bootstrapping pip via uv",
- [
- "uv",
- "pip",
- "install",
- "--python",
- sys.executable,
- "pip",
- ],
- )
- else:
- # pip may not exist yet (uv-created venvs omit it). Try ensurepip
- # first, then upgrade. Only fall back to a direct upgrade when pip
- # is already present.
- _has_pip = (
- subprocess.run(
- [sys.executable, "-m", "pip", "--version"],
- stdout = subprocess.DEVNULL,
- stderr = subprocess.DEVNULL,
- ).returncode
- == 0
- )
+ # # 2. Ensure pip is available (uv venvs created by install.sh don't include pip)
+ # _progress("pip bootstrap")
+ # if USE_UV:
+ # run(
+ # "Bootstrapping pip via uv",
+ # [
+ # "uv",
+ # "pip",
+ # "install",
+ # "--python",
+ # sys.executable,
+ # "pip",
+ # ],
+ # )
+ # else:
+ # # pip may not exist yet (uv-created venvs omit it). Try ensurepip
+ # # first, then upgrade. Only fall back to a direct upgrade when pip
+ # # is already present.
+ # _has_pip = (
+ # subprocess.run(
+ # [sys.executable, "-m", "pip", "--version"],
+ # stdout = subprocess.DEVNULL,
+ # stderr = subprocess.DEVNULL,
+ # ).returncode
+ # == 0
+ # )
+ #
+ # if not _has_pip:
+ # run(
+ # "Bootstrapping pip via ensurepip",
+ # [sys.executable, "-m", "ensurepip", "--upgrade"],
+ # )
+ # else:
+ # run(
+ # "Upgrading pip",
+ # [sys.executable, "-m", "pip", "install", "--upgrade", "pip"],
+ # )
- if not _has_pip:
- run(
- "Bootstrapping pip via ensurepip",
- [sys.executable, "-m", "ensurepip", "--upgrade"],
- )
- else:
- run(
- "Upgrading pip",
- [sys.executable, "-m", "pip", "install", "--upgrade", "pip"],
- )
+ # # 3. Core packages: unsloth-zoo + unsloth (or custom package name)
+ # if skip_base:
+ # print(_green(f"✅ {package_name} already installed — skipping base packages"))
+ # elif NO_TORCH:
+ # # No-torch update path: install unsloth + unsloth-zoo with --no-deps
+ # # (current PyPI metadata still declares torch as a hard dep), then
+ # # runtime deps with --no-deps (avoids transitive torch).
+ # _progress("base packages (no torch)")
+ # pip_install(
+ # f"Updating {package_name} + unsloth-zoo (no-torch mode)",
+ # "--no-cache-dir",
+ # "--no-deps",
+ # "--upgrade-package",
+ # package_name,
+ # "--upgrade-package",
+ # "unsloth-zoo",
+ # package_name,
+ # "unsloth-zoo",
+ # )
+ # pip_install(
+ # "Installing no-torch runtime deps",
+ # "--no-cache-dir",
+ # "--no-deps",
+ # req = REQ_ROOT / "no-torch-runtime.txt",
+ # )
+ # if local_repo:
+ # pip_install(
+ # "Overlaying local repo (editable)",
+ # "--no-cache-dir",
+ # "--no-deps",
+ # "-e",
+ # local_repo,
+ # constrain = False,
+ # )
+ # elif local_repo:
+ # _progress("base packages")
+ # pip_install(
+ # "Updating base packages",
+ # "--no-cache-dir",
+ # "--upgrade-package",
+ # "unsloth",
+ # "--upgrade-package",
+ # "unsloth-zoo",
+ # req = REQ_ROOT / "base.txt",
+ # )
+ # pip_install(
+ # "Overlaying local repo (editable)",
+ # "--no-cache-dir",
+ # "--no-deps",
+ # "-e",
+ # local_repo,
+ # constrain = False,
+ # )
+ # elif package_name != "unsloth":
+ # _progress("base packages")
+ # pip_install(
+ # f"Installing {package_name}",
+ # "--no-cache-dir",
+ # package_name,
+ # )
+ # else:
+ # _progress("base packages")
+ # pip_install(
+ # "Updating base packages",
+ # "--no-cache-dir",
+ # "--upgrade-package",
+ # "unsloth",
+ # "--upgrade-package",
+ # "unsloth-zoo",
+ # req = REQ_ROOT / "base.txt",
+ # )
- # 3. Core packages: unsloth-zoo + unsloth (or custom package name)
- if skip_base:
- print(_green(f"✅ {package_name} already installed — skipping base packages"))
- elif NO_TORCH:
- # No-torch update path: install unsloth + unsloth-zoo with --no-deps
- # (current PyPI metadata still declares torch as a hard dep), then
- # runtime deps with --no-deps (avoids transitive torch).
- _progress("base packages (no torch)")
- pip_install(
- f"Updating {package_name} + unsloth-zoo (no-torch mode)",
- "--no-cache-dir",
- "--no-deps",
- "--upgrade-package",
- package_name,
- "--upgrade-package",
- "unsloth-zoo",
- package_name,
- "unsloth-zoo",
- )
- pip_install(
- "Installing no-torch runtime deps",
- "--no-cache-dir",
- "--no-deps",
- req = REQ_ROOT / "no-torch-runtime.txt",
- )
- if local_repo:
- pip_install(
- "Overlaying local repo (editable)",
- "--no-cache-dir",
- "--no-deps",
- "-e",
- local_repo,
- constrain = False,
- )
- elif local_repo:
- # Local dev install: update deps from base.txt, then overlay the
- # local checkout as an editable install (--no-deps so torch is
- # never re-resolved).
- _progress("base packages")
- pip_install(
- "Updating base packages",
- "--no-cache-dir",
- "--upgrade-package",
- "unsloth",
- "--upgrade-package",
- "unsloth-zoo",
- req = REQ_ROOT / "base.txt",
- )
- pip_install(
- "Overlaying local repo (editable)",
- "--no-cache-dir",
- "--no-deps",
- "-e",
- local_repo,
- constrain = False,
- )
- elif package_name != "unsloth":
- # Custom package name (e.g. roland-sloth for testing) — install directly
- _progress("base packages")
- pip_install(
- f"Installing {package_name}",
- "--no-cache-dir",
- package_name,
- )
- else:
- # Update path: upgrade only unsloth + unsloth-zoo while preserving
- # existing torch/CUDA installations. Torch is pre-installed by
- # install.sh / setup.ps1; --upgrade-package targets only base pkgs.
- _progress("base packages")
- pip_install(
- "Updating base packages",
- "--no-cache-dir",
- "--upgrade-package",
- "unsloth",
- "--upgrade-package",
- "unsloth-zoo",
- req = REQ_ROOT / "base.txt",
- )
+ # # 3. Extra dependencies
+ # _progress("unsloth extras")
+ # pip_install(
+ # "Installing additional unsloth dependencies",
+ # "--no-cache-dir",
+ # req = REQ_ROOT / "extras.txt",
+ # )
- # 3. Extra dependencies
- _progress("unsloth extras")
- pip_install(
- "Installing additional unsloth dependencies",
- "--no-cache-dir",
- req = REQ_ROOT / "extras.txt",
- )
+ # # 3b. Extra dependencies (no-deps) -- audio model support etc.
+ # _progress("extra codecs")
+ # pip_install(
+ # "Installing extras (no-deps)",
+ # "--no-deps",
+ # "--no-cache-dir",
+ # req = REQ_ROOT / "extras-no-deps.txt",
+ # )
- # 3b. Extra dependencies (no-deps) -- audio model support etc.
- _progress("extra codecs")
- pip_install(
- "Installing extras (no-deps)",
- "--no-deps",
- "--no-cache-dir",
- req = REQ_ROOT / "extras-no-deps.txt",
- )
+ # # 4. Overrides (torchao, transformers) -- force-reinstall
+ # _progress("dependency overrides")
+ # pip_install(
+ # "Installing dependency overrides",
+ # "--force-reinstall",
+ # "--no-cache-dir",
+ # req = REQ_ROOT / "overrides.txt",
+ # )
- # 4. Overrides (torchao, transformers) -- force-reinstall
- # Skip entirely when torch is unavailable (e.g. Intel Mac GGUF-only mode)
- # because overrides.txt contains torchao which requires torch.
- if NO_TORCH:
- _progress("dependency overrides (skipped, no torch)")
- else:
- _progress("dependency overrides")
- pip_install(
- "Installing dependency overrides",
- "--force-reinstall",
- "--no-cache-dir",
- req = REQ_ROOT / "overrides.txt",
- )
-
- # 5. Triton kernels (no-deps, from source)
- # Skip on Windows (no support) and macOS (no support).
- if not IS_WINDOWS and not IS_MACOS:
- _progress("triton kernels")
- pip_install(
- "Installing triton kernels",
- "--no-deps",
- "--no-cache-dir",
- req = REQ_ROOT / "triton-kernels.txt",
- constrain = False,
- )
+ # # 5. Triton kernels (no-deps, from source)
+ # # Skip on Windows (no support) and macOS (no support).
+ # if not IS_WINDOWS and not IS_MACOS:
+ # _progress("triton kernels")
+ # pip_install(
+ # "Installing triton kernels",
+ # "--no-deps",
+ # "--no-cache-dir",
+ # req = REQ_ROOT / "triton-kernels.txt",
+ # constrain = False,
+ # )
# # 6. Patch: override llama_cpp.py with fix from unsloth-zoo feature/llama-cpp-windows-support branch
# patch_package_file(
diff --git a/studio/setup.sh b/studio/setup.sh
index 7502270276..cb3fbf83a3 100755
--- a/studio/setup.sh
+++ b/studio/setup.sh
@@ -270,7 +270,7 @@ if [ -d "$SCRIPT_DIR/backend/core/data_recipe/oxc-validator" ] && command -v npm
fi
# ── Python venv + deps ──
-STUDIO_HOME="$HOME/.unsloth/studio"
+STUDIO_HOME="${UNSLOTH_STUDIO_HOME:-$HOME/.unsloth/studio}"
VENV_DIR="$STUDIO_HOME/unsloth_studio"
VENV_T5_DIR="$STUDIO_HOME/.venv_t5"
@@ -280,7 +280,12 @@ VENV_T5_DIR="$STUDIO_HOME/.venv_t5"
# Note: do NOT delete $STUDIO_HOME/.venv here — install.sh handles migration
_COLAB_NO_VENV=false
-if [ ! -x "$VENV_DIR/bin/python" ]; then
+_DOCKER_NO_VENV=false
+if [ -n "$UNSLOTH_DOCKER" ]; then
+ # Docker: packages already in /opt/conda — skip venv entirely.
+ # Only pre-install .venv_t5 for transformers 5.x switching (handled below).
+ _DOCKER_NO_VENV=true
+elif [ ! -x "$VENV_DIR/bin/python" ]; then
if [ "$IS_COLAB" = true ]; then
# On Colab there is no Studio venv -- install backend deps into system Python.
# Strip all version constraints so pip keeps Colab's pre-installed
@@ -328,6 +333,38 @@ if [ "$_COLAB_NO_VENV" = true ]; then
echo "✅ Studio backend dependencies installed into system Python"
fi
+# In Docker, packages are pre-installed in /opt/conda — only install missing
+# studio/data-designer deps and pre-install .venv_t5 for transformers 5.x.
+if [ "$_DOCKER_NO_VENV" = true ]; then
+ echo " Docker detected — skipping venv activation."
+
+ # Install branch's unsloth/unsloth_cli/studio into /opt/conda
+ # (overwrites PyPI version with Docker-aware code)
+ echo " Installing local unsloth from branch..."
+ pip install --force-reinstall --no-deps "$REPO_ROOT"
+
+ # Install only missing deps (studio, data-designer, plugin, metadata patch).
+ # Heavy packages (torch, unsloth, vllm, etc.) are already in /opt/conda.
+ # install_python_stack.py has steps 1-5 commented out for this branch.
+ python "$SCRIPT_DIR/install_python_stack.py"
+
+ # Pre-install transformers 5.x into .venv_t5
+ echo ""
+ echo " Pre-installing transformers 5.x for newer model support..."
+ mkdir -p "$VENV_T5_DIR"
+ pip install --target "$VENV_T5_DIR" --no-deps "transformers==5.3.0" 2>/dev/null
+ pip install --target "$VENV_T5_DIR" --no-deps "huggingface_hub==1.7.1" 2>/dev/null
+ pip install --target "$VENV_T5_DIR" --no-deps "hf_xet==1.4.2" 2>/dev/null
+ pip install --target "$VENV_T5_DIR" "tiktoken" 2>/dev/null
+ echo "✅ Transformers 5.x pre-installed to $VENV_T5_DIR/"
+
+ echo ""
+ echo "╔══════════════════════════════════════╗"
+ echo "║ Docker Studio Setup Complete! ║"
+ echo "╚══════════════════════════════════════╝"
+ exit 0
+fi
+
# ── Check if Python deps need updating ──
# Compare installed package version against PyPI latest.
# Skip all Python dependency work if versions match (fast update path).
diff --git a/unsloth_cli/commands/studio.py b/unsloth_cli/commands/studio.py
index f56f582c14..4b7de9d481 100644
--- a/unsloth_cli/commands/studio.py
+++ b/unsloth_cli/commands/studio.py
@@ -84,56 +84,58 @@ def studio_default(
if ctx.invoked_subcommand is not None:
return
- # Always use the studio venv if it exists and we're not already in it
- studio_venv_dir = STUDIO_HOME / "unsloth_studio"
- in_studio_venv = sys.prefix.startswith(str(studio_venv_dir))
+ # In Docker, packages live in /opt/conda — skip venv re-exec entirely.
+ if not os.environ.get("UNSLOTH_DOCKER"):
+ # Always use the studio venv if it exists and we're not already in it
+ studio_venv_dir = STUDIO_HOME / "unsloth_studio"
+ in_studio_venv = sys.prefix.startswith(str(studio_venv_dir))
- if not in_studio_venv:
- studio_python = _studio_venv_python()
- run_py = _find_run_py()
- if studio_python and run_py:
- if not silent:
- typer.echo("Launching Unsloth Studio... Please wait...")
- args = [
- str(studio_python),
- str(run_py),
- "--host",
- host,
- "--port",
- str(port),
- ]
- if frontend:
- args.extend(["--frontend", str(frontend)])
- if silent:
- args.append("--silent")
- # On Windows, os.execvp() spawns a child but the parent lingers,
- # so Ctrl+C only kills the parent leaving the child orphaned.
- # Use subprocess.run() on Windows so the parent waits for the child.
- if sys.platform == "win32":
- import subprocess as _sp
+ if not in_studio_venv:
+ studio_python = _studio_venv_python()
+ run_py = _find_run_py()
+ if studio_python and run_py:
+ if not silent:
+ typer.echo("Launching Unsloth Studio... Please wait...")
+ args = [
+ str(studio_python),
+ str(run_py),
+ "--host",
+ host,
+ "--port",
+ str(port),
+ ]
+ if frontend:
+ args.extend(["--frontend", str(frontend)])
+ if silent:
+ args.append("--silent")
+ # On Windows, os.execvp() spawns a child but the parent lingers,
+ # so Ctrl+C only kills the parent leaving the child orphaned.
+ # Use subprocess.run() on Windows so the parent waits for the child.
+ if sys.platform == "win32":
+ import subprocess as _sp
- proc = _sp.Popen(args)
- try:
- rc = proc.wait()
- except KeyboardInterrupt:
- # Child has its own signal handler — let it finish
- rc = proc.wait()
- if rc != 0:
- typer.echo(
- f"\nError: Studio server exited unexpectedly (code {rc}).",
- err = True,
- )
- typer.echo(
- "Check the error above. If a package is missing, "
- "re-run: unsloth studio setup",
- err = True,
- )
- raise typer.Exit(rc)
+ proc = _sp.Popen(args)
+ try:
+ rc = proc.wait()
+ except KeyboardInterrupt:
+ # Child has its own signal handler — let it finish
+ rc = proc.wait()
+ if rc != 0:
+ typer.echo(
+ f"\nError: Studio server exited unexpectedly (code {rc}).",
+ err = True,
+ )
+ typer.echo(
+ "Check the error above. If a package is missing, "
+ "re-run: unsloth studio setup",
+ err = True,
+ )
+ raise typer.Exit(rc)
+ else:
+ os.execvp(str(studio_python), args)
else:
- os.execvp(str(studio_python), args)
- else:
- typer.echo("Studio not set up. Run install.sh first.")
- raise typer.Exit(1)
+ typer.echo("Studio not set up. Run install.sh first.")
+ raise typer.Exit(1)
from studio.backend.run import run_server