docker: add unsloth-studio-update for in-place Studio updates

Updating Studio in the container previously meant pulling a fresh ~25GB image
(or at best the ~6GB fused Studio layer) for what is usually a small Python/UI
change. Add a baked helper so a running container can update in place:

    docker exec <container> unsloth-studio-update

It updates only the Studio packages -- the backend code and the pre-built
frontend, which ships inside the unsloth wheel -- with `pip install -U
--no-deps unsloth unsloth_zoo`, then restarts just the studio service via
supervisor. The torch/CUDA stack is left untouched, so it is safe in both GPU
and CPU-only containers. This deliberately avoids `unsloth studio update`,
which re-runs the full installer and re-probes the GPU to pick torch wheels --
in a container started without --gpus that finds no GPU and can downgrade torch
to CPU/cu126.

Options: --ref <branch|tag|sha> installs from git (track main) instead of the
latest PyPI release; --with-deps also updates dependencies; --no-restart defers
the restart. After the swap the helper smoke-imports studio.backend.main and,
if a transitive dep is now missing, points the user at --with-deps.

The update lands in the container's writable layer (survives docker restart);
mount -v unsloth_studio_home:/opt/unsloth-studio to keep it across a recreate.
This commit is contained in:
Daniel Han 2026-06-24 06:16:54 +00:00
commit b897cf8e5f
3 changed files with 107 additions and 1 deletions

View file

@ -5,6 +5,7 @@
!fetch_llama_prebuilt.py
!supervisord.conf
!studio_launch.sh
!unsloth_studio_update.sh
!unsloth_nb_compat.py
!unsloth_pip_shim.py
!unsloth_ipython_startup.py

View file

@ -156,7 +156,11 @@ RUN set -eux \
COPY supervisord.conf /etc/supervisor/supervisord.conf
COPY studio_launch.sh /usr/local/bin/unsloth-studio-launch
RUN chmod +x /usr/local/bin/unsloth-studio-launch
# In-place Studio updater: `docker exec <container> unsloth-studio-update`
# refreshes the Studio packages (backend + baked frontend) and restarts the
# service, without pulling a new image or touching the torch/CUDA stack.
COPY unsloth_studio_update.sh /usr/local/bin/unsloth-studio-update
RUN chmod +x /usr/local/bin/unsloth-studio-launch /usr/local/bin/unsloth-studio-update
# Studio web UI, JupyterLab, sshd. All bind 0.0.0.0 inside the container's
# network namespace; the operator publishes them explicitly with -p.

101
docker/unsloth_studio_update.sh Executable file
View file

@ -0,0 +1,101 @@
#!/usr/bin/env bash
# Update Unsloth Studio in place, inside a running container, without pulling a
# new image. Updates ONLY the Studio Python packages (the backend code and the
# pre-built frontend, which ships inside the unsloth wheel) and restarts the
# Studio service. The torch/CUDA stack is left untouched.
#
# docker exec <container> unsloth-studio-update # latest PyPI release
# docker exec <container> unsloth-studio-update --ref main # latest git main
# docker exec <container> unsloth-studio-update --with-deps # also update deps
# docker exec <container> unsloth-studio-update --no-restart # update, restart later
#
# Why not `unsloth studio update`: that command re-runs the full installer,
# which re-probes the host GPU to pick torch wheels. In a CPU-only container
# (run without --gpus) it finds no GPU and can downgrade torch to CPU/cu126,
# breaking CUDA. This helper only touches the Studio packages, so it is safe in
# both GPU and CPU containers.
#
# Persistence: the update is written to the container's writable layer, so it
# survives `docker restart`. To keep it across a full `docker rm` + `docker run`
# (and to keep your chats/users/models), run Studio with its home on a named
# volume: -v unsloth_studio_home:/opt/unsloth-studio
set -euo pipefail
STUDIO_HOME="${UNSLOTH_STUDIO_HOME:-/opt/unsloth-studio}"
REF=""
NO_DEPS="--no-deps"
RESTART=1
PACKAGES="unsloth unsloth_zoo"
usage() { sed -n '2,21p' "$0"; }
while [ $# -gt 0 ]; do
case "$1" in
--ref) REF="$2"; shift 2;;
--with-deps) NO_DEPS=""; shift;;
--no-restart) RESTART=0; shift;;
--packages) PACKAGES="$2"; shift 2;;
-h|--help) usage; exit 0;;
*) echo "unsloth-studio-update: unknown argument: $1" >&2; usage; exit 2;;
esac
done
# Resolve the Studio venv python. Prefer the venv directly; fall back to
# following the launcher symlink ($STUDIO_HOME/bin/unsloth -> venv/bin/unsloth).
PY=""
for cand in \
"$STUDIO_HOME/unsloth_studio/bin/python" \
"$STUDIO_HOME/unsloth_studio/bin/python3"; do
[ -x "$cand" ] && { PY="$cand"; break; }
done
if [ -z "$PY" ] && [ -L "$STUDIO_HOME/bin/unsloth" ]; then
venv_bin="$(dirname "$(readlink -f "$STUDIO_HOME/bin/unsloth")")"
[ -x "$venv_bin/python" ] && PY="$venv_bin/python"
fi
[ -n "$PY" ] || { echo "unsloth-studio-update: could not find the Studio venv under $STUDIO_HOME" >&2; exit 1; }
version_of() { "$PY" -c "from importlib.metadata import version; print(version('unsloth'))" 2>/dev/null || echo "unknown"; }
echo "[studio-update] Studio venv: $PY"
echo "[studio-update] before: unsloth $(version_of)"
# Build the package specs. With --ref, install from git so you can track main
# (or any branch/tag/sha); otherwise take the latest PyPI release.
if [ -n "$REF" ]; then
SPECS="git+https://github.com/unslothai/unsloth.git@${REF}#egg=unsloth"
SPECS="$SPECS git+https://github.com/unslothai/unsloth-zoo.git@${REF}#egg=unsloth_zoo"
echo "[studio-update] installing from git @${REF}"
else
SPECS="$PACKAGES"
echo "[studio-update] installing latest release of: $PACKAGES"
fi
# shellcheck disable=SC2086
"$PY" -m pip install -U $NO_DEPS $SPECS
echo "[studio-update] after: unsloth $(version_of)"
# Sanity: the backend must still import after the swap (a missing transitive
# dep from --no-deps shows up here). Non-fatal: just warn with the remedy.
if ! "$PY" -c "import studio.backend.main" >/dev/null 2>&1; then
echo "[studio-update] WARNING: 'import studio.backend.main' failed after update." >&2
echo "[studio-update] A new dependency may be missing. Re-run with --with-deps:" >&2
echo "[studio-update] unsloth-studio-update --with-deps" >&2
fi
if [ "$RESTART" = "1" ]; then
SUPCTL="$(command -v supervisorctl || true)"
[ -n "$SUPCTL" ] || SUPCTL="/opt/unsloth-venv/bin/supervisorctl"
if [ -x "$SUPCTL" ] && "$SUPCTL" status studio >/dev/null 2>&1; then
echo "[studio-update] restarting the studio service"
"$SUPCTL" restart studio
else
echo "[studio-update] supervisor not managing 'studio' here; restart Studio yourself"
echo "[studio-update] (e.g. 'docker restart <container>')"
fi
else
echo "[studio-update] --no-restart: restart Studio to load the update"
echo "[studio-update] docker exec <container> supervisorctl restart studio"
fi
echo "[studio-update] done"