#!/bin/bash # JupyterLab entrypoint. # Runs WITHOUT a token/password — the aiohttp proxy in front of it handles # the password gate. Listens only on localhost:8888 so it is not directly # reachable. HF exposes the container (via the proxy) on port 7860. # ── Persistent-storage bootstrap ───────────────────────────────────── # On HF Spaces, /data is the ONLY directory guaranteed to survive a # sleep/restart/rebuild — and only if "Persistent Storage" is actually # enabled in Settings → Persistent storage. Everything else under / is # rebuilt fresh from the image every time the container starts. # # To make sure real work survives, we redirect the ephemeral spots that # matter — pip's cache + `pip install --user`, npm/bun's cache, Hugging # Face's model/dataset cache, and JupyterLab's own settings/workspaces — # onto /data. This runs on every boot; it's a no-op once already wired, # and intentionally non-fatal (no `set -e`) so a hiccup here never # prevents JupyterLab from starting. mkdir -p /data/notebooks /data/projects \ /data/.npm-global /data/node_modules \ /data/.cache /data/.local /data/.npm-cache /data/.bun-cache \ /data/.huggingface \ /data/.jupyter-data/user-settings /data/.jupyter-data/workspaces \ 2>/dev/null link_to_data() { # $1 = ephemeral path (e.g. /root/.cache), $2 = persistent path under /data local target="$1" persist="$2" if [ -L "$target" ]; then return 0 # already wired up from a previous boot of this container fi if [ -e "$target" ]; then # First boot only: keep whatever the image shipped with, then swap # in the symlink so all future writes land on /data. cp -an "$target"/. "$persist"/ 2>/dev/null rm -rf "$target" 2>/dev/null fi ln -s "$persist" "$target" 2>/dev/null } link_to_data /root/.cache "/data/.cache" # pip download/build cache, etc. link_to_data /root/.local "/data/.local" # `pip install --user` packages link_to_data /root/.npm "/data/.npm-cache" link_to_data /root/.bun "/data/.bun-cache" # Set env vars here instead of supervisord environment= directive. # supervisord's environment= REPLACES the entire env (dropping HF secrets). export HOME="/root" export JUPYTER_CONFIG_DIR="/home/user/.jupyter" export SHELL="/bin/bash" # Plain `pip install ` (no --user flag needed, even as root) now # lands under /root/.local, which is symlinked to /data/.local above. export PIP_USER=1 # transformers / diffusers / datasets / huggingface_hub all cache # downloaded models here — avoids re-downloading after every restart. export HF_HOME="/data/.huggingface" # JupyterLab's *data* — kernelspecs, settings, saved workspaces, history — # goes to /data so customizations survive. (The static config file used # below, jupyter_lab_config.py, ships in the image and doesn't need to.) export JUPYTER_DATA_DIR="/data/.jupyter-data" export JUPYTERLAB_SETTINGS_DIR="/data/.jupyter-data/user-settings" export JUPYTERLAB_WORKSPACES_DIR="/data/.jupyter-data/workspaces" # root_dir is set to "/" in jupyter_lab_config.py so the file browser can # navigate the whole filesystem. cd to /data so new terminals open in the # persistent folder by default (avoids the cwd-mismatch confusion, and # avoids people accidentally saving work somewhere that gets wiped on the # next restart). cd /data || exit 1 # Silence the "new version available" popup in JupyterLab jupyter labextension disable "@jupyterlab/apputils-extension:announcements" exec jupyter-lab \ --config=/home/user/.jupyter/jupyter_lab_config.py \ --allow-root