From 21fc3a2c1f73cd1a865cfb99c2abd4699ab833ae Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Wed, 15 Oct 2025 07:10:44 +0200 Subject: [PATCH 1/3] Traduction en anglais des fichiers --- Dockerfile | 15 ++- Dockerfile.hub | 10 +- Readme.md | 221 +++++++++++++++++++++---------------------- docker-compose.yaml | 15 +-- jupyterhub_config.py | 93 +++++++++++++++++- setup.sh | 97 ++++++++++++++----- 6 files changed, 296 insertions(+), 155 deletions(-) diff --git a/Dockerfile b/Dockerfile index 411013a..039bd7d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,30 +2,27 @@ FROM jupyter/base-notebook:latest USER root -# Installation de R et des dépendances système +# Install R and system dependencies RUN apt-get update && apt-get install -y \ r-base \ r-base-dev \ libcurl4-openssl-dev \ libssl-dev \ libxml2-dev \ - texlive-xetex \ - texlive-fonts-recommended \ - texlive-plain-generic \ && apt-get clean && rm -rf /var/lib/apt/lists/* -# Installation du kernel R pour Jupyter (en tant que root) +# Install R kernel for Jupyter (as root) RUN R -e "install.packages('IRkernel', repos='http://cran.rstudio.com/')" && \ R -e "IRkernel::installspec(user = FALSE)" -# Installation de quelques packages R utiles pour les TP -RUN R -e "install.packages(c('tidyverse','vegan','ade4'), repos='http://cran.rstudio.com/')" +# Install some useful R packages for labs +RUN R -e "install.packages(c('ggplot2', 'dplyr', 'tidyr', 'readr'), repos='http://cran.rstudio.com/')" -# Installation du kernel bash (en tant que root aussi) +# Install bash kernel (as root also) RUN pip install bash_kernel && \ python -m bash_kernel.install --sys-prefix -# Créer les répertoires nécessaires avec les bonnes permissions +# Create necessary directories with proper permissions RUN mkdir -p /home/${NB_USER}/.local/share/jupyter && \ chown -R ${NB_UID}:${NB_GID} /home/${NB_USER} diff --git a/Dockerfile.hub b/Dockerfile.hub index 245f9db..319fca6 100644 --- a/Dockerfile.hub +++ b/Dockerfile.hub @@ -1,16 +1,16 @@ FROM jupyterhub/jupyterhub:latest -# Installation de DockerSpawner +# Install DockerSpawner RUN pip install dockerspawner -# Copie de la configuration +# Copy configuration COPY jupyterhub_config.py /srv/jupyterhub/jupyterhub_config.py -# Port exposé +# Expose port EXPOSE 8000 -# Répertoire de travail +# Working directory WORKDIR /srv/jupyterhub -# Commande de démarrage +# Startup command CMD ["jupyterhub", "-f", "/srv/jupyterhub/jupyterhub_config.py"] \ No newline at end of file diff --git a/Readme.md b/Readme.md index 8336e50..0250b0f 100644 --- a/Readme.md +++ b/Readme.md @@ -1,258 +1,257 @@ -# Configuration JupyterHub avec OrbStack sur Mac (tout en Docker) +# JupyterHub Configuration with OrbStack on Mac (all in Docker) -## Prérequis -- OrbStack installé et démarré +## Prerequisites +- OrbStack installed and running -## Structure des fichiers +## File Structure -Votre dossier `~/jupyterhub-tp` doit contenir : +Your `~/jupyterhub-tp` directory should contain: ``` ~/jupyterhub-tp/ -├── Dockerfile # Image pour les étudiants (déjà créée) -├── Dockerfile.hub # Image pour JupyterHub (nouvelle) +├── Dockerfile # Image for students (already created) +├── Dockerfile.hub # Image for JupyterHub (new) ├── jupyterhub_config.py # Configuration -└── docker-compose.yml # Orchestration +├── docker-compose.yml # Orchestration +└── start-jupyterhub.sh # Startup script ``` -## Étapes d'installation +## Installation Steps -### 1. Créer la structure de dossiers +### 1. Create Directory Structure ```bash mkdir -p ~/jupyterhub-tp cd ~/jupyterhub-tp ``` -### 2. Créer tous les fichiers nécessaires +### 2. Create All Necessary Files -Créez les fichiers suivants avec le contenu des artifacts : -- `Dockerfile` (artifact "Dockerfile pour JupyterHub avec R et Bash") -- `Dockerfile.hub` (artifact "Dockerfile pour le container JupyterHub") -- `jupyterhub_config.py` (artifact "Configuration JupyterHub") +Create the following files with the content from artifacts: +- `Dockerfile` (artifact "Dockerfile for JupyterHub with R and Bash") +- `Dockerfile.hub` (artifact "Dockerfile for JupyterHub container") +- `jupyterhub_config.py` (artifact "JupyterHub Configuration") - `docker-compose.yml` (artifact "docker-compose.yml") +- `start-jupyterhub.sh` (artifact "start-jupyterhub.sh") -### 3. Construire les images Docker +### 3. Make Startup Script Executable ```bash -# Image pour les étudiants -docker build -t jupyterhub-student:latest -f Dockerfile . - -# Image pour le hub JupyterHub -docker build -t jupyterhub-hub:latest -f Dockerfile.hub . +chmod +x start-jupyterhub.sh ``` -### 4. Démarrer JupyterHub avec Docker Compose +### 4. Start JupyterHub ```bash -docker-compose up -d +./start-jupyterhub.sh ``` -### 5. Accéder à JupyterHub +### 5. Access JupyterHub -Ouvrez votre navigateur et allez à : **http://localhost:8000** +Open your browser and go to: **http://localhost:8000** -Vous pouvez vous connecter avec n'importe quel nom d'utilisateur. +You can log in with any username and password: `metabar2025` -## Commandes utiles +## Useful Commands -### Voir les logs de JupyterHub +### View JupyterHub logs ```bash docker-compose logs -f jupyterhub ``` -### Voir tous les containers (hub + étudiants) +### View all containers (hub + students) ```bash docker ps ``` -### Arrêter JupyterHub +### Stop JupyterHub ```bash docker-compose down ``` -### Redémarrer JupyterHub (après modification du config) +### Restart JupyterHub (after config modification) ```bash docker-compose restart jupyterhub ``` -### Reconstruire après modification du Dockerfile +### Rebuild after Dockerfile modification ```bash -# Pour l'image étudiants +# For student image docker build -t jupyterhub-student:latest -f Dockerfile . docker-compose restart jupyterhub -# Pour l'image hub +# For hub image docker-compose up -d --build ``` -### Voir les logs d'un étudiant spécifique +### View logs for a specific student ```bash -docker logs jupyter-nom_utilisateur +docker logs jupyter-username ``` -### Nettoyer après le TP +### Clean up after lab ```bash -# Arrêter et supprimer tous les containers +# Stop and remove all containers docker-compose down -# Supprimer les containers étudiants +# Remove student containers docker ps -a | grep jupyter- | awk '{print $1}' | xargs docker rm -f -# Supprimer les volumes (ATTENTION : supprime les données étudiants) +# Remove volumes (WARNING: deletes student data) docker volume ls | grep jupyterhub-user | awk '{print $2}' | xargs docker volume rm -# Tout nettoyer (containers + volumes + réseau) +# Clean everything (containers + volumes + network) docker-compose down -v docker ps -a | grep jupyter- | awk '{print $1}' | xargs docker rm -f docker volume prune -f ``` -## Gestion des données partagées +## Managing Shared Data -### Structure des dossiers pour chaque étudiant +### Directory Structure for Each Student -Chaque étudiant verra ces dossiers dans son JupyterLab : -- **`work/`** : Son espace personnel (persistant, privé) -- **`shared/`** : Espace partagé entre tous les étudiants (lecture/écriture) -- **`course/`** : Fichiers du cours (lecture seule, vous déposez les fichiers) +Each student will see these directories in their JupyterLab: +- **`work/`** : Personal workspace (persistent, private) +- **`shared/`** : Shared workspace between all students (read/write) +- **`course/`** : Course files (read-only, you deposit files) -### Déposer des fichiers pour le cours +### Deposit Files for Course -Pour mettre des fichiers dans le dossier `course/` (accessible en lecture seule) : +To put files in the `course/` directory (accessible read-only): ```bash -# Créer un dossier temporaire +# Create a temporary directory mkdir -p ~/jupyterhub-tp/course-files -# Copier vos fichiers dedans -cp mes_notebooks.ipynb ~/jupyterhub-tp/course-files/ -cp mes_donnees.csv ~/jupyterhub-tp/course-files/ +# Copy your files into it +cp my_notebooks.ipynb ~/jupyterhub-tp/course-files/ +cp my_data.csv ~/jupyterhub-tp/course-files/ -# Copier dans le volume Docker +# Copy into Docker volume docker run --rm \ -v jupyterhub-course:/target \ -v ~/jupyterhub-tp/course-files:/source \ alpine sh -c "cp -r /source/* /target/" ``` -### Accéder aux fichiers partagés entre étudiants +### Access Shared Files Between Students -Les étudiants peuvent collaborer via le dossier `shared/` : +Students can collaborate via the `shared/` directory: ```python -# Dans un notebook, pour lire un fichier partagé +# In a notebook, to read a shared file import pandas as pd -df = pd.read_csv('/home/jovyan/shared/donnees_groupe.csv') +df = pd.read_csv('/home/jovyan/shared/group_data.csv') -# Pour écrire un fichier partagé -df.to_csv('/home/jovyan/shared/resultats_alice.csv') +# To write a shared file +df.to_csv('/home/jovyan/shared/alice_results.csv') ``` -### Récupérer les travaux des étudiants +### Retrieve Student Work ```bash -# Lister les volumes utilisateurs +# List user volumes docker volume ls | grep jupyterhub-user -# Copier les fichiers d'un étudiant spécifique +# Copy files from a specific student docker run --rm \ -v jupyterhub-user-alice:/source \ - -v ~/rendus:/target \ + -v ~/submissions:/target \ alpine sh -c "cp -r /source/* /target/alice/" -# Copier tous les travaux partagés +# Copy all shared work docker run --rm \ -v jupyterhub-shared:/source \ - -v ~/rendus/shared:/target \ + -v ~/submissions/shared:/target \ alpine sh -c "cp -r /source/* /target/" ``` -## Gestion des utilisateurs +## User Management -### Option 1 : Liste d'utilisateurs prédéfinis -Dans `jupyterhub_config.py`, décommentez et modifiez : +### Option 1: Predefined User List +In `jupyterhub_config.py`, uncomment and modify: ```python -c.Authenticator.allowed_users = {'etudiant1', 'etudiant2', 'etudiant3'} +c.Authenticator.allowed_users = {'student1', 'student2', 'student3'} ``` -### Option 2 : Autoriser tout le monde (pour tests) -Par défaut, la configuration autorise n'importe quel utilisateur : +### Option 2: Allow Everyone (for testing) +By default, the configuration allows any user: ```python c.Authenticator.allow_all = True ``` -⚠️ **Attention** : DummyAuthenticator est UNIQUEMENT pour les tests locaux ! +⚠️ **Warning**: DummyAuthenticator is ONLY for local testing! -## Vérification des kernels +## Kernel Verification -Une fois connecté, créez un nouveau notebook et vérifiez que vous avez accès à : -- **Python 3** (kernel par défaut) -- **R** (kernel R) -- **Bash** (kernel bash) +Once logged in, create a new notebook and verify you have access to: +- **Python 3** (default kernel) +- **R** (R kernel) +- **Bash** (bash kernel) -## Personnalisation pour vos TP +## Customization for Your Labs -### Ajouter des packages R supplémentaires -Modifiez le `Dockerfile` (avant `USER ${NB_UID}`) : +### Add Additional R Packages +Modify the `Dockerfile` (before `USER ${NB_UID}`): ```dockerfile -RUN R -e "install.packages(c('votre_package'), repos='http://cran.rstudio.com/')" +RUN R -e "install.packages(c('your_package'), repos='http://cran.rstudio.com/')" ``` -Puis reconstruisez : +Then rebuild: ```bash docker build -t jupyterhub-student:latest -f Dockerfile . docker-compose restart jupyterhub ``` -### Ajouter des packages Python -Ajoutez dans le `Dockerfile` (avant `USER ${NB_UID}`) : +### Add Python Packages +Add to the `Dockerfile` (before `USER ${NB_UID}`): ```dockerfile RUN pip install numpy pandas matplotlib seaborn ``` -### Distribuer des fichiers aux étudiants -Créez un dossier `files_tp/` et ajoutez dans le `Dockerfile` : +### Distribute Files to Students +Create a `files_lab/` directory and add to the `Dockerfile`: ```dockerfile -COPY files_tp/ /home/${NB_USER}/tp/ -RUN chown -R ${NB_UID}:${NB_GID} /home/${NB_USER}/tp +COPY files_lab/ /home/${NB_USER}/lab/ +RUN chown -R ${NB_UID}:${NB_GID} /home/${NB_USER}/lab ``` -### Changer le port (si 8000 est occupé) -Modifiez dans `docker-compose.yml` : +### Change Port (if 8000 is occupied) +Modify in `docker-compose.yml`: ```yaml ports: - - "8001:8000" # Accessible sur localhost:8001 + - "8001:8000" # Accessible on localhost:8001 ``` -## Avantages de cette approche +## Advantages of This Approach -✅ **Tout en Docker** : Plus besoin d'installer Python/JupyterHub sur votre Mac -✅ **Portable** : Facile à déployer sur un autre Mac ou serveur -✅ **Isolé** : Pas de pollution de votre environnement système -✅ **Facile à nettoyer** : Un simple `docker-compose down` suffit -✅ **Reproductible** : Les étudiants auront exactement le même environnement +✅ **Everything in Docker**: No need to install Python/JupyterHub on your Mac +✅ **Portable**: Easy to deploy on another Mac or server +✅ **Isolated**: No pollution of your system environment +✅ **Easy to Clean**: A simple `docker-compose down` is enough +✅ **Reproducible**: Students will have exactly the same environment -## Dépannage +## Troubleshooting -**Erreur "Cannot connect to Docker daemon"** : -- Vérifiez qu'OrbStack est démarré -- Vérifiez que le socket existe : `ls -la /var/run/docker.sock` +**Error "Cannot connect to Docker daemon"**: +- Check that OrbStack is running +- Verify the socket exists: `ls -la /var/run/docker.sock` -**Les containers étudiants ne démarrent pas** : -- Vérifiez les logs : `docker-compose logs jupyterhub` -- Vérifiez que l'image étudiants existe : `docker images | grep jupyterhub-student` +**Student containers don't start**: +- Check logs: `docker-compose logs jupyterhub` +- Verify student image exists: `docker images | grep jupyterhub-student` -**Port 8000 déjà utilisé** : -- Changez le port dans `docker-compose.yml` +**Port 8000 already in use**: +- Change port in `docker-compose.yml` -**Après modification du config, les changements ne sont pas pris en compte** : +**After config modification, changes are not applied**: ```bash docker-compose restart jupyterhub ``` -**Je veux repartir de zéro** : +**I want to start from scratch**: ```bash docker-compose down -v docker rmi jupyterhub-hub jupyterhub-student -# Puis reconstruire tout -``` \ No newline at end of file +# Then rebuild everything +./start-jupyterhub.sh +``` diff --git a/docker-compose.yaml b/docker-compose.yaml index faaedd7..8ff08d1 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -6,21 +6,21 @@ services: container_name: jupyterhub image: jupyterhub-hub:latest ports: - - "8888:8000" + - "8000:8000" volumes: - # Accès au socket Docker pour spawner les containers étudiants + # Access to Docker socket to spawn student containers - /var/run/docker.sock:/var/run/docker.sock - # Persistance de la base de données JupyterHub + # JupyterHub database persistence - jupyterhub-data:/srv/jupyterhub - # Montage du fichier de config en direct (pour modifications faciles) + # Mount config file directly (for easy modifications) - ./jupyterhub_config.py:/srv/jupyterhub/jupyterhub_config.py:ro networks: - jupyterhub-network restart: unless-stopped environment: - # Mot de passe partagé pour tous les étudiants + # Shared password for all students JUPYTERHUB_PASSWORD: metabar2025 - # Variables d'environnement optionnelles + # Optional environment variables DOCKER_NOTEBOOK_DIR: /home/jovyan/work networks: @@ -31,4 +31,5 @@ networks: volumes: jupyterhub-data: jupyterhub-shared: - jupyterhub-course: \ No newline at end of file + jupyterhub-course: + \ No newline at end of file diff --git a/jupyterhub_config.py b/jupyterhub_config.py index d8fbc75..4bf7668 100644 --- a/jupyterhub_config.py +++ b/jupyterhub_config.py @@ -1,5 +1,96 @@ import os +# Base configuration +c.JupyterHub.spawner_class = 'dockerspawner.DockerSpawner' + +# Enable debug logs +c.JupyterHub.log_level = 'DEBUG' +c.Spawner.debug = True + +# Docker image to use for student containers +c.DockerSpawner.image = 'jupyterhub-student:latest' + +# Docker network (create with: docker network create jupyterhub-network) +c.DockerSpawner.network_name = 'jupyterhub-network' + +# Connection to OrbStack Docker socket from the hub container +c.DockerSpawner.client_kwargs = {'base_url': 'unix:///var/run/docker.sock'} + +# IMPORTANT: Internal URL for communication between containers +# The hub container communicates with user containers via Docker network +c.JupyterHub.hub_ip = '0.0.0.0' +c.JupyterHub.hub_connect_ip = 'jupyterhub' + +# Network configuration for student containers +c.DockerSpawner.use_internal_ip = True +c.DockerSpawner.network_name = 'jupyterhub-network' +c.DockerSpawner.extra_host_config = {'network_mode': 'jupyterhub-network'} + +# Remove containers after disconnection (optional, set to False to keep containers) +c.DockerSpawner.remove = True + +# Container naming +c.DockerSpawner.name_template = "jupyter-{username}" + +# Volume mounting to persist student data +# Set root to /home/jovyan to see all directories +notebook_dir = '/home/jovyan' +c.DockerSpawner.notebook_dir = notebook_dir + +# Personal volume for each student + shared volume +c.DockerSpawner.volumes = { + # Personal volume (persistent) - mounted in work/ + 'jupyterhub-user-{username}': '/home/jovyan/work', + # Shared volume between all students + 'jupyterhub-shared': '/home/jovyan/shared', + # Shared read-only volume for course files (optional) + 'jupyterhub-course': { + 'bind': '/home/jovyan/course', + 'mode': 'ro' # read-only + } +} + +# Memory and CPU configuration (adjust according to your needs) +c.DockerSpawner.mem_limit = '2G' +c.DockerSpawner.cpu_limit = 1.0 + +# User configuration - Simple password authentication for lab +from jupyterhub.auth import DummyAuthenticator + +class SimplePasswordAuthenticator(DummyAuthenticator): + """Simple authenticator with a shared password for everyone""" + + def check_allowed(self, username, authentication=None): + """Check if user is allowed""" + if authentication is None: + return False + + # Get password from environment variable + expected_password = os.environ.get('JUPYTERHUB_PASSWORD', 'metabar2025') + provided_password = authentication.get('password', '') + + # Check password + return provided_password == expected_password + +c.JupyterHub.authenticator_class = SimplePasswordAuthenticator + +# To create a list of allowed users, uncomment and modify: +# c.Authenticator.allowed_users = {'student1', 'student2', 'student3'} + +# Or allow any user with the correct password: +c.Authenticator.allow_all = True + +# Admin configuration +c.Authenticator.admin_users = {'admin'} + +# Listening port +c.JupyterHub.bind_url = 'http://0.0.0.0:8000' + +# Timeout +c.Spawner.start_timeout = 300 +c.Spawner.http_timeout = 120 +import os + # Configuration de base c.JupyterHub.spawner_class = 'dockerspawner.DockerSpawner' @@ -88,4 +179,4 @@ c.JupyterHub.bind_url = 'http://0.0.0.0:8000' # Timeout c.Spawner.start_timeout = 300 -c.Spawner.http_timeout = 120 \ No newline at end of file +c.Spawner.http_timeout = 120 diff --git a/setup.sh b/setup.sh index 5f2fb65..9d95ed5 100644 --- a/setup.sh +++ b/setup.sh @@ -1,30 +1,83 @@ -#!/usr/bin/env bash -set -e +#!/bin/bash -# === Variables === -WORKDIR="$PWD" -NETWORK="jupyterhub-net" -HUB_IMAGE="jupyterhub-hub" -USER_IMAGE="jupyter-tp-singleuser" +# JupyterHub startup script for labs +# Usage: ./start-jupyterhub.sh -# === Préparation === -mkdir -p "$WORKDIR" -cd "$WORKDIR" +set -e # Stop on error -echo "[1/5] Création du réseau Docker..." -docker network inspect $NETWORK >/dev/null 2>&1 || docker network create $NETWORK +echo "🚀 Starting JupyterHub for Lab" +echo "==============================" +echo "" -echo "[2/5] Construction des images..." -docker build -t $USER_IMAGE -f Dockerfile . -docker build -t $HUB_IMAGE -f Dockerfile.hub . +# Colors for display +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color -echo "[3/5] Lancement de JupyterHub..." -docker compose up -d +# Check we're in the right directory +if [ ! -f "Dockerfile" ] || [ ! -f "docker-compose.yml" ]; then + echo "❌ Error: Run this script from the jupyterhub-tp/ directory" + exit 1 +fi -echo "[4/5] Hub accessible sur http://localhost:8888" -echo " Login avec n'importe quel nom et mot de passe : metabar2025" +# Stop existing containers +echo -e "${BLUE}📦 Stopping existing containers...${NC}" +docker-compose down 2>/dev/null || true -echo "[5/5] Pour voir les utilisateurs actifs :" -echo " docker ps | grep jupyterhub-user" +# Remove old student containers +echo -e "${BLUE}🧹 Cleaning up student containers...${NC}" +docker ps -aq --filter name=jupyter- | xargs -r docker rm -f 2>/dev/null || true -echo "Terminé." +# Build student image +echo "" +echo -e "${BLUE}🔨 Building student image...${NC}" +docker build -t jupyterhub-student:latest -f Dockerfile . + +# Build hub image +echo "" +echo -e "${BLUE}🔨 Building JupyterHub image...${NC}" +docker build -t jupyterhub-hub:latest -f Dockerfile.hub . + +# Create volumes if they don't exist +echo "" +echo -e "${BLUE}💾 Creating shared volumes...${NC}" +docker volume create jupyterhub-shared 2>/dev/null || echo " Volume jupyterhub-shared already exists" +docker volume create jupyterhub-course 2>/dev/null || echo " Volume jupyterhub-course already exists" + +# Start the stack +echo "" +echo -e "${BLUE}🚀 Starting JupyterHub...${NC}" +docker-compose up -d + +# Wait for service to be ready +echo "" +echo -e "${YELLOW}⏳ Waiting for JupyterHub to start...${NC}" +sleep 3 + +# Check that container is running +if docker ps | grep -q jupyterhub; then + echo "" + echo -e "${GREEN}✅ JupyterHub is running!${NC}" + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo -e "${GREEN}🌐 JupyterHub available at: http://localhost:8000${NC}" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo "📝 Password: metabar2025" + echo "👥 Students can connect with any username" + echo "" + echo "📂 Each student will have access to:" + echo " - work/ : personal workspace" + echo " - shared/ : shared workspace" + echo " - course/ : course files (read-only)" + echo "" + echo "🔍 To view logs: docker-compose logs -f jupyterhub" + echo "🛑 To stop: docker-compose down" + echo "" +else + echo "" + echo -e "${YELLOW}⚠️ JupyterHub container doesn't seem to be starting${NC}" + echo "Check logs with: docker-compose logs jupyterhub" + exit 1 +fi From 60130d6b422e6afa9c989226f666f314a88d75fe Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Wed, 15 Oct 2025 07:15:05 +0200 Subject: [PATCH 2/3] Ajout des obitools --- Dockerfile | 20 +++++- Readme.md | 67 +++++++++++++++++++- docker-compose.yaml => docker-compose.yml | 4 +- install_packages.sh | 74 +++++++++++++++++++++++ jupyterhub_config.py | 51 +++++++++++----- setup.sh | 12 ++-- 6 files changed, 205 insertions(+), 23 deletions(-) rename docker-compose.yaml => docker-compose.yml (88%) create mode 100644 install_packages.sh mode change 100644 => 100755 setup.sh diff --git a/Dockerfile b/Dockerfile index 039bd7d..cc5f2a6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,9 @@ RUN apt-get update && apt-get install -y \ libcurl4-openssl-dev \ libssl-dev \ libxml2-dev \ + git \ + build-essential \ + curl \ && apt-get clean && rm -rf /var/lib/apt/lists/* # Install R kernel for Jupyter (as root) @@ -16,16 +19,29 @@ RUN R -e "install.packages('IRkernel', repos='http://cran.rstudio.com/')" && \ R -e "IRkernel::installspec(user = FALSE)" # Install some useful R packages for labs -RUN R -e "install.packages(c('ggplot2', 'dplyr', 'tidyr', 'readr'), repos='http://cran.rstudio.com/')" +RUN R -e "install.packages(c('tidyverse', 'vegan', 'ade4'), repos='http://cran.rstudio.com/')" # Install bash kernel (as root also) RUN pip install bash_kernel && \ python -m bash_kernel.install --sys-prefix +# Install obitools4 (written in Go) +RUN curl -L https://raw.githubusercontent.com/metabarcoding/obitools4/master/install_obitools.sh | bash + +# Install csvkit +RUN pip install csvkit + +# Install csvlens +RUN curl https://sh.rustup.rs -sSf | bash -s -- -y \ + && . $HOME/.cargo/env \ + && cargo install csvlens \ + && mv $HOME/.cargo/bin/csvlens /usr/local/bin/ \ + && rm -rf $HOME/.cargo $HOME/.rustup + # Create necessary directories with proper permissions RUN mkdir -p /home/${NB_USER}/.local/share/jupyter && \ chown -R ${NB_UID}:${NB_GID} /home/${NB_USER} USER ${NB_UID} -WORKDIR /home/${NB_USER} \ No newline at end of file +WORKDIR /home/${NB_USER} diff --git a/Readme.md b/Readme.md index 0250b0f..5b9f631 100644 --- a/Readme.md +++ b/Readme.md @@ -113,6 +113,71 @@ Each student will see these directories in their JupyterLab: - **`work/`** : Personal workspace (persistent, private) - **`shared/`** : Shared workspace between all students (read/write) - **`course/`** : Course files (read-only, you deposit files) + - **`course/R_packages/`** : Shared R packages (read-only for students, only admin can install) + +### User Accounts + +**Admin Account:** +- Username: `admin` +- Password: `admin2025` (change in docker-compose.yml: `JUPYTERHUB_ADMIN_PASSWORD`) +- Can write to `course/` directory + +**Student Accounts:** +- Username: any name +- Password: `metabar2025` (change in docker-compose.yml: `JUPYTERHUB_PASSWORD`) +- Read-only access to `course/` directory + +### Installing R Packages (Admin Only) + +**From your Mac (recommended):** + +```bash +chmod +x install-r-packages-admin.sh + +# Install packages +./install-r-packages-admin.sh reshape2 plotly knitr +``` + +This script: +- Installs packages in the `course/R_packages/` directory +- All students can use them (read-only) +- No need to rebuild the image + +**From admin notebook:** + +Login as `admin` and create an R notebook: + +```r +# Install packages in course directory (admin only) +course_lib <- "/home/jovyan/course/R_packages" +dir.create(course_lib, recursive = TRUE, showWarnings = FALSE) + +install.packages(c('reshape2', 'plotly', 'knitr'), + lib = course_lib, + repos = 'http://cran.rstudio.com/') +``` + +Note: Admin account has write access to the course directory. + +### Using R Packages (Students) + +Students simply load packages normally: +```r +library(reshape2) # Loads from course/R_packages/ automatically +library(plotly) +``` + +R automatically finds packages in `/home/jovyan/course/R_packages/` thanks to the `R_LIBS_USER` environment variable. + +### List Available Packages + +```r +# List all available packages +installed.packages()[,"Package"] + +# Or check course packages specifically +list.files("/home/jovyan/course/R_packages") +``` ### Deposit Files for Course @@ -254,4 +319,4 @@ docker-compose down -v docker rmi jupyterhub-hub jupyterhub-student # Then rebuild everything ./start-jupyterhub.sh -``` +``` \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yml similarity index 88% rename from docker-compose.yaml rename to docker-compose.yml index 8ff08d1..6d22d71 100644 --- a/docker-compose.yaml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ services: container_name: jupyterhub image: jupyterhub-hub:latest ports: - - "8000:8000" + - "8888:8000" volumes: # Access to Docker socket to spawn student containers - /var/run/docker.sock:/var/run/docker.sock @@ -20,6 +20,8 @@ services: environment: # Shared password for all students JUPYTERHUB_PASSWORD: metabar2025 + # Admin password (for installing R packages) + JUPYTERHUB_ADMIN_PASSWORD: admin2025 # Optional environment variables DOCKER_NOTEBOOK_DIR: /home/jovyan/work diff --git a/install_packages.sh b/install_packages.sh new file mode 100644 index 0000000..57bd94a --- /dev/null +++ b/install_packages.sh @@ -0,0 +1,74 @@ +#!/bin/bash + +# Script to install R packages in the course directory (admin only) +# Usage: ./install-r-packages.sh package1 package2 package3 + +set -e + +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +echo -e "${BLUE}📦 R Package Installer (Admin)${NC}" +echo "================================" +echo "" + +# Check if packages are provided +if [ $# -eq 0 ]; then + echo -e "${RED}❌ Error: No packages specified${NC}" + echo "" + echo "Usage:" + echo " ./install-r-packages.sh package1 package2 package3" + echo "" + echo "Example:" + echo " ./install-r-packages.sh reshape2 plotly knitr" + exit 1 +fi + +# Build package list +PACKAGES=$(IFS=,; echo "$*") +R_PACKAGES=$(echo "$PACKAGES" | sed "s/,/', '/g") + +echo -e "${YELLOW}📋 Packages to install: ${R_PACKAGES}${NC}" +echo "" + +# Create a temporary directory for installation +TEMP_DIR=$(mktemp -d) +echo -e "${BLUE}📁 Creating temporary directory: ${TEMP_DIR}${NC}" + +# Install packages in temporary directory +echo -e "${BLUE}🔨 Installing R packages...${NC}" +docker run --rm \ + --user root \ + -v "${TEMP_DIR}:/temp_packages" \ + jupyterhub-student:latest \ + R -e "install.packages(c('${R_PACKAGES}'), lib='/temp_packages', repos='http://cran.rstudio.com/')" || { + echo -e "${RED}❌ Failed to install packages${NC}" + rm -rf "${TEMP_DIR}" + exit 1 +} + +# Copy to course volume +echo "" +echo -e "${BLUE}💾 Copying to course volume...${NC}" +docker run --rm \ + -v jupyterhub-course:/target \ + -v "${TEMP_DIR}:/source" \ + alpine sh -c "mkdir -p /target/R_packages && cp -r /source/* /target/R_packages/" + +# Clean up +rm -rf "${TEMP_DIR}" + +# List installed packages +echo "" +echo -e "${GREEN}✅ Installation complete!${NC}" +echo "" +echo -e "${BLUE}📦 Installed packages in course/R_packages:${NC}" +docker run --rm \ + -v jupyterhub-course:/course \ + alpine ls -1 /course/R_packages/ + +echo "" +echo -e "${YELLOW}ℹ️ Students need to restart their R kernels to use new packages.${NC}" \ No newline at end of file diff --git a/jupyterhub_config.py b/jupyterhub_config.py index 4bf7668..ba65a7b 100644 --- a/jupyterhub_config.py +++ b/jupyterhub_config.py @@ -33,19 +33,19 @@ c.DockerSpawner.remove = True c.DockerSpawner.name_template = "jupyter-{username}" # Volume mounting to persist student data -# Set root to /home/jovyan to see all directories -notebook_dir = '/home/jovyan' +# Set root to work/ - everything is persistent +notebook_dir = '/home/jovyan/work' c.DockerSpawner.notebook_dir = notebook_dir -# Personal volume for each student + shared volume +# Personal volume for each student + shared volumes under work/ c.DockerSpawner.volumes = { - # Personal volume (persistent) - mounted in work/ + # Personal volume (persistent) - root directory 'jupyterhub-user-{username}': '/home/jovyan/work', - # Shared volume between all students - 'jupyterhub-shared': '/home/jovyan/shared', - # Shared read-only volume for course files (optional) + # Shared volume between all students - under work/ + 'jupyterhub-shared': '/home/jovyan/work/shared', + # Shared read-only volume for course files - under work/ 'jupyterhub-course': { - 'bind': '/home/jovyan/course', + 'bind': '/home/jovyan/work/course', 'mode': 'ro' # read-only } } @@ -54,6 +54,23 @@ c.DockerSpawner.volumes = { c.DockerSpawner.mem_limit = '2G' c.DockerSpawner.cpu_limit = 1.0 +# Environment variables for student containers +c.DockerSpawner.environment = { + # R package library in read-only course directory under work/ + 'R_LIBS_USER': '/home/jovyan/work/R_packages', + 'R_LIBS_SITE': '/home/jovyan/work/course/R_packages', + # Path to R packages in read-only + 'PATH': '/home/jovyan/work/course/bin:${PATH}' +} + +# Create user R lib directory +#async def create_user_hierarchy(spawner): +# cmd = "mkdir -p /home/jovyan/work/R_packages && chown jovyan:jovyan /home/jovyan/work/R_packages" +# spawner.extra_create_kwargs.update({'command': f"/bin/bash -c '{cmd} && start-notebook.sh'"}) + +#c.Spawner.pre_spawn_hook = create_user_r_libs + + # User configuration - Simple password authentication for lab from jupyterhub.auth import DummyAuthenticator @@ -65,12 +82,16 @@ class SimplePasswordAuthenticator(DummyAuthenticator): if authentication is None: return False - # Get password from environment variable - expected_password = os.environ.get('JUPYTERHUB_PASSWORD', 'metabar2025') provided_password = authentication.get('password', '') - # Check password - return provided_password == expected_password + # Admin user with special password + if username == 'admin': + admin_password = os.environ.get('JUPYTERHUB_ADMIN_PASSWORD', 'admin2025') + return provided_password == admin_password + + # Regular students with shared password + student_password = os.environ.get('JUPYTERHUB_PASSWORD', 'metabar2025') + return provided_password == student_password c.JupyterHub.authenticator_class = SimplePasswordAuthenticator @@ -125,7 +146,7 @@ c.DockerSpawner.name_template = "jupyter-{username}" # Montage de volumes pour persister les données des étudiants # Définir la racine à /home/jovyan pour voir tous les dossiers -notebook_dir = '/home/jovyan' +notebook_dir = '/home/jovyan/work' c.DockerSpawner.notebook_dir = notebook_dir # Volume personnel pour chaque étudiant + volume partagé @@ -133,10 +154,10 @@ c.DockerSpawner.volumes = { # Volume personnel (persistant) - monté dans work/ 'jupyterhub-user-{username}': '/home/jovyan/work', # Volume partagé entre tous les étudiants - 'jupyterhub-shared': '/home/jovyan/shared', + 'jupyterhub-shared': '/home/jovyan/work/shared', # Volume partagé en lecture seule pour les fichiers du cours (optionnel) 'jupyterhub-course': { - 'bind': '/home/jovyan/course', + 'bind': '/home/jovyan/work/course', 'mode': 'ro' # read-only } } diff --git a/setup.sh b/setup.sh old mode 100644 new mode 100755 index 9d95ed5..29b4fa2 --- a/setup.sh +++ b/setup.sh @@ -61,16 +61,20 @@ if docker ps | grep -q jupyterhub; then echo -e "${GREEN}✅ JupyterHub is running!${NC}" echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo -e "${GREEN}🌐 JupyterHub available at: http://localhost:8000${NC}" + echo -e "${GREEN}🌐 JupyterHub available at: http://localhost:8888${NC}" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "📝 Password: metabar2025" echo "👥 Students can connect with any username" echo "" + echo "🔑 Admin account:" + echo " Username: admin" + echo " Password: admin2025" + echo "" echo "📂 Each student will have access to:" - echo " - work/ : personal workspace" - echo " - shared/ : shared workspace" - echo " - course/ : course files (read-only)" + echo " - work/ : personal workspace (everything here is saved)" + echo " - work/shared/ : shared workspace" + echo " - work/course/ : course files (read-only)" echo "" echo "🔍 To view logs: docker-compose logs -f jupyterhub" echo "🛑 To stop: docker-compose down" From 64cb75e40ad90d2eea4036c8fd38b8ba1013b45c Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Wed, 15 Oct 2025 14:08:52 +0200 Subject: [PATCH 3/3] Image multistage --- Dockerfile | 60 +++++++++++++++++++++++--------------------- Readme.md | 60 ++++++++++++++++++++++++++++++++------------ install_packages.sh | 8 +++--- jupyterhub_config.py | 39 +--------------------------- setup.sh | 9 ++++--- 5 files changed, 86 insertions(+), 90 deletions(-) diff --git a/Dockerfile b/Dockerfile index cc5f2a6..d9290fb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,47 +1,49 @@ -FROM jupyter/base-notebook:latest +# ---------- Stage 1 : builder ---------- +FROM jupyter/base-notebook:latest AS builder USER root -# Install R and system dependencies +# Install system dependencies for R, build tools and Go/Rust RUN apt-get update && apt-get install -y \ - r-base \ - r-base-dev \ - libcurl4-openssl-dev \ - libssl-dev \ - libxml2-dev \ - git \ - build-essential \ - curl \ + r-base r-base-dev \ + libcurl4-openssl-dev libssl-dev libxml2-dev \ + build-essential git curl \ && apt-get clean && rm -rf /var/lib/apt/lists/* -# Install R kernel for Jupyter (as root) +# Install R kernel + useful packages RUN R -e "install.packages('IRkernel', repos='http://cran.rstudio.com/')" && \ - R -e "IRkernel::installspec(user = FALSE)" + R -e "IRkernel::installspec(user = FALSE)" && \ + R -e "install.packages(c('tidyverse','vegan','ade4'), repos='http://cran.rstudio.com/')" -# Install some useful R packages for labs -RUN R -e "install.packages(c('tidyverse', 'vegan', 'ade4'), repos='http://cran.rstudio.com/')" +# Install bash kernel +RUN pip install bash_kernel && python -m bash_kernel.install --sys-prefix -# Install bash kernel (as root also) -RUN pip install bash_kernel && \ - python -m bash_kernel.install --sys-prefix - -# Install obitools4 (written in Go) +# Install obitools4 RUN curl -L https://raw.githubusercontent.com/metabarcoding/obitools4/master/install_obitools.sh | bash # Install csvkit RUN pip install csvkit -# Install csvlens -RUN curl https://sh.rustup.rs -sSf | bash -s -- -y \ - && . $HOME/.cargo/env \ - && cargo install csvlens \ - && mv $HOME/.cargo/bin/csvlens /usr/local/bin/ \ - && rm -rf $HOME/.cargo $HOME/.rustup +# Install csvlens via Rust +RUN curl https://sh.rustup.rs -sSf | bash -s -- -y && \ + . $HOME/.cargo/env && \ + cargo install csvlens -# Create necessary directories with proper permissions +RUN apt-get update && apt-get install -y ruby ruby-dev build-essential \ + && gem install youplot + +# Copy csvlens to /usr/local/bin for final use +RUN cp $HOME/.cargo/bin/csvlens /usr/local/bin/ + +# Set permissions for Jupyter user RUN mkdir -p /home/${NB_USER}/.local/share/jupyter && \ - chown -R ${NB_UID}:${NB_GID} /home/${NB_USER} + chown -R ${NB_UID}:${NB_GID} /home/${NB_USER} -USER ${NB_UID} +# Switch back to Jupyter user +USER ${NB_UID}:${NB_GID} +WORKDIR /home/${NB_USER}/work -WORKDIR /home/${NB_USER} +# Environment variables +ENV PATH="/home/${NB_USER}/work/course/bin:${PATH}" +ENV R_LIBS_USER="/home/${NB_USER}/work/R_packages" +ENV R_LIBS_SITE="/home/${NB_USER}/work/course/R_packages:/usr/local/lib/R/site-library:/usr/lib/R/site-library" diff --git a/Readme.md b/Readme.md index 5b9f631..55d6542 100644 --- a/Readme.md +++ b/Readme.md @@ -47,7 +47,7 @@ chmod +x start-jupyterhub.sh ### 5. Access JupyterHub -Open your browser and go to: **http://localhost:8000** +Open your browser and go to: **http://localhost:8888** You can log in with any username and password: `metabar2025` @@ -109,11 +109,24 @@ docker volume prune -f ### Directory Structure for Each Student -Each student will see these directories in their JupyterLab: -- **`work/`** : Personal workspace (persistent, private) -- **`shared/`** : Shared workspace between all students (read/write) -- **`course/`** : Course files (read-only, you deposit files) - - **`course/R_packages/`** : Shared R packages (read-only for students, only admin can install) +Each student will see this directory structure in their JupyterLab (everything under `work/` is persistent): +``` +work/ # Personal workspace root (persistent) +├── [student files] # Their own files and notebooks +├── R_packages/ # Personal R packages (writable by student) +├── shared/ # Shared workspace (read/write, shared with all) +└── course/ # Course files (read-only, managed by admin) + ├── R_packages/ # Shared R packages (read-only, installed by prof) + ├── bin/ # Shared executables (in PATH) + └── [course materials] # Your course files +``` + +**R Package Priority:** +1. R checks `work/R_packages/` first (personal, writable) +2. Then `work/course/R_packages/` (shared, read-only, installed by prof) +3. Then system libraries + +**Important:** Everything is under `work/`, so all student files are automatically saved in their persistent volume. ### User Accounts @@ -148,8 +161,8 @@ This script: Login as `admin` and create an R notebook: ```r -# Install packages in course directory (admin only) -course_lib <- "/home/jovyan/course/R_packages" +# Install packages in course/R_packages (admin only, available to all students) +course_lib <- "/home/jovyan/work/course/R_packages" dir.create(course_lib, recursive = TRUE, showWarnings = FALSE) install.packages(c('reshape2', 'plotly', 'knitr'), @@ -159,24 +172,39 @@ install.packages(c('reshape2', 'plotly', 'knitr'), Note: Admin account has write access to the course directory. +**Students can also install their own packages:** + +Students can install packages in their personal `work/R_packages/`: + +```r +# Install in personal library (each student has their own) +install.packages(c('mypackage')) # Will install in work/R_packages/ +``` + ### Using R Packages (Students) Students simply load packages normally: ```r -library(reshape2) # Loads from course/R_packages/ automatically +library(reshape2) # R checks: 1) work/R_packages/ 2) work/course/R_packages/ 3) system library(plotly) ``` -R automatically finds packages in `/home/jovyan/course/R_packages/` thanks to the `R_LIBS_USER` environment variable. +R automatically searches in this order: +1. Personal packages: `/home/jovyan/work/R_packages/` (R_LIBS_USER) +2. Prof packages: `/home/jovyan/work/course/R_packages/` (R_LIBS_SITE) +3. System packages ### List Available Packages ```r -# List all available packages +# List all available packages (personal + course + system) installed.packages()[,"Package"] -# Or check course packages specifically -list.files("/home/jovyan/course/R_packages") +# Check personal packages +list.files("/home/jovyan/work/R_packages") + +# Check course packages (installed by prof) +list.files("/home/jovyan/work/course/R_packages") ``` ### Deposit Files for Course @@ -205,10 +233,10 @@ Students can collaborate via the `shared/` directory: ```python # In a notebook, to read a shared file import pandas as pd -df = pd.read_csv('/home/jovyan/shared/group_data.csv') +df = pd.read_csv('/home/jovyan/work/shared/group_data.csv') # To write a shared file -df.to_csv('/home/jovyan/shared/alice_results.csv') +df.to_csv('/home/jovyan/work/shared/alice_results.csv') ``` ### Retrieve Student Work @@ -319,4 +347,4 @@ docker-compose down -v docker rmi jupyterhub-hub jupyterhub-student # Then rebuild everything ./start-jupyterhub.sh -``` \ No newline at end of file +``` diff --git a/install_packages.sh b/install_packages.sh index 57bd94a..099c248 100644 --- a/install_packages.sh +++ b/install_packages.sh @@ -50,9 +50,9 @@ docker run --rm \ exit 1 } -# Copy to course volume +# Copy to course R packages directory echo "" -echo -e "${BLUE}💾 Copying to course volume...${NC}" +echo -e "${BLUE}💾 Copying to course/R_packages...${NC}" docker run --rm \ -v jupyterhub-course:/target \ -v "${TEMP_DIR}:/source" \ @@ -65,10 +65,10 @@ rm -rf "${TEMP_DIR}" echo "" echo -e "${GREEN}✅ Installation complete!${NC}" echo "" -echo -e "${BLUE}📦 Installed packages in course/R_packages:${NC}" +echo -e "${BLUE}📦 Installed packages in work/course/R_packages:${NC}" docker run --rm \ -v jupyterhub-course:/course \ alpine ls -1 /course/R_packages/ echo "" -echo -e "${YELLOW}ℹ️ Students need to restart their R kernels to use new packages.${NC}" \ No newline at end of file +echo -e "${YELLOW}ℹ️ Students need to restart their R kernels to use new packages.${NC}" diff --git a/jupyterhub_config.py b/jupyterhub_config.py index ba65a7b..2334e2e 100644 --- a/jupyterhub_config.py +++ b/jupyterhub_config.py @@ -54,23 +54,6 @@ c.DockerSpawner.volumes = { c.DockerSpawner.mem_limit = '2G' c.DockerSpawner.cpu_limit = 1.0 -# Environment variables for student containers -c.DockerSpawner.environment = { - # R package library in read-only course directory under work/ - 'R_LIBS_USER': '/home/jovyan/work/R_packages', - 'R_LIBS_SITE': '/home/jovyan/work/course/R_packages', - # Path to R packages in read-only - 'PATH': '/home/jovyan/work/course/bin:${PATH}' -} - -# Create user R lib directory -#async def create_user_hierarchy(spawner): -# cmd = "mkdir -p /home/jovyan/work/R_packages && chown jovyan:jovyan /home/jovyan/work/R_packages" -# spawner.extra_create_kwargs.update({'command': f"/bin/bash -c '{cmd} && start-notebook.sh'"}) - -#c.Spawner.pre_spawn_hook = create_user_r_libs - - # User configuration - Simple password authentication for lab from jupyterhub.auth import DummyAuthenticator @@ -166,26 +149,6 @@ c.DockerSpawner.volumes = { c.DockerSpawner.mem_limit = '2G' c.DockerSpawner.cpu_limit = 1.0 -# Configuration des utilisateurs - Mot de passe simple pour TP -from jupyterhub.auth import DummyAuthenticator - -class SimplePasswordAuthenticator(DummyAuthenticator): - """Authentificateur simple avec un mot de passe partagé pour tous""" - - def check_allowed(self, username, authentication=None): - """Vérifie si l'utilisateur est autorisé""" - if authentication is None: - return False - - # Récupérer le mot de passe depuis la variable d'environnement - expected_password = os.environ.get('JUPYTERHUB_PASSWORD', 'metabar2025') - provided_password = authentication.get('password', '') - - # Vérifier le mot de passe - return provided_password == expected_password - -c.JupyterHub.authenticator_class = SimplePasswordAuthenticator - # Pour créer une liste d'utilisateurs autorisés, décommentez et modifiez: # c.Authenticator.allowed_users = {'etudiant1', 'etudiant2', 'etudiant3'} @@ -200,4 +163,4 @@ c.JupyterHub.bind_url = 'http://0.0.0.0:8000' # Timeout c.Spawner.start_timeout = 300 -c.Spawner.http_timeout = 120 +c.Spawner.http_timeout = 120 \ No newline at end of file diff --git a/setup.sh b/setup.sh index 29b4fa2..0af5ba4 100755 --- a/setup.sh +++ b/setup.sh @@ -72,9 +72,12 @@ if docker ps | grep -q jupyterhub; then echo " Password: admin2025" echo "" echo "📂 Each student will have access to:" - echo " - work/ : personal workspace (everything here is saved)" - echo " - work/shared/ : shared workspace" - echo " - work/course/ : course files (read-only)" + echo " - work/ : personal workspace (everything saved)" + echo " - work/R_packages/ : personal R packages (writable)" + echo " - work/shared/ : shared workspace" + echo " - work/course/ : course files (read-only)" + echo " - work/course/R_packages/ : shared R packages by prof (read-only)" + echo " - work/course/bin/ : shared executables (in PATH)" echo "" echo "🔍 To view logs: docker-compose logs -f jupyterhub" echo "🛑 To stop: docker-compose down"