---
title: "JupyterHub로 다중 사용자 환경에서 Jupyter Notebook을 효율적으로 활용하는 방법"
description: "JupyterHub는 다중 사용자 환경에서 Jupyter Notebook을 효율적으로 관리할 수 있는 서버로, 사용자별 독립된 환경을 제공한다. Ubuntu 서버에 JupyterHub를 설치하고 설정하는 과정에는 pip 설치, systemd 서비스 등록, Nginx reverse proxy 구성 등이 포함된다. PAM 인증을 통해 사용자 관리를 하며, SSL 적용으로 보안을 강화할 수 있다. 각 사용자의 리소스 제한 설정과 WebSocket 지원도 중요하다."
date: "2024-09-24"
last_modified: "2026-09-01T20:27:00.000Z"
type: "Post"
tags:
  - "Docs"
  - "python"
  - "ubuntu"
  - "install"
  - "ssl"
categories:
  - "📗 Docs"
canonical_url: "https://blog.pieroot.xyz/jupyterhub-multiuser"
markdown_url: "https://blog.pieroot.xyz/jupyterhub-multiuser.md"
---

# JupyterHub로 다중 사용자 환경에서 Jupyter Notebook을 효율적으로 활용하는 방법

JupyterHub는 다중 사용자 환경에서 Jupyter Notebook을 효율적으로 관리할 수 있는 서버로, 사용자별 독립된 환경을 제공한다. Ubuntu 서버에 JupyterHub를 설치하고 설정하는 과정에는 pip 설치, systemd 서비스 등록, Nginx reverse proxy 구성 등이 포함된다. PAM 인증을 통해 사용자 관리를 하며, SSL 적용으로 보안을 강화할 수 있다. 각 사용자의 리소스 제한 설정과 WebSocket 지원도 중요하다.

연구실이나 팀에서 여러 사람이 동시에 Jupyter Notebook을 사용해야 하는 상황이 생긴다. 각자 서버에 SSH로 접속해서 `jupyter notebook`을 띄우면 포트 충돌에 환경 꼬임까지... 관리하는 입장에서 상당히 골치 아프다.

**JupyterHub**는 이런 문제를 해결해주는 멀티 유저 Jupyter 서버다. 중앙에서 사용자 관리를 하면서, 각 사용자별로 독립된 Jupyter Notebook/Lab 환경을 제공한다. TensorFlow나 PyTorch 같은 ML 프레임워크를 사용하는 환경에서 특히 유용하다.

이번 글에서는 Ubuntu 서버에서 JupyterHub를 설치하고, systemd 서비스로 등록하고, Nginx reverse proxy까지 구성하는 전체 과정을 정리한다.

> **이 글에서 다루는 내용**
> 
> - JupyterHub란 무엇이고 왜 필요한가
> 
> - pip 기반 JupyterHub 설치
> 
> - 설정 파일(`jupyterhub_config.py`) 주요 항목 해설
> 
> - systemd 서비스 등록으로 자동 시작 구성
> 
> - 사용자 관리 및 관리자 설정
> 
> - Nginx reverse proxy + SSL 구성

### 관련 프로젝트

