CLAMリポジトリにDocker＋VSCode環境を実装してみた

## 概要
共同での研究や開発において、環境の違いによる問題を回避するために  
統一された便利な`Docker`コンテナ環境を持つことには利点があると思われます。   
　
また、VSCodeを用いて作業を行うことで、  
コードの変更や動作確認、デバッグなどが行いやすくなると考えられます。  

本記事では、`github`から取得できる`CLAM`リポジトリについて、  
`Dockerコンテナ`構築手順を用意し、  
`コンソール`または`VSCode`から扱えるような手順を紹介しております。  

:::note info
CLAMは2020年にハーバード大学医学部ブリガム・アンド・ウイメンズ病院 病理部のMing Y.Lu氏らが発表した、スライド画像から特徴量を抽出し、スライド単位のラベルを用いて弱教師あり学習でクラス分類を行うスクリプトです。
:::

今回の例は`CLAM`ですが、`CLAM`以外でも、DockerとVSCodeを連携させた  
環境が用意されていないリポジトリを、共同で使ってみたい場合などの参考になればと思います。  
　
なお、`GPU`を用いない場合と用いる場合の両方に対応できるよう記載しております。  
(`Docker`が`GPU`をサポートするためには、  
事前に`nvidia-container-toolkit`をインストールしている必要があります)  

## 環境構築用ファイル作成手順
### 手順１
`CLAM`のリポジトリをクローン

```
git clone https://github.com/mahmoodlab/CLAM.git
```

### 手順２
取得した`CLAM`リポジトリに移動し、`docker`ディレクトリを作成
```
cd CLAM
mkdir docker
```
### 手順３
`docker`ディレクトリに、`Dockerfile`や`docker-compose.yml`等で参照する環境変数を設定するシェルファイル`init_env.sh`を作成

<details><summary>init_env.sh</summary>  

```bash
#!/bin/bash
set -euo pipefail

# 本ファイルのあるディレクトリの１つ上のディレクトリに移動
cd $(dirname $0)/..

REPLACE_USERNAME=$(echo $USER |sed 's/\./_/g')

# ホストのユーザー名がrootの場合、REPLACE_USERNAME="developer"に指定
if [ "$USER" = "root" ]; then
  REPLACE_USERNAME="developer"
fi

# .envファイルがない場合、下記内容の環境変数ファイルを作成
if [ ! -f .env ]; then
    cat <<EOT > .env

COMPOSE_PROJECT_NAME=clam_${REPLACE_USERNAME}
USERNAME=$REPLACE_USERNAME
HOST_UID=`id -u`
HOST_GID=`id -g`
EOT
fi
```
</details>

### 手順4
`docker`ディレクトリに`Dockerfile`を作成

:::note info 
`apt-get install`や`pip install`などの項目は`CLAM`の`env.yml`をベースに作成します。
:::

<details><summary>Dockerfile</summary>

