docker: optional Cloudflare tunnel for JupyterLab (UNSLOTH_JUPYTER_CLOUDFLARE)

Mirror the public-link convenience Studio already has for its own UI, for
JupyterLab. Off by default; opt in two ways:

  docker run -e UNSLOTH_JUPYTER_CLOUDFLARE=1 ... unsloth/unsloth
  docker exec <container> unsloth-jupyter-tunnel --force

unsloth-jupyter-tunnel waits for JupyterLab, reuses a cached cloudflared (or
fetches the static binary for the arch, no account needed), and starts a
quick tunnel to the Jupyter port; the https://<name>.trycloudflare.com URL is
printed to docker logs. supervisord runs it as the jupyter-cloudflare program,
autostarted only when UNSLOTH_JUPYTER_CLOUDFLARE=1 (studio_launch.sh exports a
0 default so the autostart gate expands, matching the sshd pattern). JupyterLab
still enforces its password, so the tunnel is not an open door.

Verified: the helper fetches cloudflared and mints a working trycloudflare URL
that reaches JupyterLab (HTTP 200) inside a running container.
This commit is contained in:
Daniel Han 2026-06-24 09:55:11 +00:00
commit 08f9b67f60
5 changed files with 89 additions and 1 deletions

View file

@ -7,6 +7,7 @@
!studio_launch.sh
!unsloth_studio_update.sh
!unsloth_llama_update.sh
!unsloth_jupyter_tunnel.sh
!unsloth_nb_compat.py
!unsloth_pip_shim.py
!unsloth_ipython_startup.py

View file

@ -167,9 +167,13 @@ COPY unsloth_llama_update.sh /usr/local/bin/unsloth-llama-update
# API, so it is not rate-limited; deterministic portable bundle that runs on CPU
# and every supported GPU) rather than the host-probing installer.
COPY fetch_llama_prebuilt.py /usr/local/lib/unsloth/fetch_llama_prebuilt.py
# Optional public Cloudflare tunnel for JupyterLab (UNSLOTH_JUPYTER_CLOUDFLARE=1,
# or `unsloth-jupyter-tunnel --force`); supervisord runs it as jupyter-cloudflare.
COPY unsloth_jupyter_tunnel.sh /usr/local/bin/unsloth-jupyter-tunnel
RUN chmod +x /usr/local/bin/unsloth-studio-launch \
/usr/local/bin/unsloth-studio-update \
/usr/local/bin/unsloth-llama-update
/usr/local/bin/unsloth-llama-update \
/usr/local/bin/unsloth-jupyter-tunnel
# Studio web UI, JupyterLab, sshd. All bind 0.0.0.0 inside the container's
# network namespace; the operator publishes them explicitly with -p.

View file

@ -17,6 +17,9 @@ set -euo pipefail
export JUPYTER_PORT="${JUPYTER_PORT:-8888}"
export UNSLOTH_STUDIO_HOME="${UNSLOTH_STUDIO_HOME:-/opt/unsloth-studio}"
# Default off so supervisord's %(ENV_UNSLOTH_JUPYTER_CLOUDFLARE)s autostart gate
# resolves; set to 1 (docker run -e) to expose JupyterLab on a trycloudflare URL.
export UNSLOTH_JUPYTER_CLOUDFLARE="${UNSLOTH_JUPYTER_CLOUDFLARE:-0}"
# Make the runtime env visible to SSH sessions, which get a fresh login shell
# without the `docker run -e` vars. Secrets are excluded on purpose: tokens,
@ -79,6 +82,11 @@ fi
mkdir -p /workspace
echo "Unsloth Studio -> http://localhost:8000 (first-boot password below)"
echo "JupyterLab -> http://localhost:${JUPYTER_PORT} (${JUPYTER_NOTE})"
if [[ "${UNSLOTH_JUPYTER_CLOUDFLARE}" == "1" ]]; then
echo "JupyterLab tunnel-> enabled; public trycloudflare URL appears below once it is up"
else
echo "JupyterLab tunnel-> off (set UNSLOTH_JUPYTER_CLOUDFLARE=1 for a public link)"
fi
if [[ "${UNSLOTH_ENABLE_SSHD}" == "true" ]]; then
echo "sshd -> port 22 (key-only)"
fi