[TensorFlow 2.12 GPU 최적화를 위한 Miniconda 환경 설정 가이드](https://app.notion.com/p/21e067c015d080a4ac66d3d666580c08) 

---

### 🤔 JupyterHub란?

JupyterHub는 **멀티 유저를 위한 Jupyter 서버**다. 단일 서버에서 여러 사용자에게 각각 독립된 Jupyter Notebook 또는 JupyterLab 환경을 제공한다.

> **JupyterHub의 핵심 구성 요소**
> 
> - **Hub**: 사용자 인증과 세션 관리를 담당하는 중앙 프로세스
> 
> - **Proxy**: 사용자 요청을 적절한 Notebook 서버로 라우팅. `configurable-http-proxy`(Node.js 기반)를 사용한다
> 
> - **Spawner**: 각 사용자별 Notebook 서버를 생성하는 컴포넌트
> 
> - **Authenticator**: 사용자 인증을 처리. 기본적으로 PAM(Linux 시스템 계정) 인증을 사용한다

일반 Jupyter Notebook과의 차이를 정리하면 이렇다:

여기서 중요한 건 **Proxy**다. JupyterHub는 사용자가 로그인하면 그 사용자 전용 Notebook 서버를 Spawner를 통해 생성하고, Proxy가 해당 서버로 요청을 리다이렉트하는 구조다. 그래서 `configurable-http-proxy`가 반드시 설치되어야 JupyterHub가 작동한다.

```
┌─────────────────────────────────────────────────┐
│                  JupyterHub                     │
│                                                 │
│  ┌───────────┐   ┌────────────┐   ┌──────────┐  │
│  │   Hub     │──▶│   Proxy    │──▶│ Spawner  │  │
│  │ (인증/관리) │   │ (라우팅)     │   │ (서버생성) │  │
│  └───────────┘   └─────┬──────┘   └────┬─────┘  │
│                        │               │         │
└────────────────────────┼───────────────┼─────────┘
                         │               │
          ┌──────────────┴───┐    ┌──────┴──────┐
          │  User A Server   │    │ User B Server│
          │  (JupyterLab)    │    │ (JupyterLab) │
          └──────────────────┘    └──────────────┘
```

---

### 📋 사전 준비

설치를 시작하기 전에 다음 패키지들이 필요하다:

> **필요 환경**
> 
> - Ubuntu 20.04 이상 (또는 Debian 계열)
> 
> - Python 3.8 이상
> 
> - Node.js 12 이상 + npm
> 
> - root 또는 sudo 권한

```bash
# 시스템 패키지 업데이트
sudo apt-get update && sudo apt-get upgrade -y

# Python 및 개발 도구 설치
sudo apt-get install python3 python3-pip python3-dev build-essential -y

# Node.js 및 npm 설치
sudo apt-get install nodejs npm -y

# 버전 확인
python3 --version
node --version
npm --version
```

> **Node.js 버전이 너무 낮다면?**
> 
> Ubuntu 기본 저장소의 Node.js는 버전이 낮을 수 있다. [NodeSource](https://github.com/nodesource/distributions)를 통해 최신 LTS 버전을 설치하는 것을 권장한다:
> 
> `curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -`
> 
> `sudo apt-get install -y nodejs`

---

### 🔧 1단계: JupyterHub 설치

아래 명령어를 실행하여 JupyterHub와 필요한 패키지를 설치한다:

```bash
# configurable-http-proxy 설치 (JupyterHub의 프록시 역할)
sudo npm install -g configurable-http-proxy

# JupyterHub 및 JupyterLab 설치
sudo pip install jupyterhub jupyterlab notebook
```

JupyterHub는 사용자 요청을 각 Notebook 서버로 리다이렉트하는 방식으로 동작하므로, `configurable-http-proxy`가 반드시 먼저 설치되어야 한다. ~~이걸 빠뜨리면 Hub가 실행은 되는데 아무것도 안 된다~~

Hub만 설치하면 기본으로 Jupyter Notebook(클래식) 인터페이스로 실행된다. JupyterLab의 모던한 UI를 사용하고 싶다면 `jupyterlab`도 같이 설치해주자.

#### 설치 확인

```bash
# JupyterHub 버전 확인
jupyterhub --version

# configurable-http-proxy 확인
configurable-http-proxy --version
```

정상적으로 버전이 출력되면 설치 완료다. 🎉

---

### ⚙️ 2단계: JupyterHub 설정

#### 설정 파일 생성

JupyterHub 기본 설정 파일을 생성하고, `/etc/jupyterhub/` 디렉토리로 이동시킨다:

```bash
# 기본 설정 파일 생성
sudo jupyterhub --generate-config

# 설정 디렉토리 생성 및 파일 이동
sudo mkdir -p /etc/jupyterhub
sudo mv jupyterhub_config.py /etc/jupyterhub/
```

> **왜 ****`/etc/jupyterhub/`****에 두는가?**
> 
> `--generate-config`를 실행하면 현재 디렉토리에 설정 파일이 생성된다. 하지만 systemd 서비스로 운영할 것이기 때문에, 시스템 설정 파일이 모여있는 `/etc/` 하위에 두는 것이 관리상 편하다.

#### 주요 설정 항목

설정 파일을 열어서 편집한다:

```bash
sudo vi /etc/jupyterhub/jupyterhub_config.py
```

아래 설정을 추가한다:

```python
# 바인딩 주소 - 모든 인터페이스에서 접근 허용
c.JupyterHub.ip = '0.0.0.0'

# 포트 설정 (기본: 8000)
c.JupyterHub.port = 18000

# 기본 인터페이스를 JupyterLab으로 변경
c.Spawner.default_url = '/lab'

# 각 사용자의 Notebook 작업 디렉토리
c.Spawner.notebook_dir = '~/jupyter'

# 관리자 계정 설정
c.Authenticator.admin_users = {'root', 'your-username'}

# 모든 시스템 사용자 로그인 허용
c.Authenticator.allow_all = True
```

기본 포트는 8000번이나, Portainer 등 다른 서비스가 이미 사용 중일 수 있으므로 18000번으로 변경했다. `default_url`을 `/lab`으로 설정해야 JupyterLab 인터페이스로 기본 실행이 바뀐다.

> **설정 항목 상세 해설**
> 
> - `c.JupyterHub.ip = '0.0.0.0'`: 모든 네트워크 인터페이스에서 접근을 허용한다. Nginx reverse proxy를 사용할 경우 `127.0.0.1`로 변경하여 로컬에서만 접근하도록 제한하는 것이 보안상 좋다
> 
> - `c.JupyterHub.port = 18000`: 원하는 포트 번호로 설정. 방화벽에서 해당 포트를 열어야 한다
> 
> - `c.Spawner.default_url = '/lab'`: 이 설정이 없으면 클래식 Notebook으로 열린다
> 
> - `c.Spawner.notebook_dir = '~/jupyter'`: 각 사용자의 홈 디렉토리 아래 `jupyter` 폴더를 작업 공간으로 지정한다
> 
> - `c.Authenticator.admin_users`: 관리자 페이지(`/hub/admin`)에 접근할 수 있는 사용자 목록
> 
> - `c.Authenticator.allow_all = True`: 서버에 존재하는 모든 시스템 사용자가 로그인할 수 있다. 특정 사용자만 허용하려면 `c.Authenticator.allowed_users = {'user1', 'user2'}`를 사용하자

> **보안 주의사항**
> 
> JupyterHub는 기본적으로 **PAM 인증**을 사용한다. 즉, 서버의 Linux 시스템 계정과 비밀번호로 로그인한다. 따라서 **반드시 sudo 권한으로 실행**해야 다른 사용자의 Notebook 서버를 생성할 수 있다. 외부에 직접 노출하는 경우 반드시 HTTPS를 적용해야 비밀번호가 평문으로 전송되는 것을 방지할 수 있다.

#### 추가 유용한 설정

필요에 따라 아래 설정도 추가할 수 있다:

```python
# 로그아웃 시 서버 자동 종료
c.JupyterHub.shutdown_on_logout = True

# Notebook 서버 시작 타임아웃 (초)
c.Spawner.start_timeout = 60

# 각 사용자에게 할당할 메모리 제한
c.Spawner.mem_limit = '4G'

# 각 사용자에게 할당할 CPU 제한
c.Spawner.cpu_limit = 2
```

> **리소스 제한이 중요한 이유**
> 
> ML 작업을 하다 보면 한 사용자가 메모리를 독점해서 서버 전체가 먹통이 되는 경우가 있다. `mem_limit`과 `cpu_limit`을 적절히 설정해두면 이런 사태를 예방할 수 있다. ~~누군가 for 루프 안에서 DataFrame을 무한 복사하는 걸 막으려면 필수다~~

---

### 🚀 3단계: JupyterHub 실행

설정이 완료되었으면, 아래 명령어로 JupyterHub를 실행할 수 있다:

```bash
sudo jupyterhub -f /etc/jupyterhub/jupyterhub_config.py
```

이제 브라우저에서 `http://{your-ip-address}:18000`으로 접속하면 JupyterHub 로그인 페이지가 나타난다.

![image](https://blog.pieroot.xyz/api/image-proxy?id=960cad6f-fd29-40fc-9fb2-9f6f7000bcef&kind=s3&pageId=30d067c0-15d0-8003-9b48-e1f1712e123e&source=block&blockId=30d067c0-15d0-8041-9cd4-eb4e693f84c9)

접속하면 위의 로그인 창이 나오게 되는데, **설치한 서버의 Linux 시스템 계정과 비밀번호**를 입력하면 접속이 가능하다. 로그인에 성공하면 사용자 전용 JupyterLab 환경이 자동으로 생성된다.

> **처음 접속 시 확인할 것**
> 
> 1. 로그인 후 JupyterLab 화면이 정상적으로 뜨는지 확인
> 
> 1. 터미널을 열어서 `pwd`로 작업 디렉토리가 `~/jupyter`인지 확인
> 
> 1. 관리자 계정으로 로그인한 경우 `http://{ip}:18000/hub/admin`에서 관리자 패널 접근 확인

---

### 📝 4단계: systemd 서비스 등록

매번 수동으로 JupyterHub를 실행하는 건 비현실적이다. systemd에 서비스로 등록하여 **시스템 부팅 시 자동 시작**되도록 설정하자.

#### 서비스 파일 생성

```bash
sudo vi /etc/systemd/system/jupyterhub.service
```

아래 내용을 작성한다:

```
[Unit]
Description=JupyterHub
After=syslog.target network.target

[Service]
User=root
Environment="PATH=/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin"
ExecStart=/usr/local/bin/jupyterhub -f /etc/jupyterhub/jupyterhub_config.py
WorkingDirectory=/etc/jupyterhub
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target
```

> **서비스 파일 해설**
> 
> - `After=syslog.target network.target`: 네트워크가 준비된 후에 시작한다. 네트워크 없이 시작하면 바인딩 실패가 발생할 수 있다
> 
> - `User=root`: PAM 인증을 사용하므로 root 권한이 필요하다. 다른 사용자의 프로세스를 생성해야 하기 때문이다
> 
> - `Environment="PATH=..."`: 시스템 PATH를 명시적으로 지정한다. systemd 환경에서는 기본 PATH가 제한적이라 `jupyterhub`나 `configurable-http-proxy` 바이너리를 찾지 못할 수 있다
> 
> - `WorkingDirectory=/etc/jupyterhub`: JupyterHub가 생성하는 SQLite DB(`jupyterhub.sqlite`)와 쿠키 시크릿 파일이 이 디렉토리에 저장된다
> 
> - `Restart=on-failure`: 비정상 종료 시 자동 재시작. `RestartSec=10`으로 10초 대기 후 재시작한다

#### 서비스 등록 및 시작

```bash
# systemd 데몬 리로드
sudo systemctl daemon-reload

# 부팅 시 자동 시작 활성화
sudo systemctl enable jupyterhub.service

# 서비스 시작
sudo systemctl start jupyterhub.service

# 상태 확인
sudo systemctl status jupyterhub.service
```

> 서비스 상태가 **`active (running)`**이면 성공이다! 🎉
> 
> 이제부터는 설정 파일을 수정한 후 아래 명령어로 간편하게 재시작할 수 있다:
> 
> `sudo systemctl restart jupyterhub.service`

로그 확인이 필요한 경우:

```bash
# 실시간 로그 확인
sudo journalctl -u jupyterhub -f

# 최근 100줄 로그 확인
sudo journalctl -u jupyterhub -n 100
```

---

### 👥 5단계: 사용자 관리

JupyterHub는 기본적으로 **Linux PAM 인증**을 사용한다. 즉, 서버에 시스템 사용자를 추가하면 바로 JupyterHub에 로그인할 수 있다.

#### 새 사용자 추가

```bash
# 시스템 사용자 추가
sudo adduser newuser

# Jupyter 작업 디렉토리 생성 (자동 생성되지 않는 경우)
sudo mkdir -p /home/newuser/jupyter
sudo chown newuser:newuser /home/newuser/jupyter
```

#### 관리자 패널

관리자 계정으로 로그인한 후, `http://{ip}:18000/hub/admin`에 접속하면 관리자 패널을 사용할 수 있다.

> **관리자 권한 설정**
> 
> `jupyterhub_config.py`에서 관리자 사용자를 설정할 수 있다:
> 
> ```python
> # 관리자 사용자 지정
> c.Authenticator.admin_users = {'admin', 'your-username'}
> 
> # 관리자가 다른 사용자 서버에 접근 허용 (디버깅에 유용)
> c.JupyterHub.admin_access = True
> ```

> **주의:** `admin_users` 설정은 **권한을 부여**할 때만 사용된다. 한번 부여된 관리자 권한은 이 설정에서 사용자를 제거해도 자동으로 해제되지 않는다. 관리자 권한을 회수하려면 관리자 패널이나 API를 통해 직접 변경해야 한다.

#### Conda 환경별 커널 연동

사용자마다 다른 Conda 환경을 Jupyter 커널로 등록하여 사용할 수 있다. 예를 들어 TensorFlow 전용 환경을 만들어서 커널로 등록하려면:

```bash
# Conda 환경 생성
conda create -n tf python=3.10

# 환경 활성화
conda activate tf

# TensorFlow 설치
pip install tensorflow

# ipykernel 설치 및 커널 등록
pip install ipykernel
python -m ipykernel install --user --name tf --display-name "Python (TensorFlow)"
```

이렇게 등록하면 JupyterLab에서 새 Notebook을 만들 때 커널을 선택할 수 있다.

---

### 🌐 6단계: Nginx Reverse Proxy

JupyterHub를 직접 외부에 노출하는 것보다 **Nginx를 앞단에 두고 reverse proxy**로 구성하는 것을 강력히 권장한다. 이유는 다음과 같다:

- **SSL/TLS 종료**: Nginx에서 HTTPS를 처리하여 보안 통신 확보

- **포트 통합**: 443 포트로 깔끔하게 접근 가능

- **WebSocket 지원**: JupyterHub는 커널 통신에 WebSocket을 사용하므로, 프록시에서 이를 명시적으로 지원해야 한다

- **다른 서비스와 공존**: 같은 서버에서 여러 도메인/서비스를 운영할 수 있다

#### Nginx 설치

```bash
sudo apt-get install nginx -y
```

#### JupyterHub 설정 변경

Nginx를 사용할 경우, JupyterHub는 **로컬에서만 리슨**하도록 변경한다:

```python
# /etc/jupyterhub/jupyterhub_config.py
c.JupyterHub.bind_url = 'http://127.0.0.1:18000'
```

> **`ip`****/****`port`****와 ****`bind_url`****의 차이**
> 
> 이전에 설정한 `c.JupyterHub.ip`와 `c.JupyterHub.port` 대신 `c.JupyterHub.bind_url`을 사용하면 프로토콜, IP, 포트를 한 번에 지정할 수 있어 더 직관적이다. 두 방식을 동시에 쓰면 충돌이 발생할 수 있으니 하나만 사용하자.

#### Nginx 설정 파일 작성

```bash
sudo vi /etc/nginx/sites-available/jupyterhub
```

아래 내용을 작성한다:

```javascript
# WebSocket 헤더 매핑
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

# HTTP → HTTPS 리다이렉트
server {
    listen 80;
    server_name jupyter.yourdomain.com;
    return 302 https://$host$request_uri;
}

# HTTPS 서버
server {
    listen 443 ssl;
    server_name jupyter.yourdomain.com;

    # SSL 인증서 (Let's Encrypt 기준)
    ssl_certificate /etc/letsencrypt/live/jupyter.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/jupyter.yourdomain.com/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;

    # JupyterHub 프록시
    location / {
        proxy_pass http://127.0.0.1:18000;

        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # WebSocket 지원 (필수!)
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header X-Scheme $scheme;

        proxy_buffering off;
    }
}
```

> **WebSocket 설정은 필수다!**
> 
> JupyterHub는 커널과의 통신에 **WebSocket**을 사용한다. `proxy_set_header Upgrade`와 `Connection` 설정이 없으면 Notebook을 열어도 커널 연결이 실패하여 코드 실행이 불가능하다. ~~이거 때문에 "커널이 안 잡힌다"고 삽질하는 사람이 정말 많다~~

핵심 설정을 하나씩 살펴보면:

#### `map $http_upgrade $connection_upgrade`

이 블록은 WebSocket 프록시를 위한 **변수 매핑**이다. 클라이언트가 WebSocket 업그레이드를 요청하면(`Upgrade: websocket`) Connection 헤더를 `upgrade`로 설정하고, 일반 HTTP 요청이면 `close`로 설정한다.

#### X-Forwarded 헤더

```javascript
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;
```

reverse proxy를 거치면 JupyterHub 입장에서는 **원래 클라이언트의 정보를 알 수 없게** 된다. 이 헤더들은 원본 요청의 정보(클라이언트 IP, 프로토콜)를 전달해주는 역할을 한다.

#### SSL 인증서 발급 (Let's Encrypt)

아직 SSL 인증서가 없다면 Let's Encrypt의 Certbot을 사용하여 무료 인증서를 발급받을 수 있다:

```bash
# Certbot 설치
sudo apt-get install certbot python3-certbot-nginx -y

# 인증서 발급 (Nginx 플러그인)
sudo certbot --nginx -d jupyter.yourdomain.com

# 자동 갱신 테스트
sudo certbot renew --dry-run
```

> **Certbot의 Nginx 플러그인**을 사용하면 SSL 설정을 자동으로 추가해주기 때문에 편리하다. 하지만 기존 설정을 덮어쓸 수 있으니, 수동으로 인증서만 발급받고 직접 설정하는 것도 좋은 방법이다:
> 
> `sudo certbot certonly --standalone -d jupyter.yourdomain.com`

#### Nginx 활성화 및 시작

```bash
# 사이트 활성화
sudo ln -s /etc/nginx/sites-available/jupyterhub /etc/nginx/sites-enabled/

# 설정 문법 검사 (필수!)
sudo nginx -t

# Nginx 재시작
sudo systemctl restart nginx
```

> `nginx -t`에서 **`syntax is ok`**가 나오면 정상이다. 이 단계를 건너뛰고 바로 재시작했다가 문법 오류로 Nginx가 죽으면... 서버의 모든 웹 서비스가 한 번에 날아간다. 꼭 검증하자! 🙏

이제 `https://jupyter.yourdomain.com`으로 접속하면 JupyterHub를 HTTPS로 안전하게 사용할 수 있다.

---

### 🔧 트러블슈팅

#### configurable-http-proxy를 찾을 수 없는 경우

```bash
# npm 글로벌 설치 경로 확인
npm config get prefix

# 결과가 /usr/local이 아닌 경우, 심볼릭 링크 생성
sudo ln -s $(npm config get prefix)/bin/configurable-http-proxy /usr/local/bin/
```

~~이 에러는 systemd에서 실행할 때 특히 자주 발생한다. PATH 문제다~~

#### 500 에러가 발생하는 경우

```bash
# JupyterHub 로그 확인
sudo journalctl -u jupyterhub -n 50

# SQLite DB 초기화 (최후의 수단)
sudo rm /etc/jupyterhub/jupyterhub.sqlite
sudo systemctl restart jupyterhub
```

#### 사용자의 Notebook 서버가 시작되지 않는 경우

가장 흔한 원인은 **해당 사용자의 홈 디렉토리가 없거나 권한이 잘못된 경우**다:

```bash
# 홈 디렉토리 확인
ls -la /home/username/

# jupyter 작업 디렉토리 수동 생성
sudo mkdir -p /home/username/jupyter
sudo chown username:username /home/username/jupyter
```

#### Nginx 프록시 뒤에서 커널 연결이 안 되는 경우

WebSocket 관련 설정이 빠졌을 가능성이 높다. Nginx 설정에서 아래 항목이 있는지 확인하자:

```javascript
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
```

> **브라우저 개발자 도구(F12)의 Network 탭**에서 WebSocket 연결(`ws://` 또는 `wss://`)이 정상적으로 `101 Switching Protocols` 응답을 받는지 확인하면 문제를 빠르게 진단할 수 있다.

---

### ✅ 핵심 정리

✅ **JupyterHub**: 멀티 유저 Jupyter 서버. Hub + Proxy + Spawner 구조로 사용자별 독립 환경 제공

✅ **configurable-http-proxy**: Node.js 기반 프록시. JupyterHub의 필수 의존성으로, 반드시 설치해야 한다

✅ **PAM 인증**: 기본 인증 방식. Linux 시스템 계정으로 로그인하며, root 권한으로 실행해야 한다

✅ **default\_url = '/lab'**: 이 설정이 없으면 클래식 Notebook으로 실행된다. JupyterLab을 원하면 반드시 설정

✅ **systemd 서비스**: 안정적인 운영을 위해 서비스 등록 필수. `Restart=on-failure`로 장애 대응

✅ **Nginx Reverse Proxy**: SSL 종료, WebSocket 지원, 포트 통합을 위해 강력 권장

✅ **WebSocket 설정**: Nginx에서 `Upgrade`/`Connection` 헤더 설정이 없으면 커널 연결 실패

### ⚠️ 주의사항

⚠️ **HTTPS 필수**: PAM 인증은 비밀번호를 전송하므로, 외부 노출 시 반드시 SSL을 적용해야 한다

⚠️ **PATH 문제**: systemd 환경에서는 PATH가 제한적이므로, 서비스 파일에 명시적으로 지정하자

⚠️ **포트 충돌**: 기본 포트 8000번은 다른 서비스와 충돌할 수 있다. 확인 후 변경하자

⚠️ **디스크 용량**: 사용자가 늘어나면 각 사용자별 Notebook과 데이터가 축적된다. 주기적으로 디스크 사용량을 모니터링하자

### 📚 참고 자료

- [JupyterHub 공식 문서](https://jupyterhub.readthedocs.io/en/stable/)

- [JupyterHub Quickstart Guide](https://jupyterhub.readthedocs.io/en/stable/tutorial/quickstart.html)

- [JupyterHub Nginx Reverse Proxy 설정](https://jupyterhub.readthedocs.io/en/latest/howto/configuration/config-proxy.html)

- [JupyterHub 사용자 인증 및 관리](https://jupyterhub.readthedocs.io/en/latest/tutorial/getting-started/authenticators-users-basics.html)

- [Deploy JupyterHub on Ubuntu Server - Medium](https://medium.com/@prateekaverma/install-jupyterhub-and-jupyterlab-on-ubuntu-server-d390570b5bb6)
