-
-
Notifications
You must be signed in to change notification settings - Fork 6.1k
docker: Colab-grade JupyterLab and Studio UX for the Unsloth image #6681
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: docker-blackwell-build
Are you sure you want to change the base?
Changes from 2 commits
c9ff52b
7b5bb24
e496095
50f7170
b963403
dde7a26
2eba8a4
30e7e09
9a53256
a56a7a8
2e4699c
38f1115
6fd9744
3fb8804
d1a5b58
9cd1951
2482719
0a8ae80
2c190aa
04452eb
67c0a8e
5d41a03
38df387
b558bc7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -285,18 +285,30 @@ RUN set -eux \ | |
| # omegaconf TTS families + both NeMo-Gym RL notebooks' config objects | ||
| # einx TTS codec tensor-rearrange (Llasa / Oute / Spark TTS) | ||
| # librosa Whisper audio feature extraction (pairs with soundfile + torchcodec) | ||
| # decord ERNIE-VL vision notebook video decode | ||
| # ftfy Oute TTS text normalisation | ||
| # decord (ERNIE-VL video decode) is installed separately below: it ships no | ||
| # aarch64 wheel, so a hard install here would break the arm64 build. | ||
| # librosa pulls numba/soxr/audioread; numba is already pinned >=0.65 (numpy 2.4 | ||
| # compatible) by the vLLM pass, so the resolve must NOT move torch/numpy/numba -- | ||
| # the assertion below fails the build loudly if it did. | ||
| # Pinned (==) to the resolved, tested versions for reproducible rebuilds -- the | ||
| # same convention as the cu128 core (torch/torchvision/torchaudio). Bump these | ||
| # deliberately, not silently on the next build. Transitive deps of these are | ||
| # captured by the full venv lockfile (docker/freeze.sh -> requirements.lock.txt). | ||
| RUN ${VENV}/bin/uv pip install \ | ||
| --python ${VENV}/bin/python \ | ||
| jupyterlab notebook ipywidgets matplotlib \ | ||
| soundfile evaluate jiwer tensorboard langid easydict protobuf \ | ||
| omegaconf einx librosa decord ftfy \ | ||
| "jupyterlab==4.6.0" "notebook==7.6.0" "ipywidgets==8.1.8" "matplotlib==3.11.0" \ | ||
| "soundfile==0.14.0" "evaluate==0.4.6" "jiwer==4.0.0" "tensorboard==2.20.0" \ | ||
| "langid==1.1.6" "easydict==1.13" "protobuf==6.33.6" \ | ||
| "omegaconf==2.3.1" "einx==0.4.3" "librosa==0.11.0" "ftfy==6.3.1" \ | ||
| && ${VENV}/bin/python -c "import torch, numpy, numba; from packaging.version import Version; assert torch.__version__.startswith('2.10.0'), torch.__version__; assert Version(numpy.__version__) >= Version('2.3'), numpy.__version__; assert Version(numba.__version__) >= Version('0.65'), numba.__version__; print('notebook-deps pins OK:', torch.__version__, numpy.__version__, numba.__version__)" | ||
|
|
||
| # decord (ERNIE-VL video decode) publishes wheels only for x86_64 / win_amd64, | ||
| # so install it on its own and fail-soft: amd64 gets it; on arm64 the ERNIE-VL | ||
| # video path is skipped rather than breaking the whole image build. | ||
| RUN ${VENV}/bin/uv pip install --python ${VENV}/bin/python "decord==0.6.0" \ | ||
| || echo ">> decord skipped (no matching wheel for ${TARGETARCH:-amd64}); ERNIE-VL video decode unavailable" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This fallback is unconditional, so on amd64 a transient resolver failure or incompatible Useful? React with 👍 / 👎. |
||
|
|
||
| # Audio decode out of the box: the TTS/STT notebooks feed datasets' Audio | ||
| # features, which decode through torchcodec. Three traps, all defended: | ||
| # * version pairing: torchcodec 0.10 pairs with torch 2.10 (newer builds | ||
|
|
@@ -647,19 +659,22 @@ RUN mkdir -p ${HF_HOME} ${TRITON_CACHE_DIR} | |
| # * unsloth-run: headless `unsloth-run <notebook|url>` that auto-picks the | ||
| # sidecar and executes every cell -- the robust driven path. | ||
| # --------------------------------------------------------------------------- | ||
| COPY unsloth_nb_compat.py unsloth_pip_shim.py unsloth_ipython_startup.py unsloth_run.py unsloth_sync_notebooks.sh unsloth_nb_content_sig.py /opt/unsloth-nb/ | ||
| COPY unsloth_nb_compat.py unsloth_pip_shim.py unsloth_ipython_startup.py unsloth_run.py unsloth_sync_notebooks.sh unsloth_nb_content_sig.py unsloth_nb_view.py unsloth_nb_strip_colab.py unsloth_colab_compat.py /opt/unsloth-nb/ | ||
| RUN set -eux \ | ||
| && SP=/opt/unsloth-venv/lib/python${PYTHON_VERSION}/site-packages \ | ||
| && cp /opt/unsloth-nb/unsloth_nb_compat.py "$SP/unsloth_nb_compat.py" \ | ||
| && chmod +x /opt/unsloth-nb/unsloth_pip_shim.py /opt/unsloth-nb/unsloth_run.py /opt/unsloth-nb/unsloth_sync_notebooks.sh /opt/unsloth-nb/unsloth_nb_content_sig.py \ | ||
| && cp /opt/unsloth-nb/unsloth_colab_compat.py "$SP/unsloth_colab_compat.py" \ | ||
| && chmod +x /opt/unsloth-nb/unsloth_pip_shim.py /opt/unsloth-nb/unsloth_run.py /opt/unsloth-nb/unsloth_sync_notebooks.sh /opt/unsloth-nb/unsloth_nb_content_sig.py /opt/unsloth-nb/unsloth_nb_view.py /opt/unsloth-nb/unsloth_nb_strip_colab.py \ | ||
| && mkdir -p /opt/unsloth-nb/bin \ | ||
| && for t in pip pip3 uv; do ln -sf /opt/unsloth-nb/unsloth_pip_shim.py /opt/unsloth-nb/bin/$t; done \ | ||
| && ln -sf /opt/unsloth-nb/unsloth_run.py /usr/local/bin/unsloth-run \ | ||
| && ln -sf /opt/unsloth-nb/unsloth_sync_notebooks.sh /usr/local/bin/unsloth-sync-notebooks \ | ||
| && ln -sf /opt/unsloth-nb/unsloth_nb_content_sig.py /usr/local/bin/unsloth-nb-content-sig \ | ||
| && ln -sf /opt/unsloth-nb/unsloth_nb_view.py /usr/local/bin/unsloth-nb-view \ | ||
| && ln -sf /opt/unsloth-nb/unsloth_nb_strip_colab.py /usr/local/bin/unsloth-nb-strip-colab \ | ||
| && mkdir -p /root/.ipython/profile_default/startup \ | ||
| && cp /opt/unsloth-nb/unsloth_ipython_startup.py /root/.ipython/profile_default/startup/00-unsloth-nb.py \ | ||
| && /opt/unsloth-venv/bin/python -c "import sys, glob; sys.path.insert(0, '$SP'); import unsloth_nb_compat; print('nb-compat OK; baked sidecars:', sorted(glob.glob('/opt/unsloth-venv/tf-sidecars/t_*')))" | ||
| && /opt/unsloth-venv/bin/python -c "import sys, glob; sys.path.insert(0, '$SP'); import unsloth_nb_compat, unsloth_colab_compat; print('nb-compat OK; baked sidecars:', sorted(glob.glob('/opt/unsloth-venv/tf-sidecars/t_*')))" | ||
| # Shim dir AHEAD of the venv bin so `!pip`/`!uv` resolve to the shim, not the real tool. | ||
| ENV PATH=/opt/unsloth-nb/bin:${PATH} | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,9 @@ | ||
| # Full Unsloth image: base training stack + Studio + JupyterLab + sshd. | ||
| # | ||
| # This is the image published as docker.io/unsloth/unsloth:latest. It layers | ||
| # Unsloth Studio on top of the lean base image (Dockerfile, published under | ||
| # the `base` tags) and runs the same service trio as the previous production | ||
| # This is the image published as docker.io/unsloth/unsloth:studio (and the | ||
| # default :latest). It layers Unsloth Studio on top of the lean core image | ||
| # (Dockerfile, published under the `core` tags) and runs the same service trio | ||
| # as the previous production | ||
| # image: Studio on 8000, JupyterLab on 8888, key-only sshd on 22. | ||
| # | ||
| # Build (local): | ||
|
|
@@ -28,6 +29,22 @@ | |
| # images always ship the same stack. | ||
|
|
||
| ARG BASE_IMAGE=unsloth-blackwell:test | ||
|
|
||
| # --- builder stage: prebuild the Unsloth JupyterLab extension ----------------- | ||
| # Builds the named "Unsloth Dark" (Monokai) theme + the Colab-style Down/Up | ||
| # cell-navigation keymap. Node lives ONLY in this throwaway stage; the final | ||
| # image copies just the prebuilt static labextension, so the runtime stays | ||
| # Node-free. Uses the base image's bundled jlpm + jupyterlab (version-matched). | ||
| FROM ${BASE_IMAGE} AS labext-builder | ||
| ENV DEBIAN_FRONTEND=noninteractive | ||
| RUN apt-get update \ | ||
| && apt-get install -y --no-install-recommends nodejs npm git \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
|
Comment on lines
+43
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On the Ubuntu 24.04 CUDA base, Useful? React with 👍 / 👎. |
||
| COPY jupyter/unsloth_labext /opt/labext-src | ||
| RUN cd /opt/labext-src \ | ||
| && /opt/unsloth-venv/bin/jlpm install \ | ||
| && /opt/unsloth-venv/bin/jlpm build:prod | ||
|
|
||
| FROM ${BASE_IMAGE} | ||
|
|
||
| # Studio source ref to clone. Defaults to `main`, but a CI publish pipeline | ||
|
|
@@ -170,6 +187,43 @@ 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 | ||
| # JupyterLab defaults baked for every container: the named "Unsloth Dark" | ||
| # (Monokai) theme with adaptive light/dark by system preference, a per-cell run | ||
| # button that does NOT auto-advance, a labeled "Restart & Run All", windowing | ||
| # disabled so collapsing a long output does not snap to the cell top, | ||
| # ArrowDown/Up jumping to the TOP of the next/previous cell, and the official | ||
| # Jupyter "get notified about news" prompt suppressed (fetchNews/checkForUpdates | ||
| # off). overrides.json is the system-wide settings override (read from the base | ||
| # venv's share/jupyter/lab/settings); the theme + keymap + Unsloth top-bar logo | ||
| # ship as the prebuilt labextension built in the labext-builder stage above. | ||
| COPY jupyter/overrides.json /opt/unsloth-venv/share/jupyter/lab/settings/overrides.json | ||
| COPY --from=labext-builder /opt/labext-src/unsloth-jupyterlab/labextension /opt/unsloth-venv/share/jupyter/labextensions/unsloth-jupyterlab | ||
| # Unsloth branding (all served by jupyter_server, so applied to its site-packages | ||
| # the same way): replace the browser-tab favicon and the page logo with the | ||
| # Unsloth logo, and brand the login screen (dark Unsloth-themed login.html). | ||
| # Also disable + lock the stock top-left Jupyter logo plugin so the Unsloth logo | ||
| # widget shipped by the labextension is the only one rendered in the top bar | ||
| # (lock keeps users from re-enabling it in the UI). | ||
| COPY jupyter/favicon.ico /tmp/unsloth-branding/favicon.ico | ||
| COPY jupyter/logo.png /tmp/unsloth-branding/logo.png | ||
| COPY jupyter/login.html /tmp/unsloth-branding/login.html | ||
| COPY jupyter/install_sloth_stickers.py /tmp/unsloth-branding/install_sloth_stickers.py | ||
| RUN JS="$(/opt/unsloth-venv/bin/python -c 'import os, jupyter_server; print(os.path.dirname(jupyter_server.__file__))')" \ | ||
| && for n in favicon.ico favicon-notebook.ico favicon-file.ico favicon-terminal.ico; do \ | ||
| cp /tmp/unsloth-branding/favicon.ico "${JS}/static/favicons/${n}"; \ | ||
| done \ | ||
| && cp /tmp/unsloth-branding/logo.png "${JS}/static/logo/logo.png" \ | ||
| && cp /tmp/unsloth-branding/login.html "${JS}/templates/login.html" \ | ||
| # Copy the curated Studio sloth stickers the login page rotates through into | ||
| # jupyter_server's static dir (sloth/NN.png). Fail-soft: if the Studio | ||
| # public folder ever moves, login.html's onerror falls back to the logo. | ||
| && /opt/unsloth-venv/bin/python /tmp/unsloth-branding/install_sloth_stickers.py \ | ||
| --src "${UNSLOTH_STUDIO_HOME}/src/studio/frontend/public/Sloth emojis" \ | ||
| --dest "${JS}/static/sloth" \ | ||
| || echo ">> sloth stickers not installed (login falls back to the Unsloth logo)" \ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Because this Useful? React with 👍 / 👎. |
||
| && rm -rf /tmp/unsloth-branding \ | ||
| && /opt/unsloth-venv/bin/jupyter labextension disable @jupyterlab/application-extension:logo \ | ||
| && /opt/unsloth-venv/bin/jupyter labextension lock @jupyterlab/application-extension:logo | ||
| RUN chmod +x /usr/local/bin/unsloth-studio-launch \ | ||
| /usr/local/bin/unsloth-studio-update \ | ||
| /usr/local/bin/unsloth-llama-update \ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,7 +23,7 @@ | |
| # Studio chat / Jupyter / GGUF tooling still validate. | ||
| # | ||
| # Env overrides: IMAGE (default unsloth/unsloth:latest) | ||
| # BASE_IMAGE (default unsloth/unsloth:base) | ||
| # BASE_IMAGE (default unsloth/unsloth:core) | ||
| # GPUS=all|none|0|0,1 (default: auto-detect) | ||
| # PORT_STUDIO=18000 PORT_JUPYTER=18888 | ||
| # WORK=~/unsloth_docker_test (logs) | ||
|
|
@@ -33,7 +33,7 @@ | |
| set -uo pipefail | ||
|
|
||
| IMAGE="${IMAGE:-unsloth/unsloth:latest}" | ||
| BASE_IMAGE="${BASE_IMAGE:-unsloth/unsloth:base}" | ||
| BASE_IMAGE="${BASE_IMAGE:-unsloth/unsloth:core}" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This default now pulls Useful? React with 👍 / 👎. |
||
| GPUS="${GPUS:-auto}" | ||
| PORT_STUDIO="${PORT_STUDIO:-18000}" | ||
| PORT_JUPYTER="${PORT_JUPYTER:-18888}" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| #!/usr/bin/env python3 | ||
| """Install the Unsloth Studio sloth stickers for the JupyterLab login screen. | ||
|
|
||
| The branded login page (login.html) shows a different sloth sticker on each | ||
| visit, the same curated set Studio offers as profile avatars. The PNGs live in | ||
| the Studio frontend (`studio/frontend/public/Sloth emojis/`), which is present | ||
| in the studio image after install.sh runs. This copies the curated subset into | ||
| jupyter_server's static dir as `sloth/01.png .. sloth/20.png` so the template | ||
| can reference stable, space-free, auth-free URLs via `static_url(...)`. | ||
|
|
||
| Usage: | ||
| install_sloth_stickers.py --src "<Sloth emojis dir>" --dest "<static>/sloth" | ||
|
|
||
| Fail-soft: a missing source file is skipped (login.html's onerror falls back to | ||
| the Unsloth logo), and the script still exits 0 as long as at least one sticker | ||
| was installed. Stdlib only. | ||
| """ | ||
|
|
||
| import argparse | ||
| import os | ||
| import shutil | ||
| import sys | ||
|
|
||
| # Curated, in display order -> NN.png. Mirrors Studio's SLOTH_AVATARS | ||
| # (frontend/src/features/profile/sloth-avatars.ts): the square, low-whitespace | ||
| # stickers that frame cleanly. Kept in sync by hand; missing names are skipped. | ||
| CURATED = [ | ||
| "large sloth yay.png", | ||
| "large sloth heart.png", | ||
| "large sloth wave.png", | ||
| "large sloth thumbs.png", | ||
| "large sloth cheeky.png", | ||
| "large sloth glasses.png", | ||
| "large sloth fire.png", | ||
| "large sloth drink.png", | ||
| "large sloth sad.png", | ||
| "Large sloth Question mark.png", | ||
| "sloth shy large.png", | ||
| "sloth shock large.png", | ||
| "sloth sir large.png", | ||
| "sloth huglove large.png", | ||
| "sloth headphones.png", | ||
| "sloth pc square.png", | ||
| "sloth on phone.png", | ||
| "sloth magnify final.png", | ||
| "Sloth loca pc.png", | ||
| "UnSloth GPU Front square.png", | ||
| ] | ||
|
|
||
|
|
||
| def main() -> int: | ||
| parser = argparse.ArgumentParser(description = __doc__) | ||
| parser.add_argument("--src", required = True, help = "Studio 'Sloth emojis' dir") | ||
| parser.add_argument("--dest", required = True, help = "output dir (static/sloth)") | ||
| args = parser.parse_args() | ||
|
|
||
| os.makedirs(args.dest, exist_ok = True) | ||
| installed = 0 | ||
| for index, name in enumerate(CURATED, start = 1): | ||
| source = os.path.join(args.src, name) | ||
| target = os.path.join(args.dest, "%02d.png" % index) | ||
| if not os.path.isfile(source): | ||
| print(" skip (missing): %s" % name) | ||
| continue | ||
| try: | ||
| shutil.copyfile(source, target) | ||
| installed += 1 | ||
| except OSError as error: | ||
| print(" skip (%s): %s" % (error, name)) | ||
|
|
||
| print("installed %d/%d sloth stickers into %s" % (installed, len(CURATED), args.dest)) | ||
| # Non-fatal: the login page degrades to the logo if none were installed, but | ||
| # a totally empty copy usually means a wrong --src, so signal that. | ||
| return 0 if installed else 1 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| sys.exit(main()) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| {# Unsloth-branded JupyterLab login page. Overwrites jupyter_server's default | ||
| login.html (same overwrite pattern as the favicon/logo). Extends the stock | ||
| page.html so favicon (already the Unsloth icon) and form plumbing stay intact; | ||
| we override the title, hide the stock header, and render a dark centered card | ||
| matching the "Unsloth Dark" (Monokai) theme. The card logo reads | ||
| static/logo/logo.png, which the image build replaces with the Unsloth logo. #} | ||
| {% extends "page.html" %} | ||
|
|
||
| {% block title %}Unsloth{% endblock %} | ||
|
|
||
| {% block stylesheet %} | ||
| <style> | ||
| html, body { | ||
| background: hsl(70, 8%, 12%) !important; | ||
| color: hsl(60, 30%, 96%); | ||
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; | ||
| } | ||
| /* Hide the stock top header (jupyter_server's index.css uses a higher- | ||
| specificity selector, so force it); the centered card carries the brand. */ | ||
| #header, .header-bar { display: none !important; } | ||
| #site { display: flex; justify-content: center; align-items: flex-start; } | ||
| .unsloth-login-card { | ||
| margin-top: 11vh; | ||
| background: hsl(70, 8%, 18%); | ||
| border: 1px solid hsl(70, 8%, 28%); | ||
| border-radius: 12px; | ||
| padding: 38px 40px 32px; | ||
| width: 360px; | ||
| max-width: 90vw; | ||
| text-align: center; | ||
| box-shadow: 0 10px 34px rgba(0, 0, 0, 0.45); | ||
| } | ||
| .unsloth-login-card img.logo { height: 72px; width: auto; margin-bottom: 14px; } | ||
| /* A random Unsloth Studio sloth sticker, shown like the Studio login screen. */ | ||
| .unsloth-login-card img.sloth { | ||
| height: 104px; width: 104px; object-fit: contain; | ||
| margin: 2px auto 10px; display: block; | ||
| } | ||
| .unsloth-login-card h1 { font-size: 22px; margin: 0 0 4px; font-weight: 700; } | ||
| .unsloth-login-card p.sub { color: hsl(60, 8%, 64%); margin: 0 0 24px; font-size: 14px; } | ||
| .unsloth-login-card label { | ||
| display: block; text-align: left; font-size: 13px; | ||
| margin-bottom: 6px; color: hsl(60, 8%, 76%); | ||
| } | ||
| .unsloth-login-card input[type="password"] { | ||
| width: 100%; box-sizing: border-box; padding: 10px 12px; | ||
| border-radius: 8px; border: 1px solid hsl(70, 8%, 32%); | ||
| background: hsl(70, 8%, 13%); color: inherit; font-size: 14px; margin-bottom: 18px; | ||
| } | ||
| .unsloth-login-card input[type="password"]:focus { | ||
| outline: none; border-color: hsl(160, 55%, 48%); | ||
| } | ||
| .unsloth-login-card button { | ||
| width: 100%; padding: 10px 12px; border-radius: 8px; border: none; | ||
| background: hsl(160, 55%, 42%); color: #fff; font-weight: 600; font-size: 14px; cursor: pointer; | ||
| } | ||
| .unsloth-login-card button:hover { background: hsl(160, 55%, 36%); } | ||
| .unsloth-login-card .message { margin-top: 16px; font-size: 13px; } | ||
| .unsloth-login-card .message.error { color: hsl(0, 75%, 68%); } | ||
| </style> | ||
| {% endblock %} | ||
|
|
||
| {% block site %} | ||
| {# A different Unsloth Studio sloth sticker each visit (matches Studio's login). | ||
| The PNGs are copied into static/sloth/NN.png by the image build; if one is | ||
| missing the onerror handler falls back to the Unsloth logo so the page never | ||
| shows a broken image. #} | ||
| {% set sloths = [ | ||
| "01.png", "02.png", "03.png", "04.png", "05.png", "06.png", "07.png", | ||
| "08.png", "09.png", "10.png", "11.png", "12.png", "13.png", "14.png", | ||
| "15.png", "16.png", "17.png", "18.png", "19.png", "20.png" | ||
| ] %} | ||
| <div class="unsloth-login-card"> | ||
| <img class="sloth" src='{{ static_url("sloth/" ~ (sloths | random)) }}' | ||
| onerror="this.onerror=null;this.className='logo';this.src='{{ static_url('logo/logo.png') }}';" | ||
| alt='Unsloth' /> | ||
| <h1>Unsloth</h1> | ||
| <p class="sub">Sign in to JupyterLab</p> | ||
| {% if login_available %} | ||
| <form action="{{base_url}}login?next={{next}}" method="post"> | ||
| {{ xsrf_form_html() | safe }} | ||
| <label for="password_input"> | ||
| {% if token_available %}{% trans %}Password or token{% endtrans %}{% else %}{% trans %}Password{% endtrans %}{% endif %} | ||
| </label> | ||
| <input type="password" name="password" id="password_input" autofocus> | ||
| <button type="submit" id="login_submit">{% trans %}Log in{% endtrans %}</button> | ||
| </form> | ||
| {% endif %} | ||
| {% if message %} | ||
| {% for key in message %} | ||
| <div class="message {{key}}">{{ message[key] }}</div> | ||
| {% endfor %} | ||
| {% endif %} | ||
| </div> | ||
| {% endblock %} | ||
|
|
||
| {% block script %}{% endblock %} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When building
Dockerfile.studiowithdocker/as the context, this allowlist still excludesjupyter/install_sloth_stickers.pybecause the file is not whitelisted after the top-level**ignore. The newCOPY jupyter/install_sloth_stickers.py ...in the studio Dockerfile therefore fails with a missing source before the image can build; add an exception for that helper alongside the otherjupyter/assets.Useful? React with 👍 / 👎.