WSL2にDocker+PyTorch(GPU)環境を構築

以下のPCに、NVIDIA GPUを利用できる、Docker+PyTorch環境を構築します。

- CPU: AMD Ryzen 7 3700X
- GPU: NVIDIA GeForce GTX 1660 SUPER
- OS: Windows 11 Home (23H2） , WSL2 + Ubuntu24.04

構築にあたっては、主に以下記事を参照させていただきました。

https://qiita.com/YuH25/items/58fa7109ac4579780a85

# 概要

この記事では、以下の開発環境を構築する手順を紹介します。

- WSL2+Ubuntu24.04上で動作するDockerコンテナ
  - NVIDIA GPUでアクセラレートできるPyTorch（※）が動作
  - ユーザー権限で動作
  - コンテナ内ホームディレクトリのデータ永続化（コンテナを停止してもデータが消えない）

（※）PyTorch: python向けの機械学習ライブラリ

手順はざっと以下になります。

1. Dockerのインストール
1. NVIDIAドライバのインストール
1. Dockerコンテナの構築、起動

2のNVIDIAドライバのインストールだけがWindows上での作業で、あとは、WSL2のUbuntu上での作業になります。

# 1. Dockerのインストール（Ubuntu）

Dockerとは、仮想化技術を使用してアプリケーションの実行環境を１台のPCに複数構築できるソフトです。１台のPCで色んな開発をしていると、別開発の環境を壊してしまったりといったことはよくありますが、Dockerで、そういう心配を減らせたりします。詳細は[ここ](https://qiita.com/riku_ogawa/items/4ffc6aaf7a3df053a4ad)とかに、わかりやすい説明がありました。

https://qiita.com/riku_ogawa/items/4ffc6aaf7a3df053a4ad

WSL2のUbuntuに、Dockerをインストールします。
WSL2のUbuntuターミナルで、以下コマンドを順に実行します。
```
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
```

```
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
```
```
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
```
これでDockerインストールは完了なのですが、これだけだと、Dockerをrootユーザーしか利用できないので、一般ユーザー（現在のUbuntuユーザー）で利用できるようにするため、以下を実行します。

```
sudo groupadd docker
sudo usermod -aG docker $USER
newgrp docker
```

# 2. NVIDIAドライバのインストール

## 2.1 NVIDIAドライバ（Windows）

Windowsに、NVIDIAドライバをインストールします。
[NVIDIA公式サイト](https://www.nvidia.com/ja-jp/drivers/)からドライバのインストーラをダウンロードして、インストールします。

https://www.nvidia.com/ja-jp/drivers/

既にドライバがインストール済であれば不要です。大抵は、既になんらかのドライバがインストール済だと思われます。何がインストールされているかは、画面右下から緑のNVIDIAアイコンをクリックし、ドライバタブで確認できます。
![nvidia設定_ドライバ確認.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1299377/856a0421-ac74-a23a-5078-de4c20039f9e.png)


自分のPCに装着されているGPUは、タスクマネージャで確認できます。
![task_manager.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1299377/9e49f490-d74f-8588-068b-1b5547df6b37.png)


## 2.2. NVIDIA Container Toolkitのインストール（Ubuntu）

NVIDIA Container Toolkitは、Dockerコンテナ内でドライバを利用できるよう、必要なホスト側(WSL2+Ubuntu)のデバイスファイル等の一式をマウントしてくれるツールなんだそうです。詳細は以下記事を参照。

https://qiita.com/tkusumi/items/f275f0737fb5b261a868


WSL2のUbuntuに、NVIDIA Container Toolkitをインストールします。
WSL2のUbuntuターミナルで、以下コマンドを順に実行します。
これは、[NVIDIA公式サイト](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)の「Installing with Apt」に載っているものです。

```
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg \
  && curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
    sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
    sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
```

```
sudo sed -i -e '/experimental/ s/^#//g' /etc/apt/sources.list.d/nvidia-container-toolkit.list
sudo apt-get update
sudo apt-get install -y nvidia-container-toolkit
```

https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html

`nvidia-smi`で、インストールできたかを確認できます。成功すると以下が出力されます。

```
$ nvidia-smi
Sat Dec  7 17:44:31 2024
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 565.51.01              Driver Version: 565.90         CUDA Version: 12.7     |
|-----------------------------------------+------------------------+----------------------+
| GPU  Name                 Persistence-M | Bus-Id          Disp.A | Volatile Uncorr. ECC |
| Fan  Temp   Perf          Pwr:Usage/Cap |           Memory-Usage | GPU-Util  Compute M. |
|                                         |                        |               MIG M. |
|=========================================+========================+======================|
|   0  NVIDIA GeForce GTX 1660 ...    On  |   00000000:05:00.0  On |                  N/A |
| 32%   36C    P8             16W /  125W |     707MiB /   6144MiB |      1%      Default |
|                                         |                        |                  N/A |
+-----------------------------------------+------------------------+----------------------+

+-----------------------------------------------------------------------------------------+
| Processes:                                                                              |
|  GPU   GI   CI        PID   Type   Process name                              GPU Memory |
|        ID   ID                                                               Usage      |
|=========================================================================================|
|    0   N/A  N/A        33      G   /Xwayland                                   N/A      |
+-----------------------------------------------------------------------------------------+
```

# 3. Dockerコンテナの構築、起動（Ubuntu）

ここから、Dockerコンテナ内にPyTorch開発環境を構築します。Dockerコンテナは、開発環境の入れ物のようなものです（Docker初心者なので間違っていたらすみません）。

## 3.1 全体像

以下Dockerfileを作成し、ビルドします。
コマンドが長く入力が面倒なので、シェルスクリプト（docker_build.sh）にまとめました。container_init.shは、Dockerコンテナ起動時のスクリプトです。
ビルド後は、作成したコンテナを起動します。こちらもシェルスクリプト（docker_run.sh）にまとめました。

PC環境に合わせてカスタマイズが必要な箇所は「★★★」コメントで記載しました。GPUに合わせたCUDA（※）バージョンの選択に関するところです。
（※）CUDA：NVIDIA GPUのアプリ開発環境

個々の箇所については、以降の節で説明します。

なお、ファイルはgithubにも上げておきましたので、まとめてダウンロードしたい方はこちらをどうぞ。

https://github.com/Shimasaki2021/docker_pytorch

```docker:Dockerfile
FROM ubuntu:24.04

# ------------------
#  一般ユーザー追加
# ------------------
ARG USERNAME=user
ARG GROUPNAME=user
ARG UID=1000
ARG GID=1000

#   追加しようとしているユーザー／グループが存在していれば削除
RUN if [ `cat /etc/group | grep ${GID} | awk -F':' '{print $1}'` != "" ]; then \
        groupdel -f `cat /etc/group | grep ${GID} | awk -F':' '{print $1}'`; \
fi
RUN if [ `cat /etc/passwd | grep ${UID} | awk -F':' '{print $1}'` != "" ]; then \
        userdel -r `cat /etc/passwd | grep ${UID} | awk -F':' '{print $1}'`; \
fi

# ユーザー／グループ追加
RUN groupadd -g ${GID} ${GROUPNAME} && \
	useradd -m -s /bin/bash -u ${UID} -g ${GID} ${USERNAME}

# ------------------------------
#  Ubuntu更新、アプリインストール
# ------------------------------
# wget以外は必須ではない。
RUN apt-get update && \
    apt-get upgrade -y && \
    apt-get install -y wget && \
    apt-get install -y htop vim nano less x11-apps

# ---------------------------------------
#  CUDA(NVIDIA GPU開発環境)のインストール
# ---------------------------------------
RUN wget https://developer.download.nvidia.com/compute/cuda/repos/wsl-ubuntu/x86_64/cuda-keyring_1.1-1_all.deb
RUN dpkg -i cuda-keyring_1.1-1_all.deb

# ↓Ubuntu23.04以降でCUDAのインストールに必要なコマンド
RUN echo "deb http://archive.ubuntu.com/ubuntu/ jammy universe" >> /etc/apt/sources.list.d/jammy.list
RUN echo "Package: *\n\
    Pin: release n=jammy\n\
    Pin-Priority: -10\n\n\
    Package: libtinfo5\n\
    Pin: release n=jammy\n\
    Pin-Priority: 990" >> /etc/apt/preferences.d/pin-jammy
# ↑Ubuntu23.04以降でCUDAのインストールに必要なコマンド

# ★★★ cuda-toolkit-12-4は、GPUに合わせたバージョンを要選択 ★★★
RUN apt-get update && \
    apt-get -y install cuda-toolkit-12-4

RUN rm cuda-keyring_1.1-1_all.deb

# -----------------------
#  PyTorchのインストール
# -----------------------
RUN apt-get update && \
    apt-get install -y python3-pip

RUN mkdir -p /root/.pip && \
    echo "[global]" > /root/.pip/pip.conf && \
    echo "break-system-packages = true" >> /root/.pip/pip.conf

# ★★★ PyTorch公式HPで調べた、CUDAバージョンに対応するコマンドをここに記載 ★★★
# RUN python3 -m pip install --upgrade pip
RUN python3 -m pip install --no-cache-dir \
            torch \
            torchvision \
            torchaudio

RUN python3 -m pip install --no-cache-dir \
            jupyter \
            ipykernel

# -------------------------------
#  Dockerコンテナ起動時スクリプト
# -------------------------------
COPY container_init.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/container_init.sh
ENTRYPOINT [ "/usr/local/bin/container_init.sh" ]

```

```sh: container_init.sh
#!/bin/bash

# bashrc等を作成（既にある場合は何もしないので、あとから編集した内容が消えない）
if [ ! -e ./.bashrc ]; then
    cp /etc/skel/.bash_logout ./
    cp /etc/skel/.bashrc ./
    cp /etc/skel/.profile ./

    # ★★★ パスは、CUDAバージョンに合わせて要変更 ★★★
    echo "export PATH=/usr/local/cuda-12.4/bin:\$PATH" >> .bashrc
    echo "export LD_LIBRARY_PATH=/usr/local/cuda-12.4/lib64"  >> .bashrc
    sed -i 1iforce_color_prompt=yes .bashrc

    # （必須ではない）bashプロンプトにコンテナ名を表示＆色を変更
    if [ -n "$CONTAINER_NAME" ]; then
        echo "export PS1='${debian_chroot:+($debian_chroot)}\[\e[01;33m\]\u@${CONTAINER_NAME}\[\e[00m\]:\[\e[01;36m\]\w\[\e[00m\]\$ '" >> .bashrc
    fi
fi

# （必須ではない）Dockerコンテナ内でpip実行時にエラーが出ないようにする
#   （既にpip.confがある場合は何もしない）
if [ ! -d ./.pip ]; then
    mkdir -p .pip
    echo "[global]" > .pip/pip.conf
    echo "break-system-packages = true" >> .pip/pip.conf
fi

# .bashrcを読み込み
source .bashrc
exec "$@"
```

```sh: docker_build.sh
#!/bin/bash

# コンテナイメージ名。任意の名前でOK
CONTAINER_IMAGE_NAME="cuda_ubuntu:12.4-24.04"

OPT_NOCACHE=""
if [ $# = 1 ] && [ ${1} = "rebuild" ]; then
    OPT_NOCACHE="--no-cache"
fi

# 実行コマンドのecho。なくてもよい
echo docker build . -t ${CONTAINER_IMAGE_NAME} \
               --build-arg USERNAME=${USER} \
               --build-arg GROUPNAME=${USER} \
               --build-arg UID=$(id -u) \
               --build-arg GID=$(id -g) \
               ${OPT_NOCACHE}


# Dockerコンテナに現在のユーザーを追加
docker build . -t ${CONTAINER_IMAGE_NAME} \
               --build-arg USERNAME=${USER} \
               --build-arg GROUPNAME=${USER} \
               --build-arg UID=$(id -u) \
               --build-arg GID=$(id -g) \
               ${OPT_NOCACHE}
```

```sh: docker_run.sh
#!/bin/bash

# コンテナイメージ名。docker_build.shと合わせること
CONTAINER_IMAGE_NAME="cuda_ubuntu:12.4-24.04"

# コンテナ名。任意の名前でOK
CONTAINER_NAME=pytorch_gpu

# コンテナ内のホームディレクトリのマウント先。現在ディレクトリ直下に、/home/xxx/を作成
CONTAINER_HOMEDIR=$(pwd)/$HOME

# ホームディレクトリのマウント先となる空ディレクトリを、ホスト側（Ubuntu）に作成
if [ ! -d ${CONTAINER_HOMEDIR} ]; then
    mkdir -p ${CONTAINER_HOMEDIR}
fi

docker run -it --rm --gpus all \
            --name ${CONTAINER_NAME} \
            -u $(id -u):$(id -g) \
            --mount type=bind,source=/tmp/.X11-unix,target=/tmp/.X11-unix \
            --mount type=bind,source=/mnt/wslg,target=/mnt/wslg \
            --mount type=bind,source=${CONTAINER_HOMEDIR},target=${HOME} \
            --env CONTAINER_NAME=${CONTAINER_NAME} \
            --env DISPLAY=${DISPLAY} --env WAYLAND_DISPLAY=${WAYLAND_DISPLAY} \
            --env XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR} \
            -w ${HOME} \
            ${CONTAINER_IMAGE_NAME} \
            /bin/bash
```

## 3.2 (Dockerfile) 一般ユーザー追加

Dockerコンテナ内で作業できるユーザーは、Defaultではrootユーザーのみです。ただ、rootユーザーでの作業は、何かの拍子にシステムファイルを不意に壊してしまうリスクがあったりして、一般的にはあまり好ましくありません。

なので、一般ユーザーを追加する処理をDockerfileに記載します。
ここでは、ホスト側の現在のユーザー（大抵はuid=1000）をDockerコンテナ内に引き継げるよう設定していきます。

基本的には、Linuxコマンド```groupadd```、```useradd```を実行するだけですが、どうもubuntu24.04のDockerイメージには、uid=1000のubuntuというユーザーが登録されているようで、uid=1000でユーザーを作成しようとするとエラーになります。

なので、現在のユーザーのuidが既に存在していたら、削除する処理を追加しました。

```docker:Dockerfile（※一部抜粋）
# ------------------
#  一般ユーザー追加
# ------------------

# ここは、docker buildコマンドの引数で、現在のユーザー名等に書き換える想定(docker_build.sh参照)なので、この通りの記載でOK
ARG USERNAME=user
ARG GROUPNAME=user
ARG UID=1000
ARG GID=1000

#   追加しようとしているユーザー／グループが存在していれば削除
RUN if [ `cat /etc/group | grep ${GID} | awk -F':' '{print $1}'` != "" ]; then \
        groupdel -f `cat /etc/group | grep ${GID} | awk -F':' '{print $1}'`; \
fi
RUN if [ `cat /etc/passwd | grep ${UID} | awk -F':' '{print $1}'` != "" ]; then \
        userdel -r `cat /etc/passwd | grep ${UID} | awk -F':' '{print $1}'`; \
fi

RUN groupadd -g ${GID} ${GROUPNAME} && \
	useradd -m -s /bin/bash -u ${UID} -g ${GID} ${USERNAME}
```

Defaultのubuntuというユーザーも（おそらく）一般ユーザーなので、上記設定をせずに、それをそのまま使ってもよいかと思います。

ただ、ホスト側の現在のユーザーと違っていると、ホスト側のディレクトリをマウントしてデータ永続化するときなどに、アクセス権限でトラブるかもしれません（未確認）。

## 3.3 (Dockerfile) Ubuntu更新、アプリインストール

Dockerコンテナ内のubuntuを最新に更新し、必要なアプリを追加でインストールします。

アプリのうち、```wget```は、次節の「CUDA(NVIDIA GPU開発環境)のインストール」で早速使うので必須ですが、他アプリは任意です。

今後、このDockerコンテナを使って行くうちに、必要になったら、順次追加していけばよいかと思います。

```docker:Dockerfile（※一部抜粋）
# ------------------------------
#  Ubuntu更新、アプリインストール
# ------------------------------

# wget以外は必須ではない。
RUN apt-get update && \
    apt-get upgrade -y && \
    apt-get install -y wget && \
    apt-get install -y htop vim nano less x11-apps
```

## 3.4 (Dockerfile) CUDA(NVIDIA GPU開発環境)のインストール

Dockerコンテナに、CUDAをインストールするためのコマンドをDockerfileに書きます。

CUDAとは、NVIDIA GPU上で動くアプリを開発するためのツール一式です。PyTorchも、GPUアクセラレートする部分はこれを使って作られているようなので、CUDAのインストールは必須です。

GPUと、CUDAバージョン、PyTorchバージョンが合ってないと動いてくれないので、ここのバージョン選択が、神経を使う面倒なところです。

Dockerfile先頭の「FROM」をCUDAのDockerイメージにすれば、ここの作業は必要なくなるのかもしれませんが、未確認です。

### 3.4.1 GPUとCUDAバージョン確認
本節では、GPUとCUDAバージョン確認を実施します。CUDAとPyTorchのバージョン合わせこみは次節で説明します。

バージョン確認は、[Wikipedia掲載の表](https://en.wikipedia.org/wiki/CUDA#GPUs_supported)から行えました。NVIDIA公式ページからもがんばれば行えるのかもしれませんが、よくわからず断念しました。

まずは、下側の表に、GPU名でページ内検索(Ctrl+F)を行い、GPUに対応する「MicroArchtecture」を調べます。

GeForce GTX 1660 Superの場合、MicroArchtectureは```Turning```でした（下図）。
![gpu_cuda対応1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1299377/00e4d357-aa68-e9de-656a-85d66f91e295.png)

次に、上側の表で、「MicroArchtecture」から、目的のCUDAバージョンを調べます。「MicroArchtecture」列の、緑の網掛けがかかっているのが、対応するCUDAバージョンです。

MicroArchtectureが```Turning```の場合、対応するCUDAバージョンは、10.0～12.5となります（下図）。

![gpu_cuda対応2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1299377/f3c154df-56e4-ed66-6adf-bd88f98977cb.png)

### 3.4.2 CUDAインストール

DockerコンテナにCUDAをインストールするコマンドをDockerfileに書きます。

記載するコマンドは、以下から、3.4.1節で調べたCUDAバージョンのリンクをクリックし、OS等の情報を順次クリックすれば、出てきます。

https://developer.nvidia.com/cuda-toolkit-archive

CUDA12.4.1のコマンドを調べた結果は下図です。
![coda_toolkit_インストールコマンド.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1299377/14bf8083-14df-4733-609b-a997feea101e.png)

これをDockerfileに書けばいいのですが、Ubuntu24.04だと、これだけではエラーになってしまったので、以下記事で紹介されていたコマンドを追加しました（```↓Ubuntu23.04以降でCUDAのインストールに必要なコマンド```箇所）。

https://qiita.com/gomi1994/items/1d888f64c52c86981e0a

```docker:Dockerfile（※一部抜粋）
# ---------------------------------------
#  CUDA(NVIDIA GPU開発環境)のインストール
# ---------------------------------------
RUN wget https://developer.download.nvidia.com/compute/cuda/repos/wsl-ubuntu/x86_64/cuda-keyring_1.1-1_all.deb
RUN dpkg -i cuda-keyring_1.1-1_all.deb

# ↓Ubuntu23.04以降でCUDAのインストールに必要なコマンド
RUN echo "deb http://archive.ubuntu.com/ubuntu/ jammy universe" >> /etc/apt/sources.list.d/jammy.list
RUN echo "Package: *\n\
    Pin: release n=jammy\n\
    Pin-Priority: -10\n\n\
    Package: libtinfo5\n\
    Pin: release n=jammy\n\
    Pin-Priority: 990" >> /etc/apt/preferences.d/pin-jammy
# ↑Ubuntu23.04以降でCUDAのインストールに必要なコマンド

# ★★★ cuda-toolkit-12-4は、GPUに合わせたバージョンを要選択 ★★★
RUN apt-get update && \
    apt-get -y install cuda-toolkit-12-4

RUN rm cuda-keyring_1.1-1_all.deb
```

## 3.5 (Dockerfile) PyTorchのインストール

DockerコンテナにPyTorchをインストールするコマンドを、Dockerfileに書きます。

CUDAバージョンに対応するPyTorchをインストールするコマンドを記載する必要がありますが、これは、[PyTorch公式サイト](https://pytorch.org/get-started/locally/)から比較的簡単に調べることができました。

https://pytorch.org/get-started/locally/

CUDA12.4に対応するPyTorchインストールコマンドは下図でした。バージョン指定なしの、最新のPyTorchをインストールすればよいようです。

![pytorchコマンド.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1299377/f1bd24a3-7ea4-0ccf-dd46-8dfa84c876f0.png)

このコマンドをDockerfileに書けばよいのですが、ここでまたしてもハマりポイントが。pip実行時に、```externally-managed-environment```というエラーが出てきてしまいました。

以下記事に紹介されていた、pip.confを作る方法で対処可能とのことなので、これを実行するコマンドもDockerfile内に追加しました。

https://ameblo.jp/kazenokinosita/entry-12829560103.html

あと、必須ではないですが、コンテナ内でjupyter notebookを使えるよう、```jupyter```と```ipykernel```も追加しました。ここも、必要に応じて順次、追加や削除等を行えばよいかと思います。

```docker:Dockerfile（※一部抜粋）
# -----------------------
#  PyTorchのインストール
# -----------------------
RUN apt-get update && \
    apt-get install -y python3-pip

RUN mkdir -p /root/.pip && \
    echo "[global]" > /root/.pip/pip.conf && \
    echo "break-system-packages = true" >> /root/.pip/pip.conf

# ★★★ PyTorch公式HPで調べた、CUDAバージョンに対応するコマンドをここに記載 ★★★
# RUN python3 -m pip install --upgrade pip
RUN python3 -m pip install --no-cache-dir \
            torch \
            torchvision \
            torchaudio

# ここは必須ではない。必要に応じて順次追加
RUN python3 -m pip install --no-cache-dir \
            jupyter \
            ipykernel
```

なお、コメントアウトしている```--upgrade pip```は、不要の旨のメッセージが出て、なぜかビルドが止まってしまったので、コメントアウトしました。正しい対処方法をご存じの方おられましたら、教えていただけると幸いです。

## 3.6 (Dockerfile) Dockerコンテナ起動時スクリプト

ここまでは、Dockerコンテナのビルド時に実行するコマンドをDockerfileに記載してきましたが、本節では、Dockerコンテナ起動時に、最初に実行するコマンドをDockerfileに記載します。

Dockerコンテナ起動時に、最初に実行したいのは以下です。

:::note info
Dockerコンテナ内部のホームディレクトリの、.bashrcファイル等の作成＆実行
:::

ホームディレクトリを永続化（※）するために、ホスト側の空のディレクトリをコンテナ内のホームディレクトリにマウントすると、ホームディレクトリがマウント先の空ディレクトリに置き換わってしまうようで、コンテナビルド時にuseraddコマンドで生成した.bashrcファイル等が消えてしまいます。

そこで、コンテナ起動時に、マウント先のホスト側ディレクトリに対して、useraddコマンド実行時の処理（/etc/skel/から.bashrc等をコピー）を再実行し、そこに、CUDAの実行パス等を追加しました。

（※）永続化とは、Dockerコンテナ実行中に作成したファイルが、コンテナ停止時（exitコマンドで抜ける、PC電源をOFF等）にも消えないようにする措置のことです。詳細は以下記事等を参照ください。

https://qiita.com/Toyo_m/items/95a2543c07dc5b126208

まず、起動時処理を、スクリプトファイル（container_init.sh）に記載します。

```sh:container_init.sh（※再掲）
#!/bin/bash

# bashrc等を作成
if [ ! -e ./.bashrc ]; then
    cp /etc/skel/.bash_logout ./
    cp /etc/skel/.bashrc ./
    cp /etc/skel/.profile ./

    # ★★★ パスは、CUDAバージョンに合わせて要変更 ★★★
    echo "export PATH=/usr/local/cuda-12.4/bin:\$PATH" >> .bashrc
    echo "export LD_LIBRARY_PATH=/usr/local/cuda-12.4/lib64"  >> .bashrc
    sed -i 1iforce_color_prompt=yes .bashrc

    # （必須ではない）bashプロンプトにコンテナ名を表示＆色を変更
    if [ -n "$CONTAINER_NAME" ]; then
        echo "export PS1='${debian_chroot:+($debian_chroot)}\[\e[01;33m\]\u@${CONTAINER_NAME}\[\e[00m\]:\[\e[01;36m\]\w\[\e[00m\]\$ '" >> .bashrc
    fi
fi

# （必須ではない）Dockerコンテナ内でpip実行時にエラーが出ないようにする
if [ ! -d ./.pip ]; then
    mkdir -p .pip
    echo "[global]" > .pip/pip.conf
    echo "break-system-packages = true" >> .pip/pip.conf
fi

source .bashrc
exec "$@"
```

このスクリプトファイルをコンテナ内にコピーし、実行権限を付与し、コンテナ起動時に実行する旨を、Dockerfileに記載します。コピー先は、ユーザーアプリのインストール先として一般的な```/usr/local/bin```にしましたが、他のディレクトリでもOKです。

```docker:Dockerfile（※一部抜粋）
# -------------------------------
#  Dockerコンテナ起動時スクリプト
# -------------------------------
COPY container_init.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/container_init.sh
ENTRYPOINT [ "/usr/local/bin/container_init.sh" ]
```

## 3.7 Dockerコンテナのビルド（Ubuntu）
前節までで作成したDockerfileでビルドします。```docker build```コマンドをたたけばよいのですが、以下設定を追加しました。

- 引数に現在ユーザーの名前やIDを指定。現在ユーザーと同じユーザーをビルド時に作成
- クリーンビルドのON/OFFを、シェル引数で指定

```sh:docker_build.sh（※再掲）
#!/bin/bash

# コンテナイメージ名。任意の名前でOK
CONTAINER_IMAGE_NAME="cuda_ubuntu:12.4-24.04"

# クリーンビルドのON/OFF（第1引数=rebuildなら、クリーンビルド）
OPT_NOCACHE=""
if [ $# = 1 ] && [ ${1} = "rebuild" ]; then
    OPT_NOCACHE="--no-cache"
fi

# 実行コマンドのecho。なくてもよい
echo docker build . -t ${CONTAINER_IMAGE_NAME} \
               --build-arg USERNAME=${USER} \
               --build-arg GROUPNAME=${USER} \
               --build-arg UID=$(id -u) \
               --build-arg GID=$(id -g) \
               ${OPT_NOCACHE}


# Dockerコンテナに現在のユーザーを追加
docker build . -t ${CONTAINER_IMAGE_NAME} \
               --build-arg USERNAME=${USER} \
               --build-arg GROUPNAME=${USER} \
               --build-arg UID=$(id -u) \
               --build-arg GID=$(id -g) \
               ${OPT_NOCACHE}
```

## 3.8 Dockerコンテナの起動（Ubuntu）
前節でビルドしたDockerコンテナを起動します。

ここでは、前節で触れた、ホームディレクトリの永続化をするために、マウント先となる空ディレクトリをホスト側（Ubuntu）に作成する処理も含めてます。

```docker run```コマンドをUbuntuターミナルで実行すればよいのですが、オプション設定が多くて、コマンド長が長いので、シェルスクリプトにまとめました。

```sh:docker_run.sh（※再掲）
#!/bin/bash

# コンテナイメージ名。docker_build.shと合わせること
CONTAINER_IMAGE_NAME="cuda_ubuntu:12.4-24.04"

# コンテナ名。任意の名前でOK
CONTAINER_NAME=pytorch_gpu

# コンテナ内のホームディレクトリのマウント先。現在ディレクトリ直下に、/home/xxx/を作成
CONTAINER_HOMEDIR=$(pwd)/$HOME

# ホームディレクトリのマウント先となる空ディレクトリを、ホスト側（Ubuntu）に作成
if [ ! -d ${CONTAINER_HOMEDIR} ]; then
    mkdir -p ${CONTAINER_HOMEDIR}
fi

docker run -it --rm --gpus all \
            --name ${CONTAINER_NAME} \
            -u $(id -u):$(id -g) \
            --mount type=bind,source=/tmp/.X11-unix,target=/tmp/.X11-unix \
            --mount type=bind,source=/mnt/wslg,target=/mnt/wslg \
            --mount type=bind,source=${CONTAINER_HOMEDIR},target=${HOME} \
            --env CONTAINER_NAME=${CONTAINER_NAME} \
            --env DISPLAY=${DISPLAY} --env WAYLAND_DISPLAY=${WAYLAND_DISPLAY} \
            --env XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR} \
            -w ${HOME} \
            ${CONTAINER_IMAGE_NAME} \
            /bin/bash
```

上述の```docker run```コマンドの主なオプション/引数の説明は以下です（Docker初心者なので間違っていたらすみません）。

| オプション/引数 | 説明 |
|:-:|:-|
| ```-it```  | ホスト（Ubuntu）とコンテナの標準入出力を接続。これによって、コンテナ内の処理をシェル上でコマンド入力できるようになる。 |
| ```--rm``` | コンテナから抜けた際に、コンテナを自動で削除。これをしないと、停止したコンテナの残骸が残り続けてしまい、同じコンテナ名で起動ができなくなる。 |
| ```--gpus all``` | コンテナ内でGPUが使えるようにする。  |
| ```--name ${CONTAINER_NAME}``` | コンテナに、変数CONTAINER_NAMEに入れた名前（ここでは「pytorch_gpu」）をつける。  |
| ```-u $(id -u):$(id -g)``` | コンテナを実行するユーザー/グループIDに、現在ユーザー/グループのID（idコマンドの出力）を設定する。  |
| ```--mount（※1,2行目）``` | コンテナ内に、ホスト（Ubuntu）側のXウィンドウ関連ディレクトリをマウントする。 |
| ```--mount（※3行目）``` | コンテナ内のホームディレクトリに、ホスト（Ubuntu）側のディレクトリをマウントする。コンテナのホームディレクトリに作成したファイル／ディレクトリは、ホスト側のディレクトリに残り続ける。 |
| ```--env（※1行目）``` | コンテナ名を、環境変数CONTAINER_NAMEに設定。設定されたコンテナ名は、前出のcontainer_init.sh内で、bashプロンプトに表示するために使っている。 |
| ```--env（※2,3行目）``` | Xウィンドウ/Wayland関連の環境変数に、ホスト（Ubuntu）の環境変数を設定 |
| ```-w ${HOME}```  | コンテナに入ったときの初期ディレクトリを、ホームディレクトリに設定。これがないと、ルートディレクトリになる。 |
| ```${CONTAINER_IMAGE_NAME}```  | 起動するコンテナイメージ名 |
| ```/bin/bash```  | コンテナ内のシェル |


# 確認（Dockerコンテナ内）

前章までで、Dockerコンテナの構築＆起動は完了です。
本節は、望み通りのDockerコンテナ（以下3点を満たす）が構築できたかどうかの確認です。

- ユーザー権限で動作
- NVIDIA GPUでアクセラレートできるPyTorchが動作
- コンテナ内ホームディレクトリのデータ永続化（コンテナを停止してもデータが消えない）

---------------------------------------

まずは１点目（ユーザー権限で動作）の確認です。
下図は、docker_run.shを実行し、Dockerコンテナに入ったときのターミナルです。```whoami```コマンドで、現在ユーザー≠rootである旨が確認できます。

![コンテナ実行中_ユーザー確認.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1299377/93a88216-c5e5-d810-a9c1-f9a5a9313035.png)

---------------------------------------

２点目（PyTorch(GPU)が動作）は、jupyter notebook上で、PyTorch APIを実行する以下スクリプトを実行して確認しました。

```python: test1.ipynb 
import torch
print(torch.cuda.is_available())
print(torch.cuda.get_device_name())
```

VSCodeでjupyter notebookを実行した結果が下図です。

```torch.cuda.is_available()```の結果が「True」となっているのと、```torch.cuda.get_device_name()```で、GPU名（GTX 1660 SUPER）が出力されており、GPUを認識したPyTorchが動作している旨が確認できました。

![コンテナ実行中_PyTorch確認.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1299377/3f55d85b-8c82-2d5a-4f47-ea68f9926624.png)

VSCodeの設定は、機会があればおいおい記事にしようと思いますが、VSCodeが優秀すぎて、コンテナにアタッチすれば、あとは自動で色々やってくれるので、あまりやることはなかったです。

---------------------------------------

3点目（ホームディレクトリのデータ永続化）ですが、一度コンテナからexitコマンドで抜けた後（コンテナ停止＆削除される）、再度、docker_run.shでコンテナに入ったときのターミナルが下図です。

最初に作成したファイル（test1.ipynb）や、ホームディレクトリ内で作成したディレクトリ（work/test_codes）が残っていることが確認できました。

![コンテナ実行中_データ永続化.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1299377/1eeb1db8-432e-ea59-92bf-09f3c670e863.png)

# おわりに

だいぶ長くなってしまいました。長文にお付き合いいただき、ありがとうございました。

今回の環境構築、ググってやり方の記事を見つけて、その通りにやればすんなりいくのかと思ってましたが、実際にやってみると、記事には載ってないエラーが色々と出てきて、その都度、対処方法をググって、といった試行錯誤が必要でした。

試行錯誤していると、Dockerイメージの残骸がたまり、約240GBあったHDD（SSD）の空きが、50GBを下回るまで激減しました。以下記事（※）で無事復活しましたので、みなさんもHDD空き容量が少なくなってしまった場合はお試しください。

https://qiita.com/okm-uv/items/a147680676ec24638a8e

（※）補足
　上記記事中の```docker compose up```は、```compose.yaml```ファイルがないと動きませんが、本記事の```docker_run.sh```の実行で代用可能です。
 　消去したくないイメージをあらかじめ起動しておけば、```docker image prune -a```等のコマンドで消去されなくて済む、ということです。