基于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安装包。

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环境(先deactivateactivate),来让相关的环境变量生效。此时你应该可以使用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,然后随便输入一点代码,运行一下看看:

尝试运行一个notebook

左下会显示kernel的状态。就绪状态的kernel应该显示为“Idle”(空闲)。

kernel idle

单击这个标签,可以切换kernel。

最后,Jupyter Lab还可以打开终端(File - New - Terminal)。在一些没有SSH的机器上,可以通过这种方式来在HTTPS协议上使用终端。

本文到这里就结束了。后续我可能会根据实践情况添加一些内容。

【更新】看到一个不错的主题 https://github.com/catppuccin/jupyterlab

posted @ 2023-10-15 10:51  Eslzzyl  阅读(566)  评论(0编辑  收藏  举报