```yaml
# ベースイメージ (CPUの場合)
FROM ubuntu:22.04 AS base
# ベースイメージ (GPUの場合[Nvidia])
# FROM nvidia/cuda:12.1.0-devel-ubuntu22.04 AS base

# 時刻設定(東京)
RUN ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

# システムパッケージ更新とbuild-essential, python3.10関連パッケージ
# 及び、OpenSlide, OpenCV関連パッケージのインストール
RUN apt-get update && \
    apt-get upgrade -y && \
    apt-get install -y build-essential python3.10 python3.10-distutils python3-pip libopenslide-dev libgl1 git

## GPUの場合に下記を設定 (CUDA関連追加設定)
# CUDAホームディレクトリを環境変数CUDA_HOMEに設定
# ENV CUDA_HOME=/usr/local/cuda
# CUDAの実行可能ファイル（例: nvcc）ディレクトリを環境変数PATHに追加。これにより、nvccなどをコマンドとして直接実行可能に
# ENV PATH=$CUDA_HOME/bin:$PATH
# CUDAライブラリ（例:cuDNN）があるディレクトリを動的ライブラリの検索パスに追加。これにより、アプリケーションがCUDAライブラリをロード可能。
# ENV LD_LIBRARY_PATH=$CUDA_HOME/lib64:$LD_LIBRARY_PATH
# Pytorchで使用するGPUアーキテクチャを指定
# ENV TORCH_CUDA_ARCH_LIST="6.0;6.1;7.0;7.5;8.0;8.6"

# CLAMのenv.ymlに合わせてpipパッケージをインストール
RUN pip install --upgrade pip &&\
    pip install ipykernel &&\
    pip install pandas pyyaml opencv-python matplotlib scikit-learn scipy tqdm &&\
    pip install timm==0.9.8 torch torchvision tensorboardx &&\
    pip install openslide-python &&\
    pip install h5py &&\
    pip install git+https://github.com/oval-group/smooth-topk.git

# 言語設定(日本語)
RUN apt update && apt install -y language-pack-ja && update-locale LANG=ja_JP.UTF-8
RUN apt install -y fonts-takao fonts-takao-gothic fonts-takao-pgothic fonts-takao-mincho
ENV LANG jp_JP.UTF-8
ENV XMODIFIERS @im=fcitx
ENV GTK_IM_MODULE fcitx
ENV QT_IM_MODULE fcitx
ENV LC_CTYPE ja_JP.UTF-8

# dev(開発用)
FROM base AS dev

# 開発などで良く用いられるLinuxツール+nodejs(markdownlintインストール用)
RUN apt update && apt install -y \
    curl \
    gnupg \
    sudo \
    nano \
    vim \
    wget \
    && apt purge -y nodejs \
    && curl -sL https://deb.nodesource.com/setup_22.x | bash - \
    && apt install -y nodejs \
    && apt clean && rm -rf /var/lib/apt/lists/*

# コードの書式等チェック用linter, formatter, testerインストール
RUN pip install \
    codespell==2.3.0 \
    pytest==8.2.1 \
    ruff

# マークダウンファイル(README.mdなど)の書式チェック用
RUN npm install -g --save-dev markdownlint-cli2@0.13.0

# シンボリックリンク作成("python OOO.py"でコマンド実行できるようにするため)
RUN ln -s /usr/bin/python3 /usr/bin/python

# ipykernelの設定(jupyter notebook等を用いる場合、別途インストールが必要)
RUN python -m ipykernel install --user --name=sysmtem-python --display-name "Python(system)"

# 開発用ユーザー追加
ARG USERNAME=developer
ARG USER_UID
ARG USER_GID

# ホストのユーザーID,グループIDをコンテナ内のIDに割り当てるが、
# コンテナ環境で既存のグループID、ユーザーIDの場合は、
# ユーザーID=1000, グループID=1000を割り当て
RUN USER_GID=$(getent group $USER_GID >/dev/null && echo 1000 || echo $USER_GID) \
    && USER_UID=$(getent passwd $USER_UID >/dev/null && echo 1000 || echo $USER_UID) \
    && groupadd --gid $USER_GID $USERNAME \
    && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \
    && echo "$USERNAME ALL=(root) NOPASSWD:ALL" > /etc/sudoers.d/$USERNAME \
    && chmod 0440 /etc/sudoers.d/$USERNAME

# 指定したユーザー名でコンテナをビルド
USER $USERNAME

CMD ["usr/bin/bash"]
```
</details>

### 手順5
`CLAM`のトップディレクトリに移動し、`docker-compose.yml`を作成

<details><summary>docker-compose.yml</summary>

```yaml
# プロジェクト名。コンテナ名等のprefixとして付与される
name: clam
# コンテナ全体の設定
services:
  # service内の１つのサービス名(任意名)
  dev:
    # コンテナにホストと同等の特権を持たせる
    privileged: true
    # Dockerのimage名
    image: clam:dev-${USERNAME}
    # docker build条件
    build:
      # コンテナビルドの基準になるディレクトリ
      # `docker-compose.yml`から見て`Dockerfile`が格納されたディレクトリパス
      # `docker-compose.yml`が`clam/`にあり、`Dockerfile`は`clam/docker`にあるため、
      # `docker`を指定
      context: docker
      # Dockerfileの指定
      dockerfile: Dockerfile
      # ビルドの際のステージを指定(開発用の部分が不要の場合はbase)
      # 他のステージをDockerfileに追加しtargetに指定してもOK
      target: dev
      args:
        - USER_UID=${HOST_UID}
        - USER_GID=${HOST_GID}
    # nvidiaのGPU(id=0を指定)を用いる場合
    # deploy:
      # resources:
        # reservations:
          # devices:
            # - driver: "nvidia"
              # capabilities: ["gpu"]
              # nvidia-smiで確認できるGPU IDを指定
              # device_ids: ["0"]
    user: "${HOST_UID}:${HOST_GID}"
    # ホスト側のネットワークを指定
    # 別途ポートを指定する場合、`network_mode:`は記載せず、
    # 代わりに`ports:`を記載し`- "8888:8888"`(ホストのポート:コンテナのポート)
    # のようにポート番号を指定する
    network_mode: "host"
    # ボリューム設定
    volumes:
      # ホストのカレントディレクトリをコンテナ内の/workspaces/clamにマウント
      # (ホストでの変更を動的にコンテナに反映)
      - .:/workspaces/clam
      # ホストのデバイスディレクトリをコンテナ内の/devにマウント
      # (GPUやUSBデバイスへのアクセス用)
      - /dev:/dev
    # コンテナ内の作業ディレクトリ
    working_dir: /workspaces/clam
    # 対話型操作用
    stdin_open: true
    # docker execでアクセス用
    tty: true
    # bash -c "tail -f /dev/null"はコンテナをバックグラウンドで起動し続けるため
    # コンテナ内で常にJupyter等の他のプロセスを起動する場合は必要なし
    command: >
      bash -c "tail -f /dev/null"
```
</details>

