JupyterLab
JupyterLab can run inside an Apptainer container on a Pegasus LSF compute node. The session is accessed from a local browser through an SSH tunnel.
Use this workflow when a required Python or software environment is not available through the managed Pegasus JupyterHub service.
Note
This workflow starts a single-user JupyterLab server. It does not start a JupyterHub service.
Warning
Do not run JupyterLab directly on a Pegasus login node. The launcher submits JupyterLab to an LSF compute node.
The tested workflow uses:
Apptainer 1.0.2
The Pegasus
generalLSF queueThe official Jupyter
minimal-notebookcontainer imageToken-based browser authentication
SSH port forwarding through Pegasus
One-Time Setup
Create the installation directories:
mkdir -p "$HOME/.local/lib/pegasus-jupyter"
mkdir -p "$HOME/.local/share/pegasus-jupyter"
mkdir -p "$HOME/bin"
Pull the Jupyter Image
Load Apptainer:
module purge
module load apptainer/1.0.2
Pull the Jupyter minimal notebook image:
apptainer pull "$HOME/.local/share/pegasus-jupyter/jupyter-minimal.sif" docker://quay.io/jupyter/minimal-notebook
Confirm that JupyterLab is available:
apptainer exec "$HOME/.local/share/pegasus-jupyter/jupyter-minimal.sif" jupyter lab --version
Note
The image is downloaded only once. Future sessions reuse the local
jupyter-minimal.sif image.
Create the Launcher Script
Run the following command on Pegasus:
cat > "$HOME/.local/lib/pegasus-jupyter/pegasus-jupyter" <<'PEGASUS_JUPYTER'
#!/usr/bin/env bash
set -Eeuo pipefail
SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
JOB_SCRIPT="$SCRIPT_DIR/jupyter-server-job.sh"
BASE_DIR="$HOME/.cache/pegasus-jupyter"
LSF_LOG_DIR="$BASE_DIR/lsf"
mkdir -p "$LSF_LOG_DIR"
usage() {
cat <<'USAGE'
Usage:
pegasus-jupyter start [options]
pegasus-jupyter status [job-id]
pegasus-jupyter info <job-id>
pegasus-jupyter logs <job-id>
pegasus-jupyter stop <job-id>
Start options:
--project PROJECT Default: hpc
--queue QUEUE Default: general
--time HH:MM Default: 04:00
--cpus NUMBER Default: 2
--memory MB Default: 4000 per CPU core
USAGE
}
show_info() {
local job_id="$1"
local file="$BASE_DIR/$job_id/connection.env"
if [[ ! -r "$file" ]]; then
echo "Connection information is not available for job $job_id." >&2
echo "Run: pegasus-jupyter status $job_id" >&2
return 1
fi
source "$file"
cat <<INFO
JupyterLab is running.
Job ID: $JOB_ID
Compute node: $COMPUTE_NODE
Server port: $PORT
On your computer, run:
ssh -N -L 7777:${COMPUTE_NODE}:${PORT} \\
${USERNAME}@${LOGIN_NODE}
Then open:
http://localhost:7777/lab?token=${TOKEN}
Alternative through Acorn:
ssh -N -L 7777:${COMPUTE_NODE}:${PORT} \\
-J ${USERNAME}@acorn-gw.idsc.miami.edu \\
${USERNAME}@${LOGIN_NODE}
Stop the session with:
pegasus-jupyter stop $JOB_ID
Do not share the token.
INFO
}
command_name="${1:-help}"
shift || true
case "$command_name" in
start)
project="hpc"
queue="general"
walltime="04:00"
cpus="2"
memory="4000"
while [[ $# -gt 0 ]]; do
case "$1" in
--project) project="${2:?Missing project}"; shift 2 ;;
--queue) queue="${2:?Missing queue}"; shift 2 ;;
--time) walltime="${2:?Missing runtime}"; shift 2 ;;
--cpus) cpus="${2:?Missing CPU count}"; shift 2 ;;
--memory) memory="${2:?Missing memory value}"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown option: $1" >&2; usage >&2; exit 1 ;;
esac
done
[[ "$cpus" =~ ^[1-9][0-9]*$ ]] ||
{ echo "--cpus must be a positive integer." >&2; exit 1; }
[[ "$memory" =~ ^[1-9][0-9]*$ ]] ||
{ echo "--memory must be a positive integer." >&2; exit 1; }
[[ -x "$JOB_SCRIPT" ]] ||
{ echo "Missing executable job script: $JOB_SCRIPT" >&2; exit 1; }
echo "Submitting JupyterLab..."
echo "Project: $project"
echo "Queue: $queue"
echo "Runtime: $walltime"
echo "CPUs: $cpus"
echo "Memory per core: ${memory} MB"
if ! output="$(
bsub \
-J "jupyter-$USER" \
-P "$project" \
-q "$queue" \
-W "$walltime" \
-n "$cpus" \
-R "span[hosts=1]" \
-R "rusage[mem=$memory]" \
-o "$LSF_LOG_DIR/%J.out" \
-e "$LSF_LOG_DIR/%J.err" \
"$JOB_SCRIPT" 2>&1
)"; then
echo "$output" >&2
exit 1
fi
echo "$output"
job_id="$(
sed -n 's/.*Job <\([0-9][0-9]*\)>.*/\1/p' <<< "$output" |
head -1
)"
[[ -n "$job_id" ]] ||
{ echo "Could not determine the LSF job ID." >&2; exit 1; }
interrupt() {
echo
echo "Stopped waiting. LSF job $job_id was not cancelled."
echo "Run: pegasus-jupyter status $job_id"
exit 130
}
trap interrupt INT
echo
echo "Job ID: $job_id"
echo "Waiting for JupyterLab..."
for ((attempt = 1; attempt <= 120; attempt++)); do
if [[ -r "$BASE_DIR/$job_id/connection.env" ]]; then
trap - INT
show_info "$job_id"
exit 0
fi
status="$(
bjobs "$job_id" 2>/dev/null |
awk 'NR == 2 {print $3}' || true
)"
case "$status" in
EXIT|DONE)
trap - INT
echo "The JupyterLab job ended during startup." >&2
"$0" logs "$job_id" >&2 || true
exit 1
;;
esac
sleep 3
done
trap - INT
echo "JupyterLab is not ready yet."
echo "Run: pegasus-jupyter status $job_id"
echo "Then: pegasus-jupyter info $job_id"
;;
status)
if [[ $# -gt 0 ]]; then
bjobs "$1" || true
else
bjobs -J "jupyter-$USER" || true
fi
;;
info)
show_info "${1:?Provide a job ID}"
;;
logs)
job_id="${1:?Provide a job ID}"
for file in \
"$LSF_LOG_DIR/$job_id.out" \
"$LSF_LOG_DIR/$job_id.err" \
"$BASE_DIR/$job_id/logs/jupyter.log"
do
echo "=== $file ==="
if [[ -r "$file" ]]; then
tail -n 150 "$file"
else
echo "Not found."
fi
echo
done
;;
stop)
bkill "${1:?Provide a job ID}"
;;
help|-h|--help)
usage
;;
*)
echo "Unknown command: $command_name" >&2
usage >&2
exit 1
;;
esac
PEGASUS_JUPYTER
Create the Internal Job Script
Run the following command on Pegasus:
cat > "$HOME/.local/lib/pegasus-jupyter/jupyter-server-job.sh" <<'JUPYTER_JOB'
#!/usr/bin/env bash
set -Eeuo pipefail
if ! command -v module >/dev/null 2>&1; then
source /etc/profile.d/modules.sh 2>/dev/null || true
fi
module load apptainer/1.0.2
IMAGE="$HOME/.local/share/pegasus-jupyter/jupyter-minimal.sif"
LOGIN_NODE="pegasus2.idsc.miami.edu"
JOB_ID="${LSB_JOBID:?This script must run as an LSF job}"
CPUS="${LSB_DJOB_NUMPROC:-1}"
COMPUTE_NODE="$(hostname -s)"
PYTHON_BIN="$(command -v python3 || command -v python || true)"
[[ -n "$PYTHON_BIN" ]] ||
{ echo "ERROR: Python is required to select the port." >&2; exit 1; }
[[ -r "$IMAGE" ]] ||
{ echo "ERROR: Jupyter image is not readable: $IMAGE" >&2; exit 1; }
PORT="$(
"$PYTHON_BIN" - <<'PY'
import socket
sock = socket.socket()
sock.bind(("", 0))
print(sock.getsockname()[1])
sock.close()
PY
)"
TOKEN=""
SESSION_DIR="$HOME/.cache/pegasus-jupyter/$JOB_ID"
RUNTIME_DIR="$SESSION_DIR/runtime"
CONFIG_DIR="$SESSION_DIR/config"
LOG_DIR="$SESSION_DIR/logs"
CONNECTION_FILE="$SESSION_DIR/connection.env"
mkdir -p "$RUNTIME_DIR" "$CONFIG_DIR" "$LOG_DIR"
chmod 700 "$SESSION_DIR" "$RUNTIME_DIR" "$CONFIG_DIR" "$LOG_DIR"
export APPTAINERENV_JUPYTER_RUNTIME_DIR="$RUNTIME_DIR"
export APPTAINERENV_JUPYTER_CONFIG_DIR="$CONFIG_DIR"
export APPTAINERENV_OMP_NUM_THREADS="$CPUS"
BIND_ARGS=()
for path in /projectnb /scratch /sccc; do
[[ -d "$path" ]] && BIND_ARGS+=(--bind "$path")
done
JUPYTER_PID=""
cleanup() {
rm -f "$CONNECTION_FILE"
if [[ -n "$JUPYTER_PID" ]] &&
kill -0 "$JUPYTER_PID" 2>/dev/null; then
kill "$JUPYTER_PID" 2>/dev/null || true
fi
}
trap cleanup EXIT INT TERM HUP
cd "$HOME"
apptainer exec \
"${BIND_ARGS[@]}" \
"$IMAGE" \
jupyter lab \
--no-browser \
--ip=0.0.0.0 \
--port="$PORT" \
--ServerApp.use_redirect_file=False \
> "$LOG_DIR/jupyter.log" 2>&1 &
JUPYTER_PID=$!
READY=0
for ((attempt = 1; attempt <= 60; attempt++)); do
if ! kill -0 "$JUPYTER_PID" 2>/dev/null; then
break
fi
if [[ -z "$TOKEN" ]]; then
TOKEN="$(
grep -oE 'token=[A-Za-z0-9_-]+' "$LOG_DIR/jupyter.log" 2>/dev/null |
head -1 |
cut -d= -f2 || true
)"
fi
if [[ -n "$TOKEN" ]] &&
"$PYTHON_BIN" - "$PORT" <<'PY'
import socket
import sys
try:
socket.create_connection(
("127.0.0.1", int(sys.argv[1])),
timeout=1
).close()
except OSError:
raise SystemExit(1)
PY
then
READY=1
break
fi
sleep 1
done
if [[ "$READY" -ne 1 ]]; then
echo "ERROR: JupyterLab did not start." >&2
cat "$LOG_DIR/jupyter.log" >&2
exit 1
fi
cat > "$SESSION_DIR/connection.env.tmp" <<CONNECTION
JOB_ID=$JOB_ID
COMPUTE_NODE=$COMPUTE_NODE
PORT=$PORT
USERNAME=$USER
TOKEN=$TOKEN
LOGIN_NODE=$LOGIN_NODE
CONNECTION
chmod 600 "$SESSION_DIR/connection.env.tmp"
mv "$SESSION_DIR/connection.env.tmp" "$CONNECTION_FILE"
wait "$JUPYTER_PID"
JUPYTER_JOB
Make the Scripts Executable
Make both scripts executable:
chmod 700 "$HOME/.local/lib/pegasus-jupyter/pegasus-jupyter"
chmod 700 "$HOME/.local/lib/pegasus-jupyter/jupyter-server-job.sh"
Validate the scripts:
bash -n "$HOME/.local/lib/pegasus-jupyter/pegasus-jupyter"
bash -n "$HOME/.local/lib/pegasus-jupyter/jupyter-server-job.sh"
The validation commands should return no output.
Create the Launcher Command
Create a symbolic link in $HOME/bin:
ln -sf "$HOME/.local/lib/pegasus-jupyter/pegasus-jupyter" "$HOME/bin/pegasus-jupyter"
Add $HOME/bin to the shell path:
grep -qxF 'export PATH="$HOME/bin:$PATH"' ~/.bashrc ||
echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
Confirm that the launcher is available:
command -v pegasus-jupyter
The command should return a path similar to:
/nethome/<username>/bin/pegasus-jupyter
Note
The setup is required only once. Future sessions can be started directly
with the pegasus-jupyter command.
Start JupyterLab
Start a JupyterLab session by specifying an LSF project available to the Pegasus account:
pegasus-jupyter start --project <project>
Replace <project> with the appropriate LSF project.
The default resource request is:
Resource |
Default |
|---|---|
Queue |
|
Runtime |
4 hours |
CPU cores |
2 |
Memory |
4,000 MB per CPU core |
Local browser port |
|
The launcher submits the LSF job and waits for JupyterLab to start.
When the session is ready, the launcher displays:
the LSF job ID
the allocated compute node
the JupyterLab server port
the SSH tunnel command
the local browser URL
the temporary access token
the command used to stop the session
Example output:
JupyterLab is running.
Job ID: 440100
Compute node: n195
Server port: 42137
On your computer, run:
ssh -N -L 7777:n195:42137 <username>@pegasus2.idsc.miami.edu
Then open:
http://localhost:7777/lab?token=<token>
Warning
Do not share the Jupyter token. The token provides access to the running JupyterLab session.
Request Different Resources
Optional arguments can change the queue, runtime, CPU count, or memory request:
pegasus-jupyter start --project <project> --queue general --time 08:00 --cpus 4 --memory 6000
The available options are:
Option |
Description |
|---|---|
|
LSF project used for the job |
|
LSF queue used for the job |
|
Maximum runtime in |
|
Number of CPU cores |
|
Memory requested per CPU core, in megabytes |
Caution
Request only the resources required for the session. Larger requests may remain pending longer while waiting for available resources.
Create the SSH Tunnel
Run the SSH command printed by the launcher from a terminal on the local computer.
Do not run the tunnel command from inside the existing Pegasus session.
Direct Pegasus Connection
Use the following pattern:
ssh -N -L 7777:<compute-node>:<server-port> <username>@pegasus2.idsc.miami.edu
Keep the SSH terminal open while using JupyterLab.
Connection Through Acorn
If direct access to Pegasus is unavailable, use the Acorn jump host:
ssh -N -L 7777:<compute-node>:<server-port> -J <username>@acorn-gw.idsc.miami.edu <username>@pegasus2.idsc.miami.edu
Note
Use the exact compute node and server port printed for the current job. These values change between sessions.
Open JupyterLab
Open the token URL printed by the launcher:
http://localhost:7777/lab?token=<token>
Caution
Do not open the compute-node address or server port directly in a browser.
Access JupyterLab only through the SSH tunnel and the local
localhost address.
Verify the Session
Create a Python notebook and run:
import os
import socket
import sys
print(sys.version)
print(socket.gethostname())
print(os.getcwd())
The hostname should match the compute node printed by the launcher.
Test Persistent File Access
Run:
from pathlib import Path
test_file = Path.home() / "jupyter-container-test.txt"
test_file.write_text("Created from JupyterLab\n")
print(test_file.read_text())
Files created under $HOME remain available after the LSF job ends.
Test Plotting
Run:
import matplotlib.pyplot as plt
plt.plot([1, 2, 3], [1, 4, 9])
plt.title("Pegasus JupyterLab Test")
plt.show()
The plot should appear below the notebook cell.
Test the JupyterLab Terminal
Open a terminal from the JupyterLab launcher and run:
hostname
pwd
whoami
python --version
The hostname should match the allocated compute node.
Access Project Storage
Project storage is available inside the container when the corresponding host directory exists.
For example:
ls /projectnb
Use only the project or scratch path assigned to the account.
Manage JupyterLab Sessions
Check Session Status
List the current user’s JupyterLab jobs:
pegasus-jupyter status
Check a specific job:
pegasus-jupyter status <job-id>
A job may initially appear with the PEND status while waiting for
resources.
Recover Connection Information
Display the connection instructions for a running session:
pegasus-jupyter info <job-id>
View Logs
Display the LSF and JupyterLab logs:
pegasus-jupyter logs <job-id>
Stop the Session
First, use File > Shut Down in JupyterLab when available.
Then terminate the LSF job from Pegasus:
pegasus-jupyter stop <job-id>
Confirm the final LSF job state:
bjobs <job-id>
A manually stopped session may appear with the EXIT status.
Warning
Closing the browser or SSH tunnel does not terminate the LSF job. Always stop the JupyterLab job when the session is no longer needed.
Troubleshooting
Command Not Found
If the shell reports:
pegasus-jupyter: command not found
confirm that the symbolic link exists:
ls -l "$HOME/bin/pegasus-jupyter"
Reload the shell configuration:
source ~/.bashrc
Local Port 7777 Is Already in Use
If another local application is using port 7777, change only the first
port number in the SSH command:
ssh -N -L 7778:<compute-node>:<server-port> <username>@pegasus2.idsc.miami.edu
Then open:
http://localhost:7778/lab?token=<token>
Do not change the compute-node server port printed by the launcher.
Multiple JupyterLab Jobs Are Running
List all active JupyterLab jobs:
bjobs -J "jupyter-$USER"
Stop each old or unwanted job:
pegasus-jupyter stop <job-id>
Confirm that no unwanted jobs remain:
bjobs -J "jupyter-$USER"
Caution
Use the compute node, server port, and token generated for the same job. Credentials from one job do not apply to another JupyterLab session.
Launcher Interrupted
Pressing Ctrl+C while the launcher is waiting does not cancel the
submitted LSF job.
Check the job:
pegasus-jupyter status <job-id>
Recover the connection information:
pegasus-jupyter info <job-id>
Job Remains Pending
Check the job status:
pegasus-jupyter status <job-id>
For detailed LSF scheduling information, run:
bjobs -l <job-id>
JupyterLab Does Not Start
Display the session logs:
pegasus-jupyter logs <job-id>
Stop the failed job if it remains active:
pegasus-jupyter stop <job-id>