【LAN内 or WANから】ssh接続したUbuntuマシンのDockerコンテナ内でJupyter Labを起動・カスタマイズ【深層学習用】

# 1. 背景
大学の研究室に新しいGPUマシンを設置したので，
いつ，どこからでもGPUマシンにアクセスし，手元のクライアントPCでコーディングや実行を行える環境を整えようと思い，
セットアップを行ったついでに備忘録としてセットアップ方法をここに書き連ねておきます．
PyTorchなどで深層学習（機械学習を含む）をしたい方におすすめです．

具体的には，sshで接続することで，GPUマシンがある場所はもちろん，どんな場所からでもコーディングやGPUを用いた実行ができます．
また，1台のGPUマシンを複数人で共有し，同じタイミングでもリソースを共有することができます．
さらにDockerコンテナを活用することで，複数ユーザーが同時に様々な環境で開発を行うことができます．
今回，初めてJupyter Labを触ったのですが，ローカルで開発しているかと思えるような様々な拡張機能がとても便利ですね．

また，この記事では，GPUマシンと同じLAN内からのssh接続だけではなく，LAN外のどこからでもssh接続する方法についても記述しております．

初心者大学生の記事なので，専門用語の使い方や，セキュリティ関係が怪しいかもしれません．ご了承ください．

# 2. 前提
## PC環境
- GPUマシン（サーバー側）
    - Ubuntu 20.04 LTS
    - AMD EPYC 7402P 24-Core Processor
    - RTX 3090 ×2
    - メモリ128GB
    - openssh-serverインストール済み
    - Dockerインストール済み
- ローカルPC（クライアント側）
    - MacBook Pro (13-inch, 2020, Thunderbolt 3ポートx 4)
    - メモリ16GB
    - macOS 12.2.1
    - 2.3GHzクアッドコアIntel Core i7

尚，ローカルPCとしてWindowsでも動作確認しました．
Windows10最新版のpowershellを使えば問題なく実行できそうです．

GPUマシンにはopenssh-serverとDockerをインストールしておきましょう．

# 3. ssh接続の準備
## やりたいこと
- sshでUbuntuPCに接続
- rsa暗号を用いてパスワード入力を省略（公開鍵認証）
- host名を用いてIPアドレス入力を省略

