整理一下 jupyter notebook 的各种使用方式。

本地部署

直接在本地主机上启动 jupyter notebook 服务是最简单的用法

1
jupyter notebook

需要说明的是:

  • jupyter 服务默认监听本机的 8888 端口,如果本机的 8888 端口已经被占用,在启动时会自动递增,改为 8889 端口等,也可以使用 --port 选项指定端口;
  • jupyter 在启动时会尝试打开本地浏览器,或者手动通过 http://localhost:8888/ 访问即可,可以加上选项阻止自动开启浏览器 --no-browser
  • 默认只允许接收来自 localhost 的请求,可以使用 --ip 选项指定,例如--ip=0.0.0.0 代表允许所有的 ip 访问;
  • 默认的工作目录是执行命令时所处的目录,可以在后面加上工作目录作为位置参数,例如 jupyter notebook /path/to/notebooks,还可以使用 --notebook-dir 选项指定,例如 --notebook-dir=/path/to/notebooks

除了直接在前台运行,在 Linux 中还可以使用下面的操作启动 jupyter 服务并在后台运行,将输出定向到指定的日志中(需要完整路径)

1
nohup jupyter notebook > /path/to/jupyter.log 2>&1 &

jupyter 服务如果在前台运行,直接 ctrl-C 两次即可关闭。

如果 jupyter 服务在 Linux 后台运行,可以先查询 jupyter 相关的进程

1
ps -aux | grep jupyter

然后获取 PID,直接 kill 即可,注意不要与查询命令本身所在的进程混淆

1
kill <PID>

远程服务器部署

现在考虑在服务器上运行的 jupyter notebook,如何部署以支持本地访问。

  • 直接部署
  • 间接部署
    • 在服务器上使用 nginx 反向代理;
    • 在本机上使用 ssh 本地转发。

直接部署

直接部署 Jupyter Notebook 意味着在服务器上启动 Jupyter Notebook 服务并监听端口,默认启动方式如下

1
jupyter notebook

这种做法默认只允许来自服务器 localhost 的访问,外部无法访问。

如果在启动时加上选项 --ip=0.0.0.0,代表允许所有 IP 地址访问。

1
jupyter notebook --ip=0.0.0.0

此时在本机通过 ip 和端口访问 http://<host-ip>:8888/ 即可。

这种方式简单,但可能会带来安全风险(安全性仅靠 Jupyter 自身的密码验证),最好配置防火墙并使用安全的认证方式。

Nginx 反向代理

考虑在服务器上使用 Nginx 作为反向代理 Jupyter 服务,这可以为 Jupyter 提供更好的安全性和可配置性。

下面是 nginx 配置文件参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
server {
# listen 80;
listen 12139 ssl;
server_name jupyter.example.xyz;

ssl_certificate /path/to/ssl/jupyter.example.xyz_bundle.crt;
ssl_certificate_key /path/to/ssl/jupyter.example.xyz.key;
ssl_session_timeout 5m;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;

location / {
proxy_pass http://localhost:8888;

proxy_set_header Host $host;
proxy_set_header X-Real-Scheme $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

proxy_read_timeout 120s;
proxy_next_upstream error;
proxy_redirect off;
proxy_buffering off;

# 设置跨域头部
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Allow-Credentials' 'true';

# 跨域配置
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
}
}

解释:

  • 域名 jupyter.example.xyz,启用 ssl,需要提供对应的 SSL 证书
  • 监听服务器 localhost 的 12139 端口(不是默认的 443 端口,因此不可省略),转发到服务器 localhost 的 8888 端口的 jupyter notebook/jupyter lab 服务
  • 这里还涉及到跨域问题,因此进行了相应设置

在服务器上正常启动

1
jupyter notebook

在本地通过服务器域名加端口号即可通过浏览器访问,例如访问 https://jupyter.example.xyz:12139 即可。

这里需要修改 jupyter 服务的如下配置项,配置文件的生成见下文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# jupyter_notebook_config.py

c.NotebookApp.allow_origin = '*'
c.NotebookApp.allow_remote_access = True
c.NotebookApp.notebook_dir = '/home/user/notebooks/'
c.NotebookApp.open_browser = False
c.NotebookApp.password = ****************************
c.NotebookApp.token = ''

# jupyter_lab_config.py

c.ServerApp.allow_origin = '*'
c.ServerApp.allow_remote_access = True
c.ServerApp.notebook_dir = '/home/user/notebooks/'
c.ServerApp.open_browser = False
c.ServerApp.password = ****************************
c.ServerApp.token = ''

