去年我给实验室配了一台八核32G内存的服务器,装完系统的第一个任务,就是让组里七八个同学都能用自己的账号打开 Jupyter Notebook。一开始我也图省事,让每个人分别在各自的端口起一个 jupyter lab,结果不到两周就乱成一锅粥:端口谁用谁知道,内存经常被某个人一次全跑完,文件散落各处,给导师汇报的时候谁也找不到谁的东西。后来我老老实实把 JupyterHub 配起来,才算是彻底从这种混乱里解脱了。
这篇教程就是我当初从零配置的完整过程,包含安装步骤、认证方式、反向代理、常见报错排查,以及日常维护里总结出来的经验。目标是让一台干净的通用 Linux 服务器,变成一台能同时服务团队多个成员的 Jupyter 计算平台。如果你正好接手了类似的需求,或者正准备把自己单机的 Notebook 环境“毕业”成团队服务,这篇文章应该能帮你少走不少弯路。
1. 部署前先搞清楚:JupyterHub 到底帮你管了哪几件事
1.1 团队一起用 Jupyter 的几种错误姿势
先聊聊我为什么一开始会被“每人一个端口”的方案坑。当时大家的习惯是:A 同学在 8081 起一个 Jupyter,B 同学在 8082 再起一个,C 同学可能直接用服务器上的 VS Code Remote 连自己目录。看似互不干扰,实际上问题非常多。
第一是端口管理非常脆弱,谁在哪个端口跑服务全靠口口相传,人一多必然记错;第二是资源完全不可控,有人循环跑模型时 CPU 飙到 100%,其他人写个简单的数据分析都卡成幻灯片;第三是权限边界模糊,大家都是用同一个公用账号登录,文件系统上完全没有隔离,一个人误删了共享目录,所有人都得跟着遭殃。
所以我后来强调一个原则:多用户环境的核心不是“能不能跑”,而是“能不能隔离、能不能管理、能不能追责”。JupyterHub 正是围绕这个原则设计出来的工具。
1.2 JupyterHub 的核心组件拆解
JupyterHub 从架构上看,可以拆成四个角色,用生活场景类比的话特别好理解:
- Hub(前台总机):负责登录认证、维护用户会话、协调其他组件。用户访问服务器根地址时,看到登录页就是 Hub 在接待。
- Authenticator(门卫):负责判断“你是谁”。它问你用户名密码,然后去系统账户、OAuth 平台或者 LDAP 那边核对身份。
- Spawner(开房服务员):认证通过之后,Spawner 负责为这个用户“开一间单人间”,也就是拉起一个独立的单用户 Jupyter Server 进程,并把它分配给某个 URL 路径。
- Proxy(楼层导航):用户在浏览器里访问
/user/alice/tree这样的路径时,Proxy 负责把请求转发到 alice 对应的那个单用户进程上。没有它,多用户同时在线时就会“串门”。
理解这几个角色的分工极其重要,因为后面所有配置,包括资源限制、权限隔离、HTTPS 反向代理,都是围绕这四者的协作方式在调。遇到问题不看日志、不理解架构,真的会走很多弯路。
1.3 方案选型:通用 Linux 服务器最稳妥的组合
JupyterHub 的认证器和 Spawner 都是可插拔的。常见的认证器有 PAMAuthenticator(读取 Linux 系统账号)、OAuthenticator(对接 GitHub、Google、GitLab)、LDAPAuthenticator;常见的 Spawner 有 LocalProcessSpawner(在服务器本机启动单用户进程)、DockerSpawner(每个用户放一个 Docker 容器)、SystemdSpawner(用 Systemd 托管单用户进程)。
本教程的标题是“通用 Linux 服务器版”,所以我优先推荐一个最朴素也最稳的组合:PAMAuthenticator + LocalProcessSpawner。
为什么这样选?因为通用 Linux 服务器大多已经有一套系统用户体系,用 PAM 意味着不需要单独维护一套用户数据库,ssh、sftp、JupyterHub 全部公用同一套账号密码。而 LocalProcessSpawner 就是一个普通的 Python 子进程,不引入 Docker 和 Systemd 的额外复杂度,对一台共享计算的服务器来说,性价比最高。
如果你对进程隔离要求非常高,或者团队分配在容器里跑代码,后面再迁移到 DockerSpawner 也不迟。我在第 7 节会专门讲什么时候该迁移,以及迁移时要注意什么。
2. 环境准备:先把地基打牢
2.1 服务器规划与系统依赖
先交代一下我用到的环境:一台 Ubuntu 22.04 LTS 的服务器,4 核 16G 内存起步,建议至少 4 核 16G,内存 8G 以下就别勉强跑多用户了;硬盘建议 100G 以上,数据、conda 环境、notebook 文件加在一起涨得比想象中快。
安装依赖之前,先把系统包更新一遍:
sudo apt update && sudo apt upgrade -y然后安装编译 Jupyter 生态常用 Python 包时需要的基础依赖:
sudo apt install -y build-essential python3-dev git curl wget如果你的服务器是 CentOS / Rocky Linux,把apt换成dnf,并安装gcc gcc-c++ make和python3-devel即可,原理完全一样。
另外要提醒一句:不要图省事直接把系统自带的 Python 当唯一 Python 环境。Ubuntu 系统自带的 Python 和控制系统的 apt 包经常纠缠不清,一旦你pip install把某个依赖升级了,可能把整个系统搞挂。这里强烈建议用 Miniconda 管理 Python 环境。
2.2 通过 Miniconda 管理 Python 环境
之所以用 Miniconda,除了环境隔离之外,还有一个 JupyterHub 场景下的实际原因:团队里每个人对包版本的需求不一样,有人要 tensorflow 2.10,有人坚持用 pytorch,还有人只需要 pandas 和 matplotlib。CondA 环境可以在不互相干扰的情况下,让每个用户在不同目录里创建自己的环境;而且 conda 环境名可以作为 Jupyter kernel 名,用户能在 Notebook 里自行切换。
安装 Miniconda 时,我统一装在/opt/conda,这样所有用户都可以读取公共环境,而不会把每个人的 home 目录撑爆:
curl -fsSL https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -o /tmp/miniconda.sh sudo bash /tmp/miniconda.sh -b -p /opt/conda安装完成后,把 conda 初始化的路径加到系统环境变量里。如果是 systemd 托管 JupyterHub,还需要在 service 文件里显式指定 PATH,这块稍后会讲到。
2.3 创建系统用户与目录规划
JupyterHub 的服务进程本身不应该以 root 运行,也不建议挂在某个普通用户下。安全做法是创建一个专门用于运行 JupyterHub 的系统用户:
sudo useradd -r -m -s /bin/bash jupyterhub普通用户则各自创建独立账号:
sudo useradd -m -s /bin/bash alice sudo useradd -m -s /bin/bash bob sudo passwd alice sudo passwd bob注意顺序:先创建用户,再配置 JupyterHub,因为 PAM 认证默认就是去核对系统账号。如果你在已有生产环境中补充用户,加一个就能马上用,完全不用重启服务。
还有一个非常重要的目录规划:不要让用户互相可读 home 目录。默认情况下 Debian/Ubuntu 的useradd创建 home 目录权限是 755,这就意味着 alice 能浏览 bob 的 home。所以我创建完用户后会统一收权限:
sudo chmod 700 /home/alice /home/bob如果不小心用 root 或者公用账号创建了/home/alice/xxx.txt,那文件 owner 是 root,alice 自己反而改不了,后面经常出现 “Permission denied”。遇到这种情况,直接:
sudo chown -R alice:alice /home/alice2.4 公共数据目录设计
团队协作场景里,公共数据集是很常见的东西。我的习惯是单独建一个/data目录,按项目再分一级:
sudo mkdir -p /data/projectA /data/projectB sudo chown -R root:users /data sudo chmod -R 775 /data然后把需要读公共数据的用户加入users组:
sudo usermod -aG users alice sudo usermod -aG users bob这样做的效果是:公共目录里大家都能读能写(如果是只读数据,把775改成755就行),但回到自己的 home 目录则完全隔离。对大多数实验室和小型团队来说,这套组合已经够用了。
3. 安装 JupyterHub 并完成第一轮可用配置
3.1 安装 JupyterHub 和 JupyterLab
我建议把 JupyterHub 装进/opt/conda的 base 环境里,方便 systemd 直接用全局命令启动。安装命令:
sudo /opt/conda/bin/pip install --upgrade pip sudo /opt/conda/bin/pip install jupyterhub jupyterlab notebook顺便说一下,旧版 JupyterHub 依赖configurable-http-proxy,需要额外装 Node.js;但 JupyterHub 3.x 和 4.x 已经默认使用内置的 Traefik 代理,pip install jupyterhub会把代理依赖自动装好,不需要再折腾 Node.js 了。网上大量教程还在让你装 Node,大多是老版本时期的遗留信息。
装完后确认一下:
/opt/conda/bin/jupyterhub --version看到版本号输出就说明核心程序已经就位。
3.2 生成配置文件并理解关键项
第一次配置前,先用命令生成一个默认的jupyterhub_config.py:
sudo mkdir -p /etc/jupyterhub sudo /opt/conda/bin/jupyterhub --generate-config -f /etc/jupyterhub/jupyterhub_config.py这个文件内容非常多,默认全是注释。我的经验是:新手不要试图一次看懂所有配置项,抓住下面几个就够起步了。
c.JupyterHub.ip = '0.0.0.0':让 Hub 监听所有网卡。如果服务器有多个网卡且只想对内网开放,可以改成内网 IP。c.JupyterHub.port = 8000:默认端口。后面配了 Nginx 反向代理后,通常会让 8000 只监听本机。c.JupyterHub.authenticator_class = 'jupyterhub.auth.PAMAuthenticator':默认就是 PAM,可以不用改,但建议写出来,语义更明确。c.JupyterHub.spawner_class = 'jupyterhub.spawner.LocalProcessSpawner':同样默认是 LocalProcessSpawner,写出来方便后面替换。c.Spawner.default_url = '/lab':用户登录后直接进入 JupyterLab 界面,而不是旧版 Notebook 界面。用习惯之后会觉得 Lab 顺手太多。c.Authenticator.admin_users = {'ops'}:把ops账号设置为管理员。管理员后台可以查看在线用户、停止异常会话,非常有用。
完成后可以先用最简单的方式试一把:
sudo -u jupyterhub /opt/conda/bin/jupyterhub -f /etc/jupyterhub/jupyterhub_config.py如果用的是jupyterhub用户启动,记得确保/etc/jupyterhub和配置目录的权限可读。测试无误后,Ctrl+C 停掉,继续用 systemd 托管。
3.3 用 systemd 管理 JupyterHub 服务
生产环境仍然用前台进程跑服务是很不专业的,一旦终端断开服务就没了。我写了一个 systemd unit,丢在/etc/systemd/system/jupyterhub.service:
[Unit] Description=JupyterHub After=network.target [Service] User=jupyterhub Group=jupyterhub WorkingDirectory=/etc/jupyterhub ExecStart=/opt/conda/bin/jupyterhub -f /etc/jupyterhub/jupyterhub_config.py Restart=always RestartSec=10 Environment=PATH=/opt/conda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin [Install] WantedBy=multi-user.target然后:
sudo systemctl daemon-reload sudo systemctl enable --now jupyterhub sudo systemctl status jupyterhub看到active (running)就说明服务正常。之后所有配置变更,只需要改jupyterhub_config.py再sudo systemctl restart jupyterhub即可。
3.4 首个多用户登录测试
启动服务后,浏览器访问http://服务器IP:8000,应该能看到 JupyterHub 登录页。用 alice 登录,输入密码,登录后系统会自动帮 alice 起一个单用户 Jupyter 服务,整个过程大概 3 到 10 秒。
这里我强烈建议做两个验证,别急着把链接发出去:
- 在服务器上跑一句
ps -ef | grep jupyter,确认单用户进程是用alice用户身份跑起来的。 - 登录 bob 的账号,让 alice 再访问浏览器里的
/user/bob/tree,如果返回 403/404,说明 proxy 隔离是生效的。
这一步走通,等于 JupyterHub 的“多用户”骨架已经搭起来了。接下来要考虑的都是锦上添花但早晚要面对的事:认证管理、HTTPS、资源限制。
4. 身份认证与账号体系:让“多用户”真正可用
4.1 基于 Linux 系统账号的 PAM 认证
PAMAuthenticator 是 JupyterHub 自带默认认证器,它的工作方式就是调用操作系统底层的 PAM 模块核对账号密码。也就是说,你能用ssh alice@服务器登录,就能用 alice 登录 JupyterHub,账号体系完全统一,这省掉了维护第二套用户表的麻烦。
使用 PAM 认证时有一个隐性前提:系统里必须存在这个用户。以前有个同事问“为什么我创建了虚拟账号在 JupyterHub 里登录失败?”,其实就是因为useradd这一步漏了。
如果你想让某些用户只能访问 JupyterHub,不能 ssh 登录,可以在/etc/ssh/sshd_config里用AllowUsers或DenyUsers精细控制,比直接在 JupyterHub 侧做限制更干净。
4.2 测试环境下的 DummyAuthenticator
有时候你只想快速验证 JupyterHub 功能,又不想挨个创建系统用户,可以用 DummyAuthenticator:
c.JupyterHub.authenticator_class = 'jupyterhub.auth.DummyAuthenticator' c.Authenticator.admin_users = {'admin'} c.DummyAuthenticator.password = 'admin123'这类配置下,任何用户名只要填这个固定密码都能登录。我一般只在测试环境用,生产环境千万不要开。如果你没有特殊需求,坚持 PAM 就好。
4.3 管理员账号怎么用
管理员账号在 JupyterHub 里的能力比普通用户大很多。我在配置里给了ops账号管理员权限:
c.Authenticator.admin_users = {'ops'}配置完后用ops登录,点击右上角用户头像可以看到 “Admin” 菜单。在后台里你能看到当前哪些用户在线、哪些用户的 server 正在运行,也能直接停掉某个卡死的用户进程。这在团队协作中非常实用,比如某个人跑了一个死循环代码,你不用sudo kill,直接在后台停止他的 server 就行。
4.4 禁用用户与下线会话
当有人离开团队,或者某账号有违规行为,最直接的方式是锁定系统账号:
sudo usermod -L alice但要注意,这会把 ssh 登录也一并禁掉。如果只想禁掉 JupyterHub 访问,可以给用户设置一个无法通过 PAM 认证的过期密码,或者干脆在配置里做一层白名单:
import subprocess from jupyterhub.auth import PAMAuthenticator class WhitelistPAMAuthenticator(PAMAuthenticator): allowed_users = {'alice', 'bob'} async def authenticate(self, handler, data): username = data['username'] if username not in self.allowed_users: return None return await super().authenticate(handler, data) c.JupyterHub.authenticator_class = WhitelistPAMAuthenticator这种硬编码白名单的方式看起来不优雅,但确实能快速生效。如果你团队人数不多,用这个方案做账号下线已经很够了。
5. HTTPS、域名与 Nginx 反向代理
5.1 为什么不直接暴露 8000 端口
JupyterHub 默认不带 HTTPS,如果直接用http://服务器IP:8000访问,用户输入密码时是明文传输。在局域网或许能忍,但一旦有人从外网访问,风险就非常大。
另外,JupyterHub 有很多 WebSocket 连接,比如 JupyterLab 里的终端和 Kernel 状态实时同步都依赖 WebSocket。如果前端没有正确的代理配置,很常见的现象是:登录进去了,但点开 Notebook 后内核一直连不上。所以接入 Nginx 反向代理,不仅是加一层 HTTPS,更是为了保证 WebSocket 能正确转发。
5.2 Nginx 反向代理配置示例
我习惯让 JupyterHub 只监听本机 127.0.0.1:8000,Nginx 监听 80/443,再把请求统一转发过去。
修改jupyterhub_config.py:
c.JupyterHub.bind_url = 'http://127.0.0.1:8000'然后写 Nginx 配置/etc/nginx/conf.d/jupyterhub.conf:
server { listen 80; server_name hub.example.com; location / { proxy_pass http://127.0.0.1:8000; 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; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 86400; } }关键就在Upgrade和Connection "upgrade"这两行,少了它,WebSocket 连接会被掐断。proxy_read_timeout 86400也很重要,因为长时间不操作的 Notebook 页面,如果连接被 Nginx 回收,用户下次点一下就要重新连接内核。
如果你有证书,直接把 80 改成 443 并加上 ssl 配置。没有域名也能用 IP 访问,只是需要自签证书,浏览器会提示不信任。局域网内部环境用自签证书其实可接受,外网则建议申请正规证书。
配置完后:
sudo nginx -t sudo systemctl reload nginx如果登录后页面能正常显示、Kernel 能连接,就说明代理配好了。
5.3 在 JupyterHub 里配置 Cookie 与安全项
开启了 HTTPS 反向代理后,还有两个设置建议加上。
第一个是编写 cookie 密钥文件:
c.JupyterHub.cookie_secret_file = '/etc/jupyterhub/jupyterhub_cookie_secret'JupyterHub 所有用户的登录状态都靠这个密钥签名。重启服务前最好备份它,否则重启后用户全部掉线。
第二个是告诉 JupyterHub 信任来自 Nginx 的转发请求:
c.JupyterHub.trusted_downstream_ips = ['127.0.0.1']没有这个配置,JupyterHub 可能拿不到用户真实 IP,日志里的访问来源会全部显示成 127.0.0.1,排查问题时会很麻烦。
6. 常见问题与排查实录
6.1 用户登录后启动不到 5 秒就失败
这是用户最常遇到的第一个坑:登录页面正常,输入账号密码也成功了,但页面一直转圈,然后弹出 “Spawn failed” 或者 “Server error”。
排查思路按优先级来:
- 看 JupyterHub 日志:
sudo journalctl -u jupyterhub -n 200。 - 看 spawner 是否和权限有关。最常见的是
/home/alice目录不存在、owner 不是 alice,或者/home/alice底下的.local权限不对。 - 如果是 512 内存的小机器,也有可能是内存不足导致 spawn 进程被 OOM kill。
我实际工作中 80% 的 spawn 失败都和权限/目录归属有关。用一句命令解决:
sudo chown -R alice:alice /home/alice sudo chmod 700 /home/alice6.2 登录后显示 403 页面
出现 403 通常是 JupyterHub 的 cookie 校验失败。常见原因是服务重启后 cookie 密钥变了,浏览器里旧 cookie 全部失效。让用户强制刷新并清一下站点 cookie,一般能解决。
长期解法是把cookie_secret_file固定下来,服务重启时就不需要用户重新登录。
6.3 Notebook 打开后内核无法连接
这种情况大多出现在代理配置不完整的时候。如果 Nginx 没加 WebSocket 的 Upgrade 头,JupyterLab 页面能打开,但点 Kernel 一直报 “Connection failed”。
检查 Nginx 配置,确认包含:
proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";改完记得sudo nginx -t && sudo systemctl reload nginx。
如果是直接用 8000 端口访问但内核连不上,则要检查服务器防火墙有没有放行 8000 端口:
sudo ufw allow 8000/tcp如果这种情况还是不行,基本就是用户环境的 Kernel spec 有问题,可以用jupyter kernelspec list检查。
6.4 内存被一个用户打爆怎么办
LocalProcessSpawner 默认不限制单用户内存,一个用户写个不规范的循环,可能把整台服务器干趴。这个问题的安全解法分两层。
第一层,用ulimit做即可快速的软限制。在系统用户家目录的~/.bashrc里加:
ulimit -v 8388608 # 8GB 虚拟内存限制但这个方案对用户不透明,用户很可能一头雾水。而且如果用户没经过 shell 直接由 spawner 启动,这个配置不一定生效。
第二层,换用 SystemdSpawner。在配置里指定:
c.JupyterHub.spawner_class = 'systemdspawner.SystemdSpawner' c.SystemdSpawner.memory_limit = '8G'SystemdSpawner 会为每个用户创建独立的 systemd scope,不仅限制内存,还能顺便限制 CPU 和进程数。它比 LocalProcessSpawner 更稳,也比 DockerSpawner 更轻量。我的建议是:如果你只是想控制资源,优先上 SystemdSpawner,而不是直接跳去 Docker。
6.5 重启 JupyterHub 后所有用户掉线
JupyterHub 重启时,正在运行的单用户服务不会立刻被 kill,但 proxy 会重建。如果用户发现重启后页面打不开,先等几秒让 proxy 重新注册所有路由;如果还是不行,看日志里有没有 “Error performing full sync”。
更常见的掉线原因是 cookie 密钥变化,解决办法就是 5.3 节说的固定cookie_secret_file。记住一条铁律:升级或重启 JupyterHub 前,备份/etc/jupyterhub整个目录。
6.6 端口冲突
8000 端口被占用时,JupyterHub 服务会起来又崩掉。排查方法:
sudo ss -lntp | grep 8000如果是自己之前的残留进程,kill 掉再重启就行。如果确定要换端口,改c.JupyterHub.port后重启 JupyterHub 和 Nginx。
7. 运维经验与进阶扩展
7.1 什么时候该迁移到 DockerSpawner
LocalProcessSpawner 虽然简单,但进程隔离基本为零。用户 A 可以轻松用ps看到用户 B 的进程,虽然没法直接读别人 home 目录,但生产环境总觉得别扭。
当出现下面几种情况时,就可以认真考虑迁移到 DockerSpawner:
- 用户需要各自的 Python 环境且互不干扰,镜像隔离比 conda 环境更彻底;
- 需要限制 CPU、内存、GPU 配额,Docker 的参数支持更直接;
- 希望每个用户拥有可复制的环境,比如“换了新服务器也能一键恢复”。
迁移时主要改动是安装 Docker、安装dockerspawner,然后把 spawner_class 改成dockerspawner.DockerSpawner,并为每个用户准备镜像或指定默认镜像。这个过程相当于一次小规模的架构升级,建议在业务低峰期进行。
7.2 共享只读数据集的正确姿势
我给团队搞过一个/data/datasets目录,里面放着各种公开数据集,几千个文件。为了避免有人误删,我设成只读:
sudo mkdir -p /data/datasets sudo chown root:users /data/datasets sudo chmod 755 /data/datasets这样users组的成员可以读,但不能写。如果需要某个项目组有写权限,单独创建一个带write的组目录:
sudo mkdir -p /data/projectA_write sudo chown root:groupA /data/projectA_write sudo chmod 775 /data/projectA_write这种目录设计我用了很久,基本没出过乱子。关键原则很简单:能只读就不要给写,能组权限就不要全开 777。
7.3 日常巡检三件事
JupyterHub 跑起来后,真正花时间的不是装和配,而是日常维护。我每周会固定做三个动作:
第一,看系统负载和内存。用户多了以后,内存永远比 CPU 先爆。
free -h uptime第二,看 JupyterHub 日志里有没有异常。日志是问题诊断的第一现场:
sudo journalctl -u jupyterhub -n 200 --since "1 hour ago"第三,检查磁盘。notebook 图像、数据、conda 环境都吃磁盘,满盘会导致一切服务直接瘫痪:
df -h这三个动作加起来不到五分钟,却能避免 90% 的突发故障。
7.4 关于维护体制的个人体会
我踩过最大的坑,是刚开始上线 JupyterHub 时为了让教程好看,把所有配置塞进了一个“万能配置”里。结果某个用户需要额外环境变量,某个用户需要不同启动目录,最后整个配置文件变成一团乱麻。后来我把公共配置拆成jupyterhub_config.py,把个别用户的特殊设置放在用户自己的~/.jupyter/jupyter_config.py里,我这边只需管好服务本身,团队里每个人自己管理自己的环境,维护负担瞬间小了很多。
配 JupyterHub 这件事,一开始看着组件多、概念细,但等你把认证、Spawner、代理这三条线理顺,后面的运维其实就是按部就班的事。希望这篇文章能帮你把坑提前填上,让你的“多用户 Jupyter 工作台”一次就稳稳跑起来。