用Docker部署自己的JupyterHub

摘要:
[文章索引]包装JupyterHub图像。配置JupyterHub启动参数。启动JupyterHub以隔离JupyterHub/JupyterLab网络。[1]打包JupyterHub图片]JupyterHub架构的介绍和原理在官方文件中非常清楚。只有最基本的身份验证和管理(如在Linux下通过PAM进行身份验证、通过本地进程运行JupyterLab等)可用。

【话在前头】

用 Docker 部署 JupyterLab 感觉是部署 JupyterLab 最方便的方式了,官方提供了很多可选的镜像,也可以自己从 jupyter/base-notebook 中继续打包,镜像启动命令加上“--NotebookApp.password”就可以直接用密码登录用了。虽然只是自己一个人用,但是如果放在互联网上访问的话,总感觉不是那么安全,还是希望能像其他服务一样,能独立管理用户信息,能设置二次验证(2FA)。不过搜了下网上关于 JupyterHub 的资料比较少,甚至于官方的说明文档写的也不是很详细,有些配置和参数只能去源码里扒。

【文章索引】

  1. 打包 JupyterHub 镜像
  2. 配置 JupyterHub 启动参数
  3. 启动 JupyterHub
  4. 隔离 JupyterHub/JupyterLab 网络

【一、打包 JupyterHub 镜像】

JupyterHub 架构的介绍和原理官方文档中描述的非常清楚了,这里不再赘述了,简单说就是 JupyterHub 把 认证 和 单用户 JupyterLab 的管理 分别拆成了 Authenticator 和 Spawner 模块,可以根据不同的需要配置不同的认证方式或管理方式。不过官方的 JupyterHub 镜像只包含了 JupyterHub 项目 本身,只有最基本的认证和管理(如通过 Linux 下 PAM 进行认证、通过本地进程运行 JupyterLab 等)。如果想通过自定义账号密码、并且开启 2FA 的话,JupyterHub 其实也已经实现了一个官方的 NativeAuthenticator 模块,官方文档还是比较详细的,默认用户信息存储在 JupyterHub 的 sqlite 数据库中,可以通过数据源配置改成 Mysql,如果需要连接 Mysql 的话,官方的镜像也不包含相关模块,也需要自行安装。

除此之外,如果 JupyterHub 管理的 JupyterLab 也想在 docker 中运行的话,还需要使用官方提供的 DockerSpawner 进行管理,不过官方文档不是特别详细,好在代码不多,扒扒代码也能看明白具体应该怎么配置。