View file

@ -53,6 +53,21 @@ stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
; Optional public Cloudflare quick-tunnel for JupyterLab. Started only when
; UNSLOTH_JUPYTER_CLOUDFLARE=1 (studio_launch.sh exports a 0 default so this
; expands). The trycloudflare URL is printed to docker logs by cloudflared.
[program:jupyter-cloudflare]
command=/usr/local/bin/unsloth-jupyter-tunnel
directory=/workspace
autostart=%(ENV_UNSLOTH_JUPYTER_CLOUDFLARE)s
autorestart=true
startsecs=5
environment=HOME="/root",USER="root"
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:sshd]
command=/usr/sbin/sshd -D -e
autostart=%(ENV_UNSLOTH_ENABLE_SSHD)s

View file

@ -0,0 +1,60 @@
#!/usr/bin/env bash
# Optional public Cloudflare quick-tunnel for JupyterLab, mirroring the tunnel
# Studio creates for its own UI. Off by default. Two ways to use it:
#
# * at run time: docker run -e UNSLOTH_JUPYTER_CLOUDFLARE=1 ... unsloth/unsloth
# -> the https://<name>.trycloudflare.com URL is printed in
# `docker logs` once JupyterLab is up.
# * on demand: docker exec <container> unsloth-jupyter-tunnel --force
#
# The tunnel gives a public https URL that works from anywhere with no account
# or open inbound port. JupyterLab still requires its password, so the notebook
# is not open to the world; treat the URL as sensitive all the same.
set -u
FORCE=0
[ "${1:-}" = "--force" ] && FORCE=1
if [ "$FORCE" != "1" ] && [ "${UNSLOTH_JUPYTER_CLOUDFLARE:-0}" != "1" ]; then
echo "[jupyter-tunnel] disabled (set UNSLOTH_JUPYTER_CLOUDFLARE=1, or run with --force)"
exit 0
fi
PORT="${JUPYTER_PORT:-8888}"
echo "[jupyter-tunnel] waiting for JupyterLab on port ${PORT} ..."
for _ in $(seq 1 90); do
if curl -fsS -o /dev/null "http://localhost:${PORT}/login" 2>/dev/null; then
break
fi
sleep 2
done
# Reuse a cloudflared already on the host (Studio caches one for its own
# tunnel); otherwise fetch the static binary for this arch. No account needed.
CFD=""
for cand in \
"${UNSLOTH_STUDIO_HOME:-/opt/unsloth-studio}/bin/cloudflared" \
/usr/local/bin/cloudflared \
cloudflared; do
if command -v "$cand" >/dev/null 2>&1; then CFD="$(command -v "$cand")"; break; fi
[ -x "$cand" ] && { CFD="$cand"; break; }
done
if [ -z "$CFD" ]; then
case "$(uname -m)" in
x86_64|amd64) A=amd64;;
aarch64|arm64) A=arm64;;
*) A=amd64;;
esac
CFD=/usr/local/bin/cloudflared
echo "[jupyter-tunnel] downloading cloudflared (${A}) ..."
if ! curl -fsSL -o "$CFD" \
"https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${A}"; then
echo "[jupyter-tunnel] could not download cloudflared" >&2
exit 1
fi
chmod +x "$CFD"
fi
echo "[jupyter-tunnel] starting Cloudflare quick-tunnel to JupyterLab (port ${PORT})."
echo "[jupyter-tunnel] the https://<name>.trycloudflare.com URL appears below; log in with your Jupyter password."
exec "$CFD" tunnel --no-autoupdate --url "http://localhost:${PORT}"