### 手順6
開発に用いる`VSCode`の`Dev Container機能`用に`.devcontainer`ディレクトリを用意し、
ホストと同様に`ssh`や`git`を使用するための`補足用docker-compose.yml`を作成

<details><summary>docker-compose.yml</summary>

```yaml
services:
  dev:
    user: developer
    # コンテナ内のdeveloperユーザーがsshやgitを使用できるように指定
    volumes:
      - ~/.ssh:/home/developer/.ssh:ro
      - ~/.gitconfig:/home/developer/.gitconfig
```
</details>

### 手順7
`.devcontainer`ディレクトリに、  
`VSCode`の`Dev Container機能`用の`devcontainer.json`を用意  

<details><summary>devcontainer.json</summary>

```json
{
    "name": "clam",
    "dockerComposeFile": [
        "../docker-compose.yml",
        "docker-compose.yml"
    ],
    "service": "dev",
    "workspaceFolder": "/workspaces/clam",
    "containerEnv": {
        "SHELL": "/bin/bash"
    },
    "customizations": {
        "vscode": {
            "extensions": [
                "ms-python.python",
                "ms-python.vscode-pylance",
                "ms-python.mypy-type-checker",
                "charliermarsh.ruff",
                "DavidAnson.vscode-markdownlint"
            ]
        }
    }
}
```
</details>

**`devcontainer.json`について補足**  

・`dockerComposeFile`には`.devcontainer`にある  
　`docker-compose.yml`と`CLAM`ディレクトリの直下にある`docker-compose.yml`を指定  　
・`service`には`docker-compose.yml`で指定してある`dev`を指定  
・`workspaceFolder`には、コンテナ内の作業ディレクトリのパスを指定  
・`containerEnv`では、コンテナ内の環境変数を設定  
・
```json
"customizations": {
        "vscode": {
            "extensions": [
```
には、`VSCode`にインストールする拡張機能を指定  
(`python`系に加え、今回は書式をチェックするもの等指定)  


### 手順8
トップディレクトリに`README.md`を作成し、
環境構築手順が分かるようにする。

<details><summary>README.md例</summary>

# CLAM
## 環境構築

`Docker`コンテナでの使用を想定した手順です

### 1. リポジトリを取得

```console
git clone https://github.com/mahmoodlab/CLAM.git
```

### 2. `.env`ファイルの作成

ホストの`UID`及び`GID`を使用するために、`.env`ファイルを作成します

```console
./docker/init_env.sh
```

### 3. Dockerイメージのビルド及びコンテナの起動

下記２つの方法があります。(`VSCode`推奨)  

**[`VSCode`の拡張機能(`Dev Container`)を用いた方法]**  

下記を実施することで、VSCodeのDev Containerを使用し環境構築を行うことができます

・`VSCode`でプロジェクトのルートディレクトリを開く  
  
・`Ctrl`と`Shift`を押しながら`P`を押してコマンドパレットを開く  
 
・コマンドパレットから`Reopen in Container(コンテナーで再度開く)`を選択する  

・コンテナ内の環境が開く
 

**[`Docker compose`を用いた方法]**  

```console
docker compose build dev
docker compose run dev
```

上記実行後に、別のウインドウ又はタブからコンテナに入ります  

```console
docker exec -it コンテナ名 /bin/bash
```

</details>

## 環境構築手順

作成したファイルを用いて環境の構築を実施します  

### 方法1 (`VSCode`)

1. `VSCode`で`CLAM`リポジトリを開く  
　
2. `Ctrl & Shift + P`で開くメニューから、  
`Dev Containers: Reopen in Container`(コンテナで再度開く)を選択  
　
3. 新たにコンテナ内の処理ウインドウが開きます  
このウインドウのコンソールから、  
`create_patches_fp.py`などの`CLAM`のプログラムを実行します
　
4. コンテナ外のコンソールから、コンテナの稼働状況を確認  

```console
PS D:\OOO\CLAM> docker ps
CONTAINER ID   IMAGE            COMMAND                   CREATED       STATUS       PORTS     NAMES
3b8e557ab872   clam:dev-OOO   "/bin/sh -c 'echo Co…"   3 hours ago   Up 3 hours             clam_developer-dev-1 
```

### 方法2 (`docker compose`コマンド)

1. コンソールにて、`CLAM`ディレクトリから、`docker compose build dev`を実行  