解释一下这里的内容:

  • allow_origin 允许所有来源(处理跨域请求问题)
  • allow_remote_access 允许远程访问,默认为 false,这代表 jupyter 会检查 HTTP 请求的 Host 头部,如果它指向一个非本地的地址就会返回 403,这里我们需要支持通过服务器域名和 ip 的访问,必须设置为 true。
  • notebook_dir 默认工作目录
  • open_browser 是否尝试开启浏览器
  • password token 这两个与访问控制有关,这里的含义是提供固定的密码,禁用 token,具体细节见下文

注意:

  • 这两组配置是相似的却不完全一样的,在版本更新(包括从 notebook 到 lab 的大升级),有的选项可能被舍弃,进而名称发生变化。
  • allow_originallow_remote_access 是接收外部访问的相关网络配置,具体我也不太理解,这里不需要设置监听 ip 是因为通过 nginx 进行了反向代理,下文中的 ssh 本地转发不需要设置这两项,但是需要设置监听的 ip。

ssh 本地转发

相比前几种做法,在本机上使用 SSH 本地转发的做法可能最为安全,因为它只能通过 SSH 隧道访问。

假设 jupyter notebook 在服务器 C 启动,监听 8888 端口

1
jupyter notebook

本地主机 A 可以 ssh 正常访问到服务器 C,那么在本地主机 A 执行如下命令(不要关闭这个会话,或者加上特定选项,使隧道即使在会话结束后仍然保持连接)

1
ssh -N -L 8989:localhost:8888 user@hostname

此时本地主机 A 就可以通过浏览器访问 http://localhost:8989/,这实际上会访问到服务器 C 的8888端口,可以使用服务器上的 jupyter notebook 服务。(前面是本地访问端口,后面是服务器上开放的端口

值得注意的是,我们没有修改 ip 选项,此时 jupyter 服务仍然只接收来自服务器 localhost 的请求,为什么上述做法可行? 因为 ssh 本地转发大致可以理解为:

  • 本地 ssh 服务监听本地的 8889 端口
  • 本地 ssh 将数据发送到服务器的 22 号端口(或者其它端口,取决于 ssh 的配置)
  • 服务器的 sshd 服务将收到的数据转发到本地的 8888 端口

因此 jupyter 服务接收的仍然是来自服务器 localhost 的请求。

在本地的 vscode 中使用 jupyter 插件,使用 http://localhost:8888/ 加上 token 或密码也可以访问到对应的服务器上的 jupyter 内核。 使用 jupyter 内核与直接访问 jupyter notebook 的做法略有不同:

  • 使用 jupyter notebook 时,ipynb 文件等存储在服务器 C 上;
  • 使用 jupyter 内核时,ipynb 文件等仍然存储在本地主机 A,但是运行时会调用服务器 C 的 jupyter 服务进行计算。(这其实不好用,因为影响本地的代码提示)

补充

容器中启动 jupyter 服务

如果 jupyter notebook 服务是在 docker 容器中启动的,那么我们还需要额外考虑两个因素:

  • 权限问题:处于安全原因默认禁止 root 用户启用 jupyter 服务,但是在容器中通常直接使用 root 用户,因此需要在启动时加上 --allow-root 选项;
  • 网络问题:需要考虑容器和宿主服务器之间的网络关系,即使我们使用 -p 8888:8888 让宿主机的 8888 端口映射到容器的8888端口,但是对于容器来说这仍然不是来自 localhost 的请求,解决办法包括:
    • 可以在 jupyter 服务启动时加上选项 --ip=0.0.0.0,允许所有 IP 地址访问,但是这就和直接部署的效果一样了,不够安全;
    • 可以在 docker 容器启动时使用 --net=host 选项,直接使用宿主机的网络,此时端口映射也没有意义了,-p 选项会被忽略;(docker 处理网络时的默认做法是桥接模式)

还需要特别注意的是,默认情况下的 UFW 防火墙对 docker 容器无效,因为 UFW 只是对 iptables 操作的简化封装,而 docker 实际上会绕过了 UFW 直接修改 iptables,如果有特殊的网络配置需求,可能需要进行一定的处理,阻止其修改 iptables。

配置文件

jupyter notebook 服务并不会自动生成配置文件,可以用如下命令显示生成配置文件

1
jupyter notebook --generate-config

