Actualizado el 31 de Octubre de 2021
Introducción
Últimamente he estado usando Azure notebooks para mis notas en Jupyter pero parece que finalmente Microsoft ha decidido abandonarlo y da varias alternativas para la migración, pero ninguna me ha terminado de convencer, ya sea por estar atado a otro componente de Azure o por ser de pago, así que decidí incluir Jupyter dentro de una de mis Raspberry.
Uno de los problemas es la mala compatibilidad de Jupyter con Git, apenas existen extensiones que sean fiables así que decidí usar JupyterLab con su extensión de Git que es bastante completa.
Finalmente continuando con lo que parece que será la senda a seguir en un futuro cercano he optado a hacerlo en Docker, principalmente porque así lo puedo mover cómodamente entre mis Raspberry sin tener que instalarlo todo de nuevo (bueno, para lo que se supone que es Docker).
Estos son los pasos a seguir, en este caso use una Raspberry Pi 3 con 1 GB de RAM para comprobar el rendimiento, aunque Docker tiene más sentido en una Raspberry Pi 4 con al menos 4GB de RAM.
Configuración del sistema
Es recomendable leer este enlace antes de seguir, da instrucciones e ideas para instalar Docker.
DockerFile
Este es el fichero Dockerfile necesario para crear la imagen, vamos a usar como imagen base la versión ARM de Debian, posiblemente con Alpine el tamaño de la imagen sería menor (actualmente 1.2GB) pero puede haber algunos problemas de rendimiento.
# 0. Image and labels
FROM debian:latest
LABEL "guru.raraavis.creator"="blog@raraavis.guru"
LABEL "guru.raraavis.version"="1.1.0"
LABEL "guru.raraavis.release-date"="31/10/2021"
LABEL "guru.raraavis.description"="Raspberry Pi image with JupyterLab"
# 1. Environment vars
ARG DEBIAN_FRONTEND=noninteractive
ARG NODE_OPTIONS=--max-old-space-size=768
ARG TZ=Europe/Madrid
ENV NODE_VERSION=16
ENV JUPYTER_ID=1000
ENV TZ=$TZ
# 2. Install packages
# 2.1 Update system and install necessary packages
RUN apt-get update && \
apt-get install -y --no-install-recommends \
python3-dev \
python3-venv \
libatlas-base-dev \
libblas-dev \
liblapack-dev \
python-dev \
gfortran \
tzdata \
software-properties-common \
sudo \
gosu \
bzip2 \
git \
dumb-init \
curl && \
curl -sL https://deb.nodesource.com/setup_$NODE_VERSION.x | bash - && \
apt-get install -y --no-install-recommends nodejs && \
rm -rf /var/lib/apt/lists/* && \
rm -rf /tmp/*
# 2.2 Install pip
ADD https://bootstrap.pypa.io/get-pip.py get-pip.py
RUN python3 get-pip.py
RUN python3 -m pip config --global set global.extra-index-url https://www.piwheels.org/simple
# 2.3 Install pip packages and Jupyter
RUN python3 -m pip install --upgrade \
conda \
virtualenv \
ipykernel \
jupyter \
jupyterlab \
jupyterlab-git \
jupyter_contrib_nbextensions \
autopep8
RUN jupyter lab build
RUN jupyter contrib nbextension install --system
# 3. Add jupyter user
RUN adduser --uid $JUPYTER_ID --disabled-password --gecos '' jupyter
RUN adduser jupyter sudo
RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
# 4. Install Miniconda
WORKDIR /home/jupyter
ADD --chown=jupyter http://repo.continuum.io/miniconda/Miniconda3-latest-Linux-armv7l.sh Miniconda3.sh
RUN chmod 755 Miniconda3.sh
RUN md5sum Miniconda3.sh
RUN ./Miniconda3.sh -b -p /home/jupyter/miniconda3
RUN ./miniconda3/bin/conda config --add channels rpi
RUN rm ./Miniconda3.sh
# 5. Configure container
RUN mkdir -p /home/jupyter/notebooks
RUN chown -R jupyter:jupyter /home/jupyter
ONBUILD SHELL ["/bin/bash","-c"]
VOLUME /home/jupyter/notebooks
EXPOSE 8888
USER jupyter
# 6. Execute jupyter
CMD ["jupyter", "lab", "--port=8888", "--no-browser", "--ip=0.0.0.0", "--allow-root", "--notebook-dir=/mnt/jupyter/notebooks", "--ServerApp.token=", "--ServerApp.password="]
Variables de entorno (1)
El paquete tzdata cuando se instala necesita especificar una zona horaria, así que establecemos la variable de entorno TZ, si se establece en tiempo de ejecución indicará la zona horaria a usar para configurar el sistema, se puede volver a configurar usando dpkg-reconfigure, tzdata también necesitará establecer DEBIAN_FRONTEND=noninteractive para evitar preguntas durante la instalación.
Enlazamos ARG con ENV, ya que ARG solo se puede usar en tiempo de compilación la enlazamos con ENV para que se pueda establecer también en tiempo de ejecución.
NODE_OPTIONS se usará más adelante para establecer parámetros en NodeJs, principalmente en el uso de la memoria, ya que a no ser que la Raspberry tenga más de 2GB fallará durante la creación de la extensión de Git, para una RPI4 se podría eliminar o ampliar este parámetro.
Existe también la opción de parametrizar que versión de Node se quiere instalar -necesario para JupyterLab- por defecto la 16 que es la última disponible actualmente.
La variable JUPYTER_ID se usa para indicar que id de usuario tendrá el usuario Jupyter que luego crearemos, esto es muy útil para poder establecer permisos en el host y evitar algunos problemas.
Instalar paquetes (2)
El proceso de instalación consta de un solo comando RUN para evitar crear capas intermedias. También crea e instala la extensión de Git, hay que tener en cuenta que no existe una forma oficial de generar la extensión y luego instalarla, de ser así una opción habría sido crearla en un host y luego copiarla en otro.
También se incluyen varias librerías de sistema, muchas de ellas son necesarias para instalar ciertos paquetes Python (principalmente librerías científicas) así que es bueno tenerlas ya instaladas.
Actualizar el sistema e instalar Jupyter (2.1)
Los comandos básicos para actualizar el sistema e instalar Jupyter con sus dependencias, se usa –no-install-recommends y finalmente se elimina de la cache los paquetes para aligerar el tamaño del fichero Docker.
Instalar pip (2.2)
Aquí descargamos e instalamos pip, también añadimos el repositorio de paquetes Python piwheels, este repositorio contiene muchos de los paquetes Python preparados para Raspberry lo que ahorra mucho tiempo de compilación e instalación, también indicamos que este repositorio sea global para que cualquier usuario o entorno virtual se beneficie del mismo.
Instalar librerías Python y Jupyter (2.3)
Aquí se instalan todos los paquetes Python necesarios para JupyterLab incluyendo algunos que son útiles -como virtualenv- extensiones a JupyterLab -como la propia de Git– y finalmente el propio Jupyterlab, después solo hay que llamar al comando build. Aprovechamos también para instalar algunas extensiones útiles para JupyterLab.
Agregar usuario Jupyter (3)
Ahora creamos el usuario jupyter que usaremos a partir de ahora, lo configuramos sudo y configuramos la carpeta de trabajo.
Instalar Miniconda (4)
En este paso vamos a instalar Miniconda para el usuario Jupyter, se hace una descarga e instalación desatendida dentro de la carpeta que hemos creado previamente. Miniconda instala principalmente el gestor de paquetes Conda y el resto de paquetes habría que instalarlos por separado, hay que tener en cuenta que el soporte de Conda para Raspberry no es muy bueno.
Finalmente se agrega el canal (repositorio) de rpi a conda para poder descargar paquetes orientados a Raspberry Pi (similar a como hemos hecho antes para pip).
Al instalar Conda de manera desatendida no se incluyen los binarios de Conda en el PATH por lo tanto para usar los binarios de conda hay que ubicarse en la carpeta bin de la carpeta miniconda como se ve al usar el comando conda config, esta opción la he preferido para no tener que usar la versión de Python de Conda que generalmente es inferior a la instalada en el sistema.
Contenedor (5)
Establecemos una carpeta de trabajo donde crearemos los notebooks y la usaremos también para clonar repositorios, establecemos los permisos adecuados para el usuario Jupyter.
También usamos ONBUILD para indicar que las imágenes que hereden de esta usarán bash como shell, esto es importante porque sino algunos comandos como source muy usados para crear entornos virtuales no estarán disponibles.
Añadimos la configuración de los puertos y la carpeta que se usará como volumen para mapear con el host que es donde residen los cuadernos.
Ejecutar Jupyter
Establecemos el comando que arrancará Jupyter, algunas consideraciones:
- –no-browser: para evitar abrir el navegador al arrancar.
- –allow-root: permite ejecutar Jupyter como root.
- –notebook-dir: carpeta raíz de Jupyter, es decir de la que leerá los ficheros, que a su vez es la misma que usamos para Git.
- –NotebookApp.token y NotebookApp.password lo establecemos con cadenas vacías para indicar que no queremos usar autenticación, esto para un entorno particular como suele ser el de una Raspberry es muy adecuado, en entornos compartidos quizás tenga más sentido no usar estos valores y establecerlos a través del archivo de configuración.
Compilación
Ejecutamos con el siguiente comando para compilar, previamente hacemos una limpieza para evitar posibles errores (errores GPG con las claves de los orígenes de los paquetes por ejemplo). Es posible que de algún error al compilar por falta de memoría, quitar algunos servicios o programas activos para ganar memoria y reintentar puede ayudar.
docker image prune -f
docker image build --tag user/image_name .
Ejecución
Una vez terminada la compilación con el siguiente comando arrancamos la imagen:
docker container run --init --publish 8888:8888 --detach --volume /home/pi/jupyter:/home/jupyter/notebooks user/image_name
Estamos mapeando el puerto 8888 al mismo en puerto en el host y también estamos mapeando la carpeta en Rasperry como un volumen en el host en la carpeta home del usuario pi, en id_imagen indicamos el id de la imagen a ejecutar. El parámetro –init es un valor recomendado para Jupyter que le proporciona más estabilidad.
Se pueden establecer también las variables de entorno con –env para JUPYTER_ID, NODE_VERSION o TZ o se pueden dejar los valores por defecto.
Push & Pull
Para subir la imagen solo tenemos que ejecutar el siguiente comando e introducir nuestras credenciales de Docker.
docker login
Para subir la imagen usamos la información del repositorio que hemos creado, normalmente usamos como user/name lo mismo que hayamos especificado en el tag que hemos creado con el comando docker build.
docker push user/image_name
Y para descargarlo en otra máquina simplemente hacemos:
docker pull user/image_name
Aunque si lo invocamos directamente con docker run tendrá el mismo efecto.
Configurar Git
Si arrancamos un navegador y vamos a la dirección de la Raspberry podremos ver el entorno de JupyterLab, vamos a configurar Git desde aquí.
La mayoría de las tareas básicas se pueden hacer desde los menús pero para comandos más avanzados podemos abrir un terminal y lanzar comandos desde ahí o para verificar que todo ha sido correcto, por ejemplo vamos a configurar Git con nuestro usuario e email, (nota, pegar se realiza con mayus+Insert).
git config user.email "coyote@acme.com"
git config user.name "El Coyote"
Para evitar que nos pregunte usuario y contraseña para los commit vamos a configurarlo también, desde el terminal hacemos:
git config credential.helper store
git pull
Introducimos el usuario y contraseña cuando lo pregunte, con esto quedarán almacenadas y no lo preguntará para el resto de acciones que hagamos desde la consola o desde la interfaz.
Ahora para clonar el repositorio usamos la interfaz, desde el menú Git -> Clone a Repository introducimos la URL del repo, nos preguntará por unas credenciales que tendremos que tener previamente configuradas en el servidor Git.
Esto nos descargará el código en la carpeta que hemos indicado, ahora podemos probar a crear cualquier archivo y realizar un commit, en la parte de la izquierda pulsemos el icono de Git y veremos los archivos modificados, seleccionamos los que queremos subir y en la parte inferior escribimos un resumen y una descripción opcional, a continuación pulsamos Commit, como ya hemos configurado el nombre y el correo en Git el commit no dará ningún error, sino que saldrá una ventana pidiendo esos datos.
Para hacer Push hay dos formas, se puede hacer directamente desde esta misma ventana usando el icono o desde el menú Git -> Push to remote.
Desde esta misma ventana también podemos cambiar de ramas o ver el historial.
(Opcional) Proxy Apache
Podemos usar Apache como proxy para acceder y así configurar estas URL por algo más fácil de recordar, para ello en Apache creamos un virtual host que nos haga de proxy con el servidor de JupyterLab, creamos un fichero en la carpeta /etc/apache2/sites-available/005-jupyterlab.conf con el siguiente contenido, obviamente deberemos tener un sistema DNS que nos resuelva correctamente el nombre jupyterlab, vamos a suponer que el servidor está en la dirección 192.168.1.4
Tenemos que habilitar algunos módulos, para permitir que funcionen los websockets y cors.
a2enmod headers
a2enmod proxy
a2enmod proxy_http
a2enmod proxy_wstunnel
a2enmod headers
Y ahora configuramos el servidor virtual
<VirtualHost spotify:80>
ServerName jupyterlab.domain
SererAlias jupyterlab
Header set Access-Control-Allow-Origin "*"
ProxyPreserveHost On
ProxyPass / http://192.168.1.4:8888/
ProxyPassReverse / http://192.168.1.4:8888/
<Location "terminals/websocket">
ProxyPass "ws://192.168.1.4:8888/terminals/websocket"
</Location>
<Location "/api/kernels/">
ProxyPass "ws://192.168.1.4:8888/api/kernels/"
</Location>
CustomLog /var/log/apache2/jupyterlab.log combined
ErrorLog /var/log/apache2/jupyterlab.error.log
</VirtualHost>
Para habilitar el sitio ejecutamos:
sudo a2ensite 005-jupyterlab.conf