```console
root@OOOOO:/mnt/d/OOOOO/CLAM# docker compose build dev
[+] Building 1014.2s (16/16) FINISHED                                                                                      docker:default
 => [dev internal] load build definition from Dockerfile                                                                             0.1s
 => => transferring dockerfile: 2.39kB                                                                                               0.0s
 => [dev internal] load metadata for docker.io/library/ubuntu:22.04                                                                  2.5s
 => [dev internal] load .dockerignore                                                                                                0.1s
 => => transferring context: 2B                                                                                                      0.0s
 => [dev base 1/6] FROM docker.io/library/ubuntu:22.04@sha256:0e5e4a57c2499249aafc3b40fcd541e9a456aab7296681a3994d631587203f97       4.4s
 => => resolve docker.io/library/ubuntu:22.04@sha256:0e5e4a57c2499249aafc3b40fcd541e9a456aab7296681a3994d631587203f97                0.0s
 => => sha256:0e5e4a57c2499249aafc3b40fcd541e9a456aab7296681a3994d631587203f97 6.69kB / 6.69kB                                       0.0s
 => => sha256:3d1556a8a18cf5307b121e0a98e93f1ddf1f3f8e092f1fddfd941254785b95d7 424B / 424B                                           0.0s
 => => sha256:97271d29cb7956f0908cfb1449610a2cd9cb46b004ac8af25f0255663eb364ba 2.30kB / 2.30kB                                       0.0s
 => => sha256:6414378b647780fee8fd903ddb9541d134a1947ce092d08bdeb23a54cb3684ac 29.54MB / 29.54MB                                     3.0s
 => => extracting sha256:6414378b647780fee8fd903ddb9541d134a1947ce092d08bdeb23a54cb3684ac                                            1.1s
 => [dev base 2/6] RUN ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime                                                          3.5s
 => [dev base 3/6] RUN apt-get update &&     apt-get upgrade -y &&     apt-get install -y build-essential python3.10 python3.10-d  148.8s
 => [dev base 4/6] RUN pip install --upgrade pip &&    pip install ipykernel &&    pip install pandas pyyaml opencv-python matplo  775.2s
 => [dev base 5/6] RUN apt update && apt install -y language-pack-ja && update-locale LANG=ja_JP.UTF-8                               9.8s
 => [dev base 6/6] RUN apt install -y fonts-takao fonts-takao-gothic fonts-takao-pgothic fonts-takao-mincho                          7.8s
 => [dev dev 1/6] RUN apt update && apt install -y     curl     gnupg     sudo     nano     vim     wget     && apt purge -y nodej  27.7s
 => [dev dev 2/6] RUN pip install     codespell==2.3.0     pytest==8.2.1     ruff                                                    4.6s
 => [dev dev 3/6] RUN npm install -g --save-dev markdownlint-cli2@0.13.0                                                            10.7s
 => [dev dev 4/6] RUN ln -s /usr/bin/python3 /usr/bin/python                                                                         0.5s
 => [dev dev 5/6] RUN python -m ipykernel install --user --name=sysmtem-python --display-name "Python(system)"                       0.9s
 => [dev dev 6/6] RUN USER_GID=$(getent group 0 >/dev/null && echo 1000 || echo 0)     && USER_UID=$(getent passwd 0 >/dev/null &&   0.5s
 => [dev] exporting to image                                                                                                        17.0s
 => => exporting layers                                                                                                             17.0s
 => => writing image sha256:75c16e5d90aaafc5ccf569ca8f55ec640f939a6f56a08b3bfaf7c03eafcec396                                         0.0s
 => => naming to docker.io/library/clam:dev-developer   
```
　
2. `docker images`でイメージが作成されていることを確認  

```console
root@AlzaPC:/mnt/d/OOO/CLAM# docker images
REPOSITORY                 TAG         IMAGE ID       CREATED         SIZE
clam                       dev-OOO   ceb4480de052   4 hours ago     10.3GB
ubuntu                     22.04       97271d29cb79   2 months ago    77.9MB
continuumio/anaconda3      latest      de606c8c8d2d   9 months ago    4.45GB
docker/welcome-to-docker   latest      c1f619b6477e   12 months ago   18.6MB
```

3.`docker compose run dev`でコンテナを起動  

```console
root@AlzaPC:/mnt/d/OOO/CLAM# docker compose run dev
```

4.コンテナに入る際は、別のタブなどからコンソールを開き、下記を実行
```console
docker exec -it コンテナ名(clam-OOO) /bin/bash
```
5.下記のように`docker`内の作業環境が開きます
　ここから、`create_patches_fp.py`などの`CLAM`のプログラムを実行します
```console
developer@docker-desktop:/workspaces/clam$ 
```

環境構築方法は以上になります。  
構築した環境で`CLAM`のコードを実行することができます。  

実際に構築した例について、[github](https://github.com/Akihiko0123/CLAM_docker_vscode)に公開しておりますので、ご参考にしていただけましたら幸いです。