生成的配置文件为 ~/.jupyter/jupyter_notebook_config.py,其中包含了所有的默认选项,可以根据需要进行修改。 前面提到的很多命令行选项在配置文件中都有对应的项,并且显然,命令行参数的优先级比配置文件更高。

除此之外,jupyter 还提供了 ~/.jupyter/jupyter_notebook_config.json 这个配置文件,json 版本的配置文件相比于 py 版本的有如下区别:

  • json 版本的配置文件只是静态配置,py 版本的配置文件本质上就是个 py 脚本,可以有更丰富的行为;
  • 两者同时存在时,json 版本的配置优先级更高。

token 和密码

Jupyter 支持使用 token 或密码进行访问控制,可以单独或同时启用,甚至关闭这两者(不推荐)。

在启动 Jupyter Notebook 时:

  • 如果同时设置了 token 和密码:优先要求 token,登录后可以设置密码。
  • 如果只有 token:启动时会生成一个随机 token,必须带着 token 访问。
  • 如果只有密码:直接通过密码登录,无需 token。
  • 如果两者都禁用:不需要任何验证,但完全暴露在网络上,极不安全。

Jupyter 默认开启 token 认证,在启动信息中会提示如下片段

1
2
3
4
5
To access the server, open this file in a browser:
file:///C:/Users/xxxx/AppData/Roaming/jupyter/runtime/jpserver-30804-open.html
Or copy and paste one of these URLs:
http://localhost:8888/tree?token=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
http://127.0.0.1:8888/tree?token=xxxxxxxxxxxxxxxxxxxxxxxxxxxx

在浏览器中访问时,第一次可能需要手动输入 token,后续不再需要。

在配置文件中修改如下项可以手动禁用 token

1
c.ServerApp.token = ''

关于密码的生成有很多种方式,一种最常见的方式是在浏览器中远程访问时,第一次需要提供token,此时可以根据页面提示设置密码。

除此之外,还可以直接将密码(的哈希值)写入到配置文件中,例如在 Python 中执行如下命令

1
2
from notebook.auth import passwd
passwd()

输入密码,它会返回密码的 hash 值,把它填写到配置文件的 password 项,这可以避免在配置文件中明文存储密码。

1
c.ServerApp.password = xxxxx

在较新版本的 jupyter 中,可以有更简单的做法

1
jupyter notebook password

根据提示输入密码即可,jupyter 会自动将密码的 hash 值保存到配置文件。

到这里我们也注意到了,jupyter notebook 和 jupyter lab 只是面向单用户的服务,如果需要多用户服务,可以选择 Jupyterhub,但是并没有试过,面向多用户的服务有些额外的问题。

jupyter 切换内核

默认情况下,我们启动的 jupyter notebook 使用的是它所处的默认环境,例如在 xxx 环境下安装的 jupyter,启动后对应的环境就是 xxx。 但是 jupyter 支持通过 GUI 操作直接切换环境(在 jupyter 语境中是切换内核),需要确保将要切换的所有环境中都有 ipykernel 包。(如果没有下载即可)

我们可以直接通过命令行查看 jupyter 当前可以找到的内核列表

1
jupyter kernelspec list

输出结果通常是当前环境中的 python 路径,也可能含有其它环境中的 python 路径,此时就支持切换到对应的内核。

如果没有包含我们需要的内核,就需要手动注册,切换到对应的环境中执行如下命令(建议加上 --user 参数限于当前用户)

1
python -m ipykernel install --user --name myenv --display-name "Python (myenv)"

注册时提供的 myenvPython (myenv) 不需要是 conda 环境名称,只是 jupyter 用来标识内核以及显示用的名称,当然习惯上会取对应的名称。

我们还可以删除不需要的内核

1
jupyter kernelspec uninstall myenv

这些操作在 Linux 下的底层细节都在配置目录 ~/.local/share/jupyter 中,例如名称为 myenv 的内核对应的目录和配置文件为

1
2
~/.local/share/jupyter/kernels/myenv
~/.local/share/jupyter/kernels/myenv/kernel.json

其中 kernel.json 文件的内容大致为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"argv": [
"/path/to/python",
"-Xfrozen_modules=off",
"-m",
"ipykernel_launcher",
"-f",
"{connection_file}"
],
"display_name": "Python (myenv)",
"language": "python",
"metadata": {
"debugger": true
}
}

VSCode 的 jupyter 插件不需要手动配置内核,它可以自动识别所有可用的内核并支持内核切换。