所以,如果我们需要实现能独立管理的用户信息、支持2FA、使用Mysql数据库存储用户数据,用户的 JuyterLab 也通过 docker 镜像进行运行和管理的话,我们可以通过如下的 Dockerfile 在官方镜像之上打一个更完整的镜像。

 1 ARG BASE_IMAGE=jupyterhub/jupyterhub:1.2
 2 FROM $BASE_IMAGE
 3 
 4 LABEL maintainer="MaysWind <i@mayswind.net>"
 5 
 6 # Install Dependencies
 7 RUN apt-get update 
 8   && apt-get install -y --no-install-recommends unzip 
 9   && rm -rf /var/lib/apt/lists/* 
10   && rm -rf /tmp/*
11 
12 # Install Mysql
13 RUN pip3 --no-cache-dir install mysql-connector 
14   && rm -rf /tmp/*
15 
16 # Install NativeAuthenticator
17 RUN curl "https://github.com/jupyterhub/nativeauthenticator/archive/master.zip" -L -o /tmp/nativeauthenticator.zip 
18   && unzip /tmp/nativeauthenticator.zip -d /tmp 
19   && mv /tmp/nativeauthenticator-master /usr/local/bin/nativeauthenticator 
20   && pip --no-cache-dir install -e /usr/local/bin/nativeauthenticator 
21   && rm -rf /tmp/*
22 
23 # Install DockerSpawner
24 RUN pip --no-cache-dir install dockerspawner 
25   && rm -rf /tmp/*

注:写这篇博客的时候,JupyterHub 的最新 Release 版本是 1.1.0,但是 1.1.0 的 docker 镜像存在问题(静态资源没有编译等),所以这里使用的是还在开发中的镜像(1.2 tag 目前与 1.2.0dev tag 一致)。

【二、配置 JupyterHub 启动参数】

打完镜像后后其实就可以启动了,不过通常还有些配置需要调整下。我通过 docker-compose 启动 JupyterHub 容器,所有配置参数都通过参数或环境变量进行配置,同时由于 JupyterHub 在 docker 容器中,还需要把宿主机的 docker.sock 挂载到容器内,以便 JupyterHub 能够管理 JupyterLab 容器。并且为 JupyterHub 和之后的 JupyterLab 建了一个单独的网络,方便之后对 JupyterLab 的请求进行隔离,如果没有需求的话实际上按默认的网络配置也是可以的,相关的 yml 示例配置如下

 1 version: "2"
 2 networks:
 3   jupyter-network:
 4     driver: bridge
 5     ipam:
 6       config:
 7         - subnet: 192.168.254.0/24
 8           gateway: 192.168.254.1
 9 services:
10   jupyterhub:
11     image: 你的 JupyterHub 镜像名称 
12     container_name: jupyterhub
13     hostname: "jupyterhub"
14     networks:
15       - "jupyter-network"
16     command:
17       - "jupyterhub"
18       - "--JupyterHub.hub_bind_url='http://:8081'" # JupyterHub 默认绑定 127.0.0.1,需要改成绑定所有 IP 使 JupyterLab 能跨容器访问
19       - "--JupyterHub.db_url='mysql+mysqlconnector://Mysql用户名:Mysql密码@数据库地址/数据库名称'" # 设置 Mysql 数据库,如果使用默认 Sqlite,可以挂载目录到 /srv/jupyterhub 实现数据库持久化
20       - "--JupyterHub.authenticator_class='nativeauthenticator.NativeAuthenticator'" # 使用 NativeAuthenticator
21       - "--JupyterHub.spawner_class='dockerspawner.DockerSpawner'" # 使用 DockerSpawner
22       - "--JupyterHub.admin_access=True" # 启用管理员功能
23       - "--Authenticator.admin_users={'管理员账户名称'}" # 管理员名称
24       - "--Authenticator.allow_2fa=True" # 开启 2FA 功能
25       - "--DockerSpawner.remove_containers=True" # 每次启动 JuypyterLab 容器时都删除之前的容器,如果通过 docker-compose 设置的网络,docker-compose 重新配置网络后一定要重新创建容器才能启动
26       - "--DockerSpawner.notebook_dir='/home/jovyan/work'" # 设置笔记本默认目录(默认是 ~)
27       - "--DockerSpawner.image='你的 JupyterLab 镜像名称'"
28       - "--DockerSpawner.network_name='JupyterLab 网络名称'" # 如果是通过 docker-compose 设置的网络,与第3行可能不一致,需要通过 docker network ls 查看
29       - "--DockerSpawner.args=['--Application.log_level=WARN']" # 设置日志默认输出级别
30       - "--DockerSpawner.environment={
31              'JUPYTER_ENABLE_LAB': 'yes' # 开启 JupyterLab
32            }"
33       - "--DockerSpawner.volumes={
34              '/etc/localtime': {'bind': '/etc/localtime', 'mode': 'ro'},
35              '本机 Jupyter 笔记存储路径': '/home/jovyan/work'  # 可以使用 “{username}” 占位,表示用户名,如 '/mnt/data1/jupyter/{username}/work': '/home/jovyan/work'
36            }"
37     volumes:
38       - "/etc/localtime:/etc/localtime:ro"
39       - "/var/run/docker.sock:/var/run/docker.sock"
40     restart: on-failure

其中,JupyterHub 配置文件中的配置都可以通过启动参数的方式进行配置,如上述配置中 command 中的配置项,所有 JupyterHub 配置可以参考官方文档。对于 NativeAuthenticator,也额外提供了一些其他参数,如自己注册完账号,可以设置“Authenticator.open_signup”参数为 False,关闭开放注册功能,“Authenticator.ask_email_on_signup” 注册时需要提供邮箱账号等,这些参数可以如上附到启动参数中,或者也可写入到配置文件中,更多参数和用法可以参考官方文档。对于 DockerSpawner,有些参数是实现了基础类 Spawner 中定义的,可以查阅 Spawner 的定义文档 进行配置,也有部分是其本身单独实现的,可以查阅其源代码,例如其支持限制内存 “DockerSpawner.mem_limit”、限制CPU “DockerSpawner.cpu_limit”等参数,都是实现基础类 Spawner 中定义的,Docker 网络名称 “DockerSpawner.network_name ”、启动容器前删除已有容器的参数 “DockerSpawner.remove_containers” 等都是其本身自己实现的。

如果之前也是通过 docker 部署的 JupyterLab,可能下述几个参数能迁移大部分之前的个性化配置,

  • DockerSpawner.args 可以追加 JupyterLab 容器的启动参数,默认启动命令是“start-notebook.sh --ip=0.0.0.0 --port=8888”,可以追加多个参数(如上述设置了配置了日志输出级别为WARN,JupyterLab 配置文件中的配置都可以使用此方式进行配置,相关配置可以参考官方文档),参数格式是 python 的 dict。
  • DockerSpawner.environment 可以设置 JupyterLab 容器的环境变量,如上述设置了开启 JupyterLab 功能,容器所有环境变量可以参考官方文档,参数格式是 python 的 dict。
  • DockerSpawner.volumes 可以设置 JupyterLab 容器的挂载配置,提供了两种配置方式(读写模式:'source_path': 'target_path',或自定义读写模式(如只读):'source_path': {'bind': 'target_path', 'mode': 'ro'}),格式是 python 的 dict。

【三、启动 JupyterHub】

根据第二步的配置,就可以通过 docker-compose 或者其他方式启动 JupyterHub 的 docker 镜像了,只不过很有可能会失败,主要是由于 NativeAuthenticator 对 Mysql 的兼容性问题,用于管理注册用户信息的那张表没有自动创建成功,不过我们可以帮他完成这个任务,即编写类似如下的SQL(具体存储引擎、编码可以根据自己实际情况调整)。

CREATE TABLE `users_info` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) NOT NULL,
  `password` blob NOT NULL,
  `is_authorized` bit(1) DEFAULT NULL,
  `email` varchar(255) DEFAULT NULL,
  `has_2fa` bit(1) DEFAULT NULL,
  `otp_secret` varchar(16) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

创建完 users_info 表后 JupyterHub 应该就能正常启动了,接下来就可以自己进行注册账号了,如果没有关闭开放注册功能或者注册的账号名在配置中的管理员用户名中的话,账号直接就可以登录,否则需要自行去数据库中找到自己注册的记录,并将 “is_authorized” 字段设置为1。

登录后应该会默认启动 JupyterLab,或者也可以自行选择启动,启动成功后会自动跳转到 JupyterLab,下次访问时直接就会访问 JupyterLab,而不会再显示 JupyterHub 的界面了。如果启动失败,也可以通过 docker 查看 JupyterLab 的容器情况。

【四、隔离 JupyterHub/JupyterLab 网络】

JupyterLab 里什么都能干,能执行代码,能运行脚本,总觉得部署了 JupyterLab 后,直接把内网环境对外打开了,所以还想再对 JupyterHub/JupyterLab 的网络进行隔离,不允许其访问内网。这块通过 iptables 就可以实现,比如上述我定义了 jupyter-network 网络,IP 是 192.168.254.0/24,我内网 IP 是 192.168.1.0/24,路由(网关)是 192.168.1.1,所以我在宿主机上定义如下 iptables,禁止来自 jupyter-network 的 IP 请求内网 IP(但允许通过路由访问互联网)。当然,如果 Mysql 服务器不与 JupyterHub/JupyterLab 在一台宿主机上的话,别忘了允许 JupyterHub 的 IP 地址访问 Mysql 端口。

iptables -I DOCKER-USER -s 192.168.254.0/24 -d 192.168.1.0/24 -j DROP
iptables -I DOCKER-USER -s 192.168.254.0/24 -d 192.168.1.1 -j ACCEPT

此外,如果宿主机上还有其他服务或 docker 实例,如果需要禁止 JupyterHub/JupyterLab 访问他们,还需要再定义一条

iptables -I INPUT -s 192.168.254.0/24 -p tcp -j DROP

这样,应该就相对安全了一些吧。

免责声明:文章转载自《用Docker部署自己的JupyterHub》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Java单元测试 Http Server Mock框架选型eMMC基础技术6:eMMC data读写下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

读vue源码笔记(1)

shared/util.js 1.hasOwn var hasOwnProperty = Object.prototype.hasOwnProperty; function hasOwn (obj, key) { return hasOwnProperty.call(obj, key) } extend  export function exten...

微信小程序setdata修改数组或对象

1、this.setdata修改数组的固定一项的值 changeItemInArr: function() { this.setData({ 'arr[0].text':'changed data' }) }, 2、动态修改数组某一项的值 changeItemInArr: function(index) { let...

存储过程,存储函数(Oracle)

存储过程和存储函数 指存储在数据库中供所有用户程序调用的子程序叫存储过程、存储函数。 存储过程和存储函数的区别? 存储函数:可以通过return 语句返回函数值。 存储过程:不能 除此之外我们可以认为他们是完全一样的。   存储过程 1、创建存储过程 用create procedure命令简历存储过程。 语法: create [or replace] pr...

easyui grid 本地做分页

背景: 有的数据不是很多,但是有分页的需求,这个时候后台往往没有做分页,我们是一次请求了所有的数据。 代码: dataSource 为 grid 里的数据源 html部分: <table id="costtype"></table>    js部分: //费用类型 grid $('#costtype').datagrid({  ...

SpringMVC注解式开发之接收请求参数

逐个接收(涉及注解@RequestParam) index.jsp的name必须和后端的名字一致,第一种才可以实现。   以对象形式整合接收   域属性参数的接收   数组或集合参数的接收 restfull风格传参(涉及注解@PathVariable) 接收json字符串(涉及注解@RequestBody,注册mvc注解驱...

django类视图的使用

1 类视图引入 以函数的方式定义的视图称为函数视图,函数视图便于理解。 但是遇到一个视图对应的路径提供了多种不同HTTP请求方式的支持时,便需要在一个函数中编写不同的业务逻辑,代码可读性与复用性都不佳。 def register(request): """处理注册""" # 获取请求方法,判断是GET/POST请求 if requ...