## 手順
初めて接続する際はクライアント側で秘密鍵と公開鍵を発行し，公開鍵をGPUマシンに送信し，保存しておく必要があります．
### 鍵発行
```zsh:terminal（クライアント側）
cd #ホームディレクトリ（Users/ユーザー名/）に移動
cd .ssh #.sshフォルダがない場合はmkdirしてください
ssh-keygen [-f output_keyfile]
```
パスフレーズはなくてもOKです（Enterキー押すだけ）．
例：`output_keyfile`を`id_rsa`などにすると，`.ssh`フォルダ内に`id_rsa`（秘密鍵）と`id_rsa.pub`（公開鍵）の2種類のファイルが生成されます．複数名でGPUマシンにアクセスすることを想定しているならば，`id_rsa_user1`など違いがあるとわかりやすいです（管理しやすく）．
参考：[ssh-keygenコマンドの使い方](https://qiita.com/hana_shin/items/2003698873a5782b7efd)

### ホスト名の簡略化（hostsファイルの編集）
まず，GPUマシンのIPアドレスを調べておきます．
```bash:terminal（サーバー側）
ip a
```
例：
```bash:terminal
inet 10.##.#.###  (わざと隠してます）
```

クライアント側のhostsファイルを開き，編集します．
```zsh:terminal（クライアント側）
sudo vi /etc/hosts
```
viの使い方は[こちら](https://eng-entrance.com/linux-command-vi)を参考にしました．
「コマンドモードと入力モードの切り替え」と「保存と終了」部分が参考になります．
hostsファイルに以下を追記します．
```
[UbuntuPCのIPアドレス] [Tabキー] [任意のホスト名]
```
[UbuntuPCのIPアドレス]には先程調べたIPアドレスを入力してください．
[Tabキー]はTabスペースを空けるという意味です．
[任意のホスト名]は`gpumachine`など覚えやすいホスト名で構いません．

※Windowsの場合`C:\windows\system32\drivers\etc\hosts`にあります．これを編集してください（管理者権限が必要かも）．

### 公開鍵の転送・登録
先ほど作成した公開鍵をGPUマシンに転送し，登録します．
パスワードの入力が求められた時は，GPUマシンにログインする時のパスワードを入力してください．
**macの場合**
```zsh:terminal（クライアント側）
ssh-copy-id -i ~/.ssh/id_rsa.pub [gpuマシンのユーザー名]@[gpuマシンのホスト名]
```
例：
```zsh:terminal（クライアント側）
ssh-copy-id -i ~/.ssh/id_rsa_user1.pub user@gpumachine
```
**Windowsの場合**
[こちら](https://qiita.com/tabu_ichi2/items/446722c15e6b5678ccad)を参考にするとssh-copy-idと同様のことができます．

### リモート接続時のコマンドを簡単にする
クライアント側の「.ssh」ディレクトリの「config」ファイルにエイリアスを定義することで、リモート接続時のコマンドを省略できます．
```text:config
Host [エイリアス１]
 HostName [リモートのホスト名]
 User [リモートユーザー]
 IdentityFile /Users/[user名]/.ssh/[秘密鍵]
 Port [ポート番号]
 ServerAliveInterval 60
 LocalForward   8888 localhost:8000
```
ポート番号は，あれば記述します．
ServerAliveIntervalはssh接続時に一定時間放置した時自動的に接続が切れてしまうのを防ぐために設定します．
LocalFowardはポートフォワーディングです．セットアップ時にエラーが出る場合は全て終わった後，設定してもいいかもしれません．
私はポートフォワーディングをconfigに予め書いておけることを後で知り，セットアップ後にconfigに追記しました．

※（2022年5月9日追記）
ポートフォワーディングに関して，例で示しているport番号で実行すると
クライアント側のport 8000 ↔ GPUマシンのport 8888 が紐づけされ，
後述の`docker run`時の-p オプションにより（例：`docker run -it --gpus all --name user1lab -v ~/Desktop/user1:/container/user1 -p 8000:8888 condalab-cuda-torch-jp`），最終的にクライアント側のport 8000 ↔ （GPUマシン経由） ↔ dockerコンテナ内のport 8888が紐づけされるイメージになります（間違っていたらごめんなさい）．
ポート番号は排他的に使用しなければいけないので（実行中の1コンテナごとに違うポート番号），configファイルでLocalForwardを例のように固定してる場合，2個目のコンテナを並列で立ち上げるためにはクライアント側で2つ目のconfigファイルを活用しないssh接続を行うなどの方法があります．
例として，2つ目のターミナルウィンドウで`ssh user名:ホスト名 -L 8888:
localhost:8001`でGPUマシンにssh接続し，`docker run -it --gpus all --name コンテナ名 -v ~~~~ -p 8001:8888 dockerimage_name`でコンテナ作成しコンテナ内でjupyterの起動をし接続するやり方でうまくいきました．
また，GPUマシンを複数人で共有する場合，利用者それぞれでconfig内LocalForwardを違うport番号で設定しておき，portが被るのを防ぐなどのやり方があると思います．

※（2022年6月1日追記）
実際に複数人でコンテナを起動し試したところ，以下のようにするとスムーズに実行できました．

1人目はconfigファイル内で`LocalForward   8888 localhost:8000`としておき，コンテナ`run`実行時オプションにて`-p 8000:8888`，jupyterの起動時`jupyter lab --ip=0.0.0.0 --allow-root --port 8888`とport番号（8888はデフォルト値）の指定をします．

2人目はconfigファイル内で`LocalForward   8889 localhost:8001`としておき，コンテナ`run`実行時オプションにて`-p 8001:8889`，jupyterの起動時`jupyter lab --ip=0.0.0.0 --allow-root --port 8889`とport番号の指定をします．

このように一人ひとりが以上の箇所で違う数値を用いることにより，同時にコンテナを実行できることを確認しました．

例：
```text:config
Host gpumachine
 HostName gpumachine
 User user
 IdentityFile /Users/user1/.ssh/id_rsa_user1
 ServerAliveInterval 60
 LocalForward   8888 localhost:8000
```
これで，sshで接続したい時は以下のコマンドのみでパスワード入力無しでssh接続できるようになります．
```zsh:terminal（クライアント側）
ssh gpumachine
```
# 4. ngrokを用いたWANからのssh接続
ここまで書いていたssh接続はLAN内のみで実行でき，外部ネットワークから接続したい場合，グローバルIPアドレスからアクセスするため，ルーターでの変換を行うための設定が必要でした．
[こちら](https://qiita.com/rintarou/items/bef523c7c8f11097e577)で紹介されているような設定でもWANからのssh接続が可能になるようですが，ルーターの設定をいじれない環境でもssh接続できるようにならないか調べたところ，うまくいったのがngrokを用いた接続でした．
## やりたいこと
- ngrokのインストール・設定・起動
## 手順
[こちら](https://mylops.jp/activity/blog/00069.html)のサイトが非常に参考になります．
1. https://ngrok.com/ でアカウントを作成してください（無料）．
1. GPUマシンにUbuntu（Linux）用のngrokをDL，インストールしてください．
1. ngrok公式サイトで発行した自アカウントのauthtokenを，インストールしたGPUマシン内で登録してください．
1. `./ngrok tcp 22 --region jp`を実行することでWANからsshする際のホスト名（IPアドレス）を発行できます．

※`--region jp`のオプションでJPサーバーを利用できpingを最小限に抑えることができます．

GPUマシンのterminalの画面がngrokの情報が書かれた画面に変わった後，その中のForwardingという項目のアドレスをメモしておきましょう．
Forwardingの例：
`tcp://0.tcp.jp.ngrok.io:12345 -> localhost:22`
ここで，ホスト名は`0.tcp.jp.ngrok.io`，ポート番号は`12345`になります．

ちなみにGPUマシンはシャットダウンせずに稼働させっぱなしな前提で話を進めています．
というのも，ngrokの無料プランはngrokを再起動してしまうとホスト名の最初の数字（0など）やポート番号が変わってしまうからです．
ngrok起動中にGPUマシンを直接操作する人がいる時はngrokが終了しないようにterminalの別ウィンドウや別タブを操作してもらいましょう．

ここで，ngrok経由での接続用にhostsファイルとconfigファイルに追記を行います．
ngrok経由で接続する時用に別の秘密鍵・公開鍵の作成，GPUマシンへの転送・登録も行いました．

例：
```text:hosts
0.tcp.jp.ngrok.io:12345    nggpumachine
```
```text:config
Host nggpumachine
 HostName 0.tcp.jp.ngrok.io
 User user
 IdentityFile /Users/user1/.ssh/id_rsa_user1_ngrok
 Port 12345
 ServerAliveInterval 60
```
ngrokを再起動した際にはhostsファイルとconfigファイルの内容を修正する必要があります．
これでWANからsshで接続した時は，以下のようなコマンドで接続できます．
```zsh:terminal（クライアント側）
ssh nggpumachine
```
# 5. Dockerコンテナの作成，起動，カスタマイズ，保存
## やりたいこと
- クライアントPCからsshでGPUマシンに接続し，その中でDockerを用いてJupyter Labが使えるPyTorchの仮想実行環境を構築する
- Dockerの中に入れるもの
    - cuda 11.3.1
    - Anaconda
    - Python 3.9.7
    - PyTorch 1.10.2
    - Jupyter Lab
    - ＋好きなライブラリなど

**※今回は，nvidia/cudaのDockerイメージをベースにAnacondaを追加し，その他のライブラリを追加していく，といったやり方の例を載せています．**

※容量を抑えた[軽量版Docker Image用の記事](https://qiita.com/greentiger/items/e1a8251981fed3495599)を作ったのでGPUマシンの容量を気にしている方はこちらもご覧ください．

もちろんPyTorchのDockerイメージをベースにするなど，色々やり方はあると思います．
また，GPUの型番やOSのバージョンに合わせて，GPUドライバーやcuda，Python，PyTorchのバージョンが対応しているものを選ぶ必要があります．
GPUマシンの容量削減をしたい場合は最小限の構成でコンテナを作ると良いです．

私が試した最小構成は，Docker HubからUbuntuのOfficial Imageをpullし（`docker pull ubuntu:20.04`），これをベースにPython3をインストールし，下記の`installnodejs.command`を行い，pipでjupyter labや好きなライブラリーを追加インストールする，といった手順です．

つまり，やり方はかなり自由だということですね．

## 手順
ここからはsshでGPUマシンに接続した状態のterminalで操作するか，
GPUマシンを直接触ってGPUマシンのterminalで操作してください．
### GPUマシンにクライアントPCから接続
GPUマシンで直接操作する場合，ここは飛ばしてOKです．
```zsh:terminal（クライアント側）
ssh gpumachine
```
### cudaのDockerイメージをpull
```bash:terminal
docker pull nvidia/cuda:11.3.1-cudnn8-devel-ubuntu20.04
```
### 新規コンテナ作成 ＆ run
```bash:terminal
docker run -it --gpus all --name <コンテナ名> -v ~/Desktop/user1:/container/user1 nvidia/cuda:11.3.1-cudnn8-devel-ubuntu20.04
```
今回は，`コンテナ名`を`dockertest`とします．
`~/Desktop/user1:/container/user1`部分に関して，
GPUマシンの`~/Desktop/user1`フォルダを仮想環境内の`/container/user1`フォルダと同期することで，
仮想環境外からデータを取ってきたり，仮想環境外にデータを保存することができます．
### 各種インストール
Anacondaをインストールして，condaコマンドを使えるようにします．
`~/Desktop/user1`フォルダに`condainstall.command`ファイルを作成しておき，
仮想環境内で`$ bash condainstall.commnad`を実行するか，もしくはコマンドラインで1行ずつ実行します．
途中で地域を入力する必要があるので，「Asia」，「Tokyo」を選択してください．
最後にAnacondaのinitializeをするか入力を求められますが，私は「yes」にしました．
Anacondaに関して，[こちら](https://reiven-c-t.github.io/Programming/ubuntu-anaconda-only-commandline.html)のサイトが参考になりました．
```bash:condainstall.command
apt update
apt upgrade
apt install curl
apt install vim
apt install wget
apt install ffmpeg
apt install imagemagick
mkdir anaconda_installation
cd anaconda_installation
curl -O https://repo.anaconda.com/archive/Anaconda3-2021.11-Linux-x86_64.sh
chmod +x Anaconda3-2021.11-Linux-x86_64.sh
./Anaconda3-2021.11-Linux-x86_64.sh
source ~/.bashrc
exec $SHELL -l
```
次に，PyTorchとnode.jsのインストールを行います．
node.jsがないと動かないJupyter Labの拡張機能がいくつかあるので，インストールします．
condaコマンドで何かインストールする時はコマンドラインの最初が(base)になっていないと実行できないので注意してください．
`exec $SHELL -l`などでbashの再起動をすると(base)が現れると思います．
```bash:installnodejs.command
apt install nodejs npm
npm install -g n
n stable
apt purge nodejs npm
exec $SHELL -l
source ~/.bashrc
```

```bash:installtorch.command
conda install pytorch torchvision torchaudio cudatoolkit=11.3 -c pytorch
source ~/.bashrc
```

最後にJupyter Labをより使いやすくするために拡張機能（よく使うライブラリも）をインストールします．ここはお好みで…
私のおすすめの拡張機能を以下に貼っておきます．
```bash:labcustomize.command
pip install jupyterlab-language-pack-ja-JP
pip install lckr-jupyterlab-variableinspector
pip install jupyterlab_nvdashboard
pip install ipywidgets
pip install black
pip install yapf
pip install isort
pip install jupyterlab_code_formatter
pip install jupyterlab_tabnine
pip install jupyterlab-lsp
pip install 'python-lsp-server[all]'
pip install pprintpp
pip install jupyterlab-spreadsheet-editor
pip install ipympl
```
### Docker Imageの保存
全ての追加インストールが完了した後，コンテナから抜けます．
```bash:terminal
exit
```
現在のdockertestの環境を保存しておきます．
```bash:terminal
docker commit dockertest <新規Image名>
```
`新規Image名`は好きに決めてOKです．
例：
```bash:terminal
docker commit dockertest condalab-cuda-torch-jp
```
commitがうまくいかない場合はdockertestがまだ動いてる可能性が多いです．
以下で終了できます．
```bash:terminal
docker stop dockertest
```
保存が完了しているかはDocker Imageの一覧を表示することで確認できます．
```bash:terminal
$ docker images

REPOSITORY                  TAG                               IMAGE ID       CREATED         SIZE
condalab-cuda-torch-jp      latest                            791d31a682c2   3 hours ago     13.3GB
nvidia/cuda                 11.3.1-cudnn8-devel-ubuntu20.04   c8217e621925   2 months ago    8.26GB
```
# 6. 作業用コンテナの新規作成，Jupyter Lab起動
一度セットアップが完了していれば，
ここ以降のやり方で，新規コンテナを作成して開発スペースを増やせます（2人目以降や1人で複数コンテナ使いたい方）．
## やりたいこと
- コンテナ作成
- jupyter起動
- ブラウザでjupyterを開く
- jupyterの終了

## 手順
### コンテナ作成
```bash:terminal
docker run -it --gpus all --name user1lab -v ~/Desktop/user1:/container/user1 -p 8000:8888 condalab-cuda-torch-jp
```
このコンテナでメインの開発をするというイメージです．
追加で色々入れても大丈夫ですし，もしもエラーや不具合が多発するようでしたら，このコンテナを消して，新たなコンテナを`condalab-cuda-torch-jp`ベースで作ることができます．

### Jupyterの起動
```bash:terminal
jupyter lab --ip=0.0.0.0 --allow-root
```
と打つと，コマンドライン上に
```text:terminal
To access the server, open this file in a browser:
        file:///root/.local/share/jupyter/runtime/jpserver-####-open.html
    Or copy and paste one of these URLs:
        http://42f4cd####:8888/lab?token=###############
     or http://127.0.0.1:8888/lab?token=##############
```
といったURLが表示されます．
一番下の`http://127.0.0.1:8888/lab?token=##############`をコピーしておきます．
### ブラウザで開く
Chromeなどのブラウザを開き，アドレス入力欄に，
`http://127.0.0.1:8888/lab?token=##############`
もしくは，`localhost::8888/lab?token=##############`と入力，
Enterキーを押すことでJupyter Labを開くことができます．
Jupyter Labの使い方は記述しませんが，検索したら色々出てくると思います．
### Jupyterの終了
Jupyter実行中のterminalで`<Ctrl + C>`を押すと終了できます．
その後，`exit`でコンテナを抜けられます．

# 7. MacのFinder上でUbuntuマシンとファイルやり取り
GPUマシンのディレクトリをローカルPCにマウントすることで，
GPUマシン内のフォルダやファイルを直感的にコピー・移動することができます．ローカルPCとGPUマシン間のフォルダ，ファイルのやり取りも可能です．
以下の記事がわかりやすくおすすめです（mac）．
[Macでsshfsを使ってローカルからリモートのファイルを触る](https://qiita.com/ysk24ok/items/bb148530a55a4e55d99b)

また，Windowsの場合はwin-sshfsを使うと同様のことができます．
以下の記事が非常に参考になります．
[Windows10にwin-sshfsをインストールする方法](https://it-mikeiken.com/win-sshfs-install)
※Dokanは最新版ではなくDokan_x64-1.0.0を使わないとエラーになります．

# 8. よく使うDockerコマンド
私がよく使うDockerコマンドです．使っているうちに勝手に覚えてしまいます．
### ローカルにあるイメージの表示
```bash:terminal
docker images
```
### 起動中のコンテナ表示
```bash:terminal
docker ps
```
### コンテナ表示（停止中含む）
```bash:terminal
docker ps -a
```
### Docker Imageから新規コンテナ生成＆実行
```bash:terminal
docker run -it --gpus all --name <コンテナ名> -v <マウント元ディレクトリpath>:<マウント先コンテナ内path> -p <外部からアクセスされるポート番号>:<コンテナ側のポート番号> <Docker Image ファイル名>
```
※ `--gpus all` と `-v <マウント元ディレクトリpath>:<マウント先コンテナ内path>`，`-p <外部からアクセスされるポート番号>:<コンテナ側のポート番号>`は必要に応じて使います．
### コンテナ起動
```bash:terminal
docker start <コンテナ名 or ID>
```
### コンテナ停止
```bash:terminal
docker stop <コンテナ名 or ID>
```
### 起動中のコンテナに入る
```bash:terminal
docker exec -it <コンテナ名 or ID> bash
```
### コンテナから抜ける（コンテナ内の実行中の作業は停止する）
```bash:terminal
exit
```
### コンテナから抜ける（コンテナ内の実行中の作業は中止しない）
```bash:terminal（キー入力）
<Ctrl+p Ctrl+q>
```
### コンテナの削除（停止しているコンテナしか削除できない）
```bash:terminal
docker rm <コンテナ名 or ID>
```
### 保存されているDocker Images一覧
```bash:terminal
docker images
```
### Image削除
```bash:terminal
docker rmi <イメージID or イメージファイル名>
```
# 9. まとめ
今回の手順の全体の流れをまとめると以下のようになります．
1. ssh接続の設定（鍵発行など）
1. cudaのDocker ImageをベースにAnaconda，PyTorch，Jupyter Lab拡張機能などを追加したコンテナを作成
1. 作成したコンテナをcommitし，Imageとして保存
1. Jupyter Labの起動方法の簡単な紹介

細かいところまで書いてしまっていますが，必ずしもこのようにしないとできないわけではないので勘違いしないでください．
私自身がやり方を忘れてしまったときの備忘録としても使えるように，細かく書きました．
# 最後に
まだまだDocker初心者なので，変な表現をしている点や間違ったやり方をしているかもしれません．
ミスなどありましたら，コメントでご指摘の程よろしくお願いします．

ここまで読んでくださり，ありがとうございました．