基于Nginx反向代理配置带SSL的云端Jupyter Lab环境
折腾了一下,用Nginx弄了一个Jupyter Lab环境,这里记录一下过程。
最终达成的效果:Nginx反向代理,在一个Linux云服务器上实现对Jupyter Lab的HTTPS公网访问,并利用systemd实现Jupyter环境的自动启动。访问的接口是一个二级域名jupyter.eslzzyl.eu.org
,本文也会涉及使用acme.sh工具为二级域名配置SSL证书的流程。
需要准备的资源
- 一台Linux云服务器,需要带有一个公网IP,并能够通过SSH连接;
- 一个域名。可以到这里申请免费的eu.org域名。我在本文中就使用了这样的一个域名。
安装环境
我用Miniconda安装Jupyter,这样比较方便管理虚拟环境。所以先安装Miniconda。当然,Anaconda也是可以的。
本文不会过多介绍conda
工具的使用方法,要了解使用方法,可以查阅其他资料。
安装Miniconda
官方的安装文档:https://docs.conda.io/projects/miniconda/en/latest/miniconda-install.html
到这里下载最新的Miniconda安装包。
因为我的服务器是x64的,所以选择第一个,右键点击复制链接。
通过SSH连接服务器,下载刚刚复制的链接:
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
然后执行安装脚本:
bash Miniconda3-latest-Linux-x86_64.sh
如果没有执行权限,尝试执行chmod +x Miniconda3-latest-Linux-x86_64.sh
后再执行安装脚本。
首先脚本会要求你同意一个License,按下回车键查看License,然后按f
键若干次,直到看到Do you accept the license terms? [yes|no]
的字样。输入yes,回车。之后脚本要求你确认安装位置,保持默认即可,按回车,等待安装完成。安装完成后,重新登陆shell(可以重连一下服务器),来让更改过的环境变量生效。之后就可以执行conda
命令了。
默认设置下,在你登录shell之后,conda会自动激活base环境,这个行为可以更改。在安装过程的最后,脚本会提示你使用下面的命令来禁用这个自启动:
conda config --set auto_activate_base false
默认激活base环境会极大地拖慢shell的启动速度,因此我建议禁用这个自启动行为。该命令也会在你的主目录下新建conda配置文件.condarc
。关闭自动启动后,你可以通过conda activate base
来激活base环境,并通过conda deactivate
来退出base环境。
完成安装后,可以执行conda update --all
来更新所有的包。如果你的服务器在境内,最好先换个软件源。
后续可以使用conda install [package]
来安装新的包。
安装Jupyter
激活base环境(或者你也可以选择新建一个虚拟环境),然后使用如下的命令安装Jupyter:
conda install jupyter
安装之后,重新激活一下base环境(先deactivate
再activate
),来让相关的环境变量生效。此时你应该可以使用jupyter
命令了。
配置Jupyter环境
先执行
jupyter notebook password
根据提示输入两遍密码(由于这个密码是外网和你的Shell之间的唯一屏障,建议使用较复杂的密码),然后程序会提示你,已经将哈希过的密码写入了~/.jupyter/jupyter_notebook_config.json
。该文件大致是下面这个样子:
{
"NotebookApp": {
"password": "argon2:$argon2id$v=19$m=10240,t=10,p=8$HIxxxxxxxxxxxxxxxxjW6Q$/h1AxxxxxxxxxxxXgvsf13Ieegxxxxxxxxxxxxx+NTU"
}
}
复制password
字段的密码。
然后执行
jupyter notebook --generate-config
来让Jupyter生成配置文件。配置文件的默认位置在~/.jupyter/jupyter_notebook_config.py
。
打开这个文件,搜索下面这些配置项,取消它们的注释并将值修改为对应的内容:
c.NotebookApp.password
即Jupyter的Web服务的登陆密码,填入你刚刚记下的密码:
c.NotebookApp.password = u'argon2:$argon2id$v=19$m=10240,t=10,p=8$HIxxxxxxxxxxxxxxxxjW6Q$/h1AxxxxxxxxxxxXgvsf13Ieegxxxxxxxxxxxxx+NTU'
c.NotebookApp.port
即Jupyter的监听端口,默认值是8888。如果不介意保持默认,跳过这条即可。
c.NotebookApp.allow_remote_access
是否允许远程访问,默认为False
。这一项必须设置成True
,即使有反向代理,也必须设置成True
。
c.NotebookApp.allow_root
是否允许用户以超级用户权限执行命令,默认为False
。可改可不改,主要是影响Jupyter Lab中Terminal的使用。我设置成True
了。
注意,这一项设为True
,则前面的密码就至关重要。如果攻击者拿到了密码,通过Web登入Jupyter Lab环境,那么你的服务器就完全沦陷了。
c.NotebookApp.open_browser
是否在Jupyter服务端启动后自动打开浏览器,默认值为True
。headless服务器当然不需要浏览器,因此可设置为False
。
配置完毕后,保存文件,然后运行
jupyter lab
查看是否能正常启动服务端。没什么问题后,按Ctrl+C结束进程。
利用systemd自动化Jupyter服务端程序
本节参考了这篇文章
在/etc/systemd/system/
中新建一个jupyter.service
文件(记得用sudo),然后填入以下内容,同时将[UserName]
替换为你的用户名:
[Unit]
Description=Jupyter Lab
After=syslog.target network.target
[Service]
User=[UserName]
Environment="PATH=/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/home/[UserName]/.local/bin"
WorkingDirectory=/home/[UserName]/.jupyter/
ExecStart=/home/[UserName]/miniconda3/bin/jupyter lab --no-browser
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
注意其中的ExecStart
字段。如果你像我一样,将Jupyter安装在conda的base环境中,那么不需要修改该字段;如果你新建了一个虚拟环境,那么Jupyter的位置可能在/home/[UserName]/miniconda3/envs/[EnvName]/bin/
中。注意修改。
修改好后,保存文件,然后执行
sudo systemctl daemon-reload
来重载服务。
之后,就可以使用systemctl
命令来管理Jupyter了:
sudo systemctl start jupyter # 启动jupyter
sudo systemctl stop jupyter # 停止jupyter
sudo systemctl restart jupyter # 重启jupyter
sudo systemctl enable jupyter # 启用jupyter开机自启动
sudo systemctl disable jupyter # 禁用jupyter开机自启动
sudo systemctl status jupyter # 查询jupyter服务的状态
域名和Nginx设置
我的域名是eslzzyl.eu.org
,主域名带有SSL证书。我这里为Jupyter服务单独设置了一个子域名jupyter.eslzzyl.eu.org
,这种子域名需要签署新的SSL证书。如果你懒得费事,可以把Jupyter放在主域名下面,比如eslzzyl.eu.org/lab
这样(查阅其他资料来完成)。下面介绍一下子域名的设置方法。
注意要先设置DNS,再签署SSL证书。否则验证服务器找不到DNS解析记录,会验证失败。
配置域名DNS
选择一个DNS供应商,它们大多都提供免费服务。我这里选用腾讯系的DNSPod。
添加你的域名,由于我在申请eu.org域名时已经将域名添加到了DNSPod,因此这步跳过了。
然后为你的域名添加一条解析记录。记录类型选A(指向一个IPv4地址),主机记录填jupyter
,IPv4地址填写你服务器的公网IP。如果有其他选项,保持默认即可。
等待几分钟,ping一下刚刚设置的二级域名(如jupyter.eslzzyl.eu.org
),看看能否正常解析到IPv4地址。
SSL证书
SSL证书允许我们的网站支持HTTPS访问。如果不配置SSL证书,那么大多数现代浏览器都会提示网站“不安全”。
一些DNS供应商(如腾讯、阿里等)都提供有免费的SSL证书申请服务,但可供申请的证书数量是有限的(如DNSPod限每个账户仅能为外部域名——即非腾讯管理的域名——配置不超过20张SSL证书),而且需要手动部署,比较麻烦。
ACME协议(见此处、此处和此处)的出现极大地简化了SSL证书的签署流程。现在我们可以通过一些自动化脚本来完成证书的申请、部署和(自动)续期,申请是完全免费且没有数量限制的。我在本节使用了acme.sh这个工具。
以下内容参考了这个说明。
安装acme.sh
curl https://get.acme.sh | sh -s email=my@example.com
注意把邮箱改成自己的。安装位置是~/.acme.sh/
,安装脚本会自动创建一个shell的alias,允许你通过acme.sh
命令来调用位于~/.acme.sh/
目录中的acme.sh
脚本文件。同时还会创建自动任务,每天零点检查所有的证书,如果有快到期的证书,就自动刷新。
该脚本所有的修改都限制在~/.acme.sh/
,不会污染其他系统文件。
签发证书
申请证书时,必须证明你是域名的持有者。可以选择http验证和DNS验证两种方式。我们这里选http验证。
http验证需要你在网站的根目录下放置一个文件,来验证域名的所有权。acme.sh可以自动化这个过程:自动创建文件、自动完成验证、自动删除文件。我们只需要指定网站根目录即可。
按照计划,对jupyter.eslzzyl.eu.org
域名的访问将会被反向代理到服务器内网的Jupyter服务,属于Web API,当然没有根目录一说。但是我的主域名eslzzyl.eu.org
是有根目录的,并且我在Nginx中配置过了。于是我们通过下面的命令来签署证书:
acme.sh --issue -d jupyter.eslzzyl.eu.org --webroot /home/eslzzyl/www/homer/dist
另外,我们可以指定acme.sh在签署证书时,到Nginx的配置文件(默认位置是/etc/nginx/nginx.conf
)中自动查找域名的根目录:
acme.sh --issue -d eslzzyl.eu.org --nginx
此时acme.sh会尝试在Nginx配置文件中查找eslzzyl.eu.org
这个域名配置的根目录。但是,我们要配置的域名是jupyter.eslzzyl.eu.org
,这是一个反向代理域名,在Nginx配置文件中没有配置根目录,因此这种办法是行不通的。在做静态的Web网站时,可以使用这种方法。
于是我们得到了证书。
部署证书
acme.sh签署的所有证书都保存在~/.acme.sh
目录中。为了之后自动刷新证书,我们应该指定acme.sh将证书拷贝到需要的位置(不要手动cp
这些证书文件)。
注意,也不要直接在Nginx配置中将证书的链接指向它们在~/.acme.sh
目录中的原始位置,因为这个目录的结构可能会变化。
我在/etc/nginx
中建立了一个cert
目录,将服务器的所有SSL证书都放在此处。于是使用命令:
acme.sh --install-cert -d example.com \
--key-file /etc/nginx/cert/jupyter.eslzzyl.eu.org.key \
--fullchain-file /etc/nginx/cert/jupyter.eslzzyl.eu.org.crt \
--reloadcmd "sudo nginx -s reload"
文件名可以任意指定,只要在Nginx配置文件中进行对应的修改即可。写/etc
目录需要超级用户权限,因此可以先切换到root用户再执行上面的命令。root用户没有acme.sh这个alias了,于是不得不用/home/eslzzyl/.acme.sh/acme.sh
这个完整路径。如果你不想这么麻烦,可以把证书放在自己的个人目录,然后在Nginx配置文件中进行相应的修改。
查看证书情况
通过
acme.sh --info -d example.com
命令可以查看指定域名的证书情况。
其他
确认你的系统中安装有cronjob,从而支持证书的自动刷新:
crontab -l
# 输出应该类似下面这样
56 * * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/null
启用acme.sh的自动更新:
acme.sh --upgrade --auto-upgrade
配置Nginx
编辑/etc/nginx/nginx.conf
文件,在其中的http
块中添加一个server
块:
server {
listen 443 ssl;
client_max_body_size 50M;
server_name jupyter.eslzzyl.eu.org; # 把域名改成自己的
ssl_certificate ./cert/jupyter.eslzzyl.eu.org.crt; # 根据上面acme.sh拷贝的证书位置修改
ssl_certificate_key ./cert/jupyter.eslzzyl.eu.org.key; # 根据上面acme.sh拷贝的证书位置修改
ssl_session_timeout 5m;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
ssl_prefer_server_ciphers on;
location / {
proxy_ssl_server_name on;
proxy_pass http://127.0.0.1:8888; # 如果你改了Jupyter的监听端口,这里要对应修改
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket配置,对Jupyter服务来说是必不可少的,否则会连不上kernel
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_connect_timeout 60s;
proxy_read_timeout 500s;
proxy_send_timeout 500s;
}
}
其中关于SSL的配置,参考了腾讯云的文档;关于WebSocket的配置,参考了这篇文章。WebSocket配置是必不可少的,否则Jupyter会一直连不上kernel。我在这个地方也卡了一阵子。
如果你不采用二级域名的配置方式,而是直接把Jupyter服务挂在主域名下面,那么只需要在主域名对应的server
块中添加上面配置的location
部分即可。记得把location
后面改成/jupyter
或者/lab
之类。
保存文件,执行
sudo nginx -s reload
刷新配置文件。
测试
使用
sudo systemctl status jupyter
查看Jupyter的运行状态。正常状态应该类似下面这样:
然后在本地用浏览器访问jupyter.eslzzyl.eu.org/lab
或者jupyter.eslzzyl.eu.org
(后者会被自动重定向到前者)。
首次登陆需要输入先前设置的密码:
输入密码后,进入Jupyter Lab主页:
最后建议确认一下能否正常连上kernel。随便找一个目录,新建一个Notebook,然后随便输入一点代码,运行一下看看:
左下会显示kernel的状态。就绪状态的kernel应该显示为“Idle”(空闲)。
单击这个标签,可以切换kernel。
最后,Jupyter Lab还可以打开终端(File - New - Terminal)。在一些没有SSH的机器上,可以通过这种方式来在HTTPS协议上使用终端。
本文到这里就结束了。后续我可能会根据实践情况添加一些内容。
【更新】看到一个不错的主题 https://github.com/catppuccin/jupyterlab