【初心者向け】Dockerfileに関して簡単なpythonサンプルで説明【Windows】

## 0. はじめに

Dockerを勉強しているとDockerfileという概念がでてきます。
これに関して色々な記事を見て私も勉強してきましたが、何となくの理解はできていたものの、`自分事として理解できない部分も多かった`為、自分へのおさらいも兼ねて今の自分の理解を簡単なpythonサンプルを使用して記事にすることにしました。

本記事への反応があまりにも薄くない限り、全2回を考えています。。
**(第1回)【超初心者向け】Dockerfileに関して簡単なpythonサンプルで説明【Windows】**　←本記事
(第2回)【超初心者向け】docker-compose.ymlに関して簡単なpythonサンプルで説明【Windows】　(次回予定)


- **本記事の読者は以下のようなケースを想定しています**
  * Docker(Dockerfile)を勉強中で初心者であり、他サイトの説明では(実感として)よく理解できなかった方
  * データベース/WEB系の知識が無いのにDockerfileを説明するサンプルはそればかりでうんざりしている方
  * 使用言語はpythonをメインとして超基本的な文法はわかるので、それでDockerfileを説明してほしい方

※Dockerに関しての概念は以前の記事で説明してますので、そちらや他の方の記事で随時補足してください

https://qiita.com/ku_a_i/items/8c7d91f6e13a5091f51c


- 動作環境
  *  OS : Windows10 pro (Ver.2004)
  *  WSL2 / Ubuntu 20.04
  *  Docker : 20.10.2　※Docker Desktopは使いません
  *  python 3.8(slim-buster)

## 1. 今回出てくる用語とかの前提補足

最初に用語をまとめておきます。
文中で不明な単語があった時にはここに戻ってください。

★以下随所に折りたたみ表現を使用してる為、もし気になる場合は開いてみてください。

### 1-1.WSL2
<details><summary>WSL2について</summary><div>

WindowsにてLinuxを使用できる仮想環境のことで、**WindowsでDockerを使用する為には必須**。

なお、WSL2起動中は以下のようにフォルダとして視認することも可能
`\\wsl$`と打ち込めば以下のようにUbuntuフォルダに入れます。

![スクリーンショット 2022-05-03 111329.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373060/bd4cbc4a-fef0-85b9-ded8-a5ea9978d3fb.png)

<br>
</div></details>

### 1-2.Docker

<details><summary>Dockerについて</summary><div>

前回の記事でも私なりにかなりかみ砕いて説明したつもりですので、[その記事](https://qiita.com/ku_a_i/items/8c7d91f6e13a5091f51c)も参照のこと。
PCの中にコンテナ型という方法で仮想環境を作成できるDocker社の提供するOSS。

</div></details>

### 1-3.Docker Image

<details><summary>Docker Imageについて</summary><div>

コンテナ化するためのイメージファイル(設計図)。
ただしただの設計図なので、実際に使用するには「イメージをビルド」して「コンテナ作成」して「コンテナ起動」を行う。

[Docker Hub](https://hub.docker.com/)と呼ばれる、docker版Githubのサイトにたくさん配布されており、そこで足らないものを個別で導入することで独自のイメージを作成することができる。
※今回説明する例のように「python+pandas+jupyterlab」のような自作イメージも作成可能

</div></details>

### 1-4.Docker Desktop(本記事では未使用)

<details><summary>Docker Desktopについて</summary><div>

**★2021年9月よりある程度の規模における商用利用では有償化になってしまいました。**
なので`私はプライベートでもこれを利用するのをやめました`が、個人利用ではGUIも使用できて便利です。
**※本記事では使用してません**

Docker Desktopとは何か？
⇒ Docker を使うためのGUIアプリです。
つまり、本来なら「docker ps / docker images」等でdocker EngineへCLI[^1]で指示すべきとこをGUIで行えるということである。
また、基本的なdocker関連の一式がこのDocker Desktopの導入だけでGUI付きで入ってくるので、導入が楽な面もある。

※ちょっと違うかもですが、pythonでいえばAnaconda・・のみたいな感じ？？(最初からjupyter含め色々入ってるので)

[^1]: コマンドラインインターフェースの略ですべてのやり取りを文字によって行う方式のこと。

</div></details>

### 1-5.WSL2にてDocker Desktopを使用しないで環境構築(Docker Desktopでもいいけど)

<details><summary>WSL2にてDocker Desktopを使用しないで環境構築</summary><div>

WindowsでかつDocker Desktopを使用しない環境構築の場合、以下の記事が大変参考になります。
この記事通り作業を行っていき、Dockerを使える状態にしておきましょう。

https://zenn.dev/sprout2000/articles/95b125e3359694

</div></details>

### 1-6.Dockerデーモン

<details><summary>Dockerデーモンに関して</summary><div>

Dockerデーモンとは、Dockerコンテナが使用するOSに該当する部分という理解でいいと思います。
つまり、DockerイメージやDockerコンテナを使用する為のやりとりを統括するOS的な必須サービスである
</div></details>

## 2. Dockerfile

ここからはDockerfileに関してクドクド私の理解を書いていくことにします。
`なお前提として「jupyterlab/pandas」を使用することは最初から分かってることとし、自作するコンテナにデフォルトで導入したい・・という状況の中でDockerfileを作成することになった・・というケースを想定しています`

**最終的には以下のようなDockerfileの中身を完全に理解し、自分でも実行できるところまでを目指して解説をしていきます。**
(私ならこういうシンプルなサンプルで初学したかった・・というものです)


```Dockerfile:Dockerfile
FROM python:3.8-slim-buster

ARG dir=/workdir

WORKDIR $dir

COPY . .

RUN pip install -U pip && \
    pip install --no-cache-dir -r /workdir/src/requirements.txt

VOLUME $dir

CMD [ "python", "./src/main.py" ]
```

公式よりDockerfileの説明を参照すると以下のように書いてあります。
`Docker は Dockerfile から命令を読み込み、自動的にイメージをビルドできます。Dockerfile はテキストファイルであり、イメージを作り上げるために実行するコマンドライン命令を、すべてこのファイルに含められます。`

うーんなんだかわかるようなわからないような・・・私は最終的に以下のように理解しました。</font>
**・まず「ベースイメージ」を「Docker Hub」から選定する。**
**・そして、そのベースイメージを自分流にカスタマイズして最終的な自作イメージを作成(ベースイメージで足らないものをコマンドを使用してインストールしたりする)**

そうつまりDockerfileとは
**・Dockerに読み込ませるためのDockerイメージの設計図で**
**・ベースイメージというものを基礎として**
**・ベースイメージだけで足らないものをこれに追記することで自作イメージを作成できるもの**
というものらしい。


### 2-1. Dockerfileのベースイメージを理解する

**じゃあまず疑問になるのは「そのベースイメージってどう選べばいいの？」ということになりますよね。**

例えばこのベースイメージは「OS」の場合や「python 3.7」等のプログラミング言語だったりします。

①仮にOSを選択した場合(例えばubuntuとか)の場合は、そのOSにpython3.8をさらにインストールし、さらにそのpythonライブラリにpipでpandasとかを入れないと今回の目標は達成できません

②仮にpythonを選んだ場合、一見OSを選んでないように思えますが、pythonをイメージ化した時のOSが一緒についてきます
※逆にそのOS上でのpythonが嫌なら①のようにosをベースにする必要があります。

**より理解を深めるために、イメージ図にしてみました。** 
FROMに何書けばいいの？？というのが少し理解していただける思います。
※FROMの時点で色々違いがあるのはこの為です。同じpythonの例なのになんで違うの・・・・？はこういうことです。
※今回は使用しませんが、同じpythonでも例えばベースイメージを「Anaconda」にすると、例えばpandasは最初からイメージに入ってて便利です

![スクリーンショット 2022-05-03 114712.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373060/ae8f9486-c80f-8039-e364-537be34a1e97.png)


**つまり、ベースイメージに関してはDockerHubにある沢山のDockerイメージの中からベースにしたいものを選んでFROM命令で書けばいい。ということになります。**
`ただしそれだけではPythonが使用できるようになっただけで、今回の目標である「pandas」とかは入っていないコンテナができてしまい、後からいちいちpandasとかJuputerとかを個別に入れていくことになります。
しかし最初から使うことがわかっているのであれば、このベースイメージにpipコマンド等で入れていく処理もこのDockerfileに書いておけば手間が省ける・・ということになるのです`


### 2-2. (pythonの)イメージの種類の違い
DockerHubの公式Pythonイメージをベースにするのはわかったけど、いざページを見てみると2つ目の疑問が出てくる。
**なんかpythonに無茶苦茶種類があるんだが・・どれ選ぶの・・・(泣)**

例えばpythonの公式Docker Hubには以下の種類がちりばめられており、それ選んでいいかわからずまた迷うことになります。
・python:<version>
・python:<version>-slim
・python:<version>-alpine
・python:<version>-slim-buster
・python:<version>-bullseye

調べるとそれぞれ以下のようなことらしい。(普通にDockerHubに書いてあるから読めばいいのだが・・)
・latestは公式のフルパッケージ(重い)
・alpineは「超軽量版のlinux」でバグが多いらしく非推奨
・bullseyeはDebian(OS)のバージョン11がベース
・busterはDebian(OS)のバージョン10がベース
・windowsservercoreはWinodws serverがベース
・Slim(slim-busterとかslim-bullseyeとか)は、OSから余計なツールを削った軽量版

**特に制約がなく、ある程度環境を早く立ち上げる場合は「bullseye/buster等」を選び、さらにコンテナを軽量化するなら「Slim付き」を選ぶといいらしい。**※ここらへんは正直あまり検証したことがないのでこれ以上は不明。alpineはあまりお勧めではないらしい。

`本記事ではなんでもいいのだが、とりあえず軽量版の「python3.8 slim-bullseye」を使用することにする。`

### 2-3. DockerfileのFROM以外の記載に関して

後はベースイメージ以外の部分を書いていくのだが、色々なコマンドがあるのでかいつまんでざっくり以下に記す。

FROM以降は「そのベースイメージを自分流にカスタマイズして最終的な自作イメージを作成」するのが目標なので、それに必要なコマンドを選択して記載していくことになる。
**さらに、イメージをビルドした後にコンテナ化を行うのだが、その後に実行するコマンドライン引数命令もDockerfileに書くことが可能。**
`※Dockerfileに書かないで、普通にdocker run時に引数として書いてももちろんOK`


| 命令 | 説明 |
|:-:|:-:|
|FROM  | ベースイメージの選択  |
|COPY | コンテナへファイルをコピーする  |
|ADD | コンテナへファイルをコピーする(解凍付)  |
|RUN | イメージを作成する際に実行されるコマンドを書く  |
|CMD | コンテナを実行したときにデフォルトで実行されるコマンド  |
|ENTRYPOINT |  コンテナを実行したときに必ず実行されるコマンド |
|ENV | 環境変数(runさせたコンテナ内でも有効)  |
|ARG |  環境変数(Dockerfile内のみで有効) |
|WORKDIR |  コンテナのデフォルトフォルダを指定 |
|VOLUME | dockerコンテナで作成したデータを永続ディスクとして保存する設定  |
|EXPOSE | Dockerコンテナ内で公開するポートを指定  |

これ見てもいまいちピンとこない場合があると思うが、後述する実例を見れば理解できるようになると思う。

**疑問①：COPYとADDは何が違う？**
ADDだともし圧縮ファイルがあった場合解凍されるらしいです。

**疑問②：CMDとかENTRYPOINTは何が違う？**
私の理解では「後でコマンドライン引数にて上書きできるかどうか？」だけだと思ってる。
ENTRYPOINTは確実に実行されるが、CMDだと最後に出てくるコマンドが有効になる為に上書きが出来る(後述)

**疑問③：CMDとかENTRYPOINTを書かないでコンテナを実行するとどうなる？**
するとベースイメージ(今回はpython3.8-slim-buster)のCMDが発動し、いきなりpythonが実行されます。
⇒下記URLを見てみるとベースイメージpython3.8-slim-busterのDockerfileの最後に、CMD ["python3"]と最後に記載がある為、pythonが実行されることになる。

https://github.com/docker-library/python/blob/f871d0435e7f35a693fa1b60aa159e6a1a4c6a2e/3.8/slim-buster/Dockerfile

**疑問④：VOLUMEってどこに保存されるの？そしてマウントはできないの？**
そもそも`コンテナで生成されたデータはコンテナ消滅時に消えてしまう。`よってそれを保存する為に「永続ディスク」と言われる領域がある。
どうやら引数としてコンテナ側のディレクトリしか指定することができない為、dockerコマンドでいう「-v」のように指定したフォルダへのマウントはできないらしい。 **※次回記事予定の「Composeファイル」なら指定可能になります**

じゃあどこに保存されるの？ということだが、保存先は`docker inspect <生成コンテナ名(ID)>`実行結果の「Mounts」欄に書いてある。
※私の場合は\\\wsl$\Ubuntu-20.04\var\lib\docker\volumesの中に作成されていた模様。具体的な例は後述


### 2-4. 今回の要件でDockerfileを書いてみる(サンプル)

**<font color="Red">書き方に正解はない</font>** ですが、1例として上で説明したようなコマンドをなるべく使用して今回は記載してみました。
`・python3.8を使用して、jupyterLab＋pandasはプレインストールさせておく`
`・main.pyをあらかじめ作成しておき、コンテナ起動と同時に起動させてハローワールドを出力`

まず今回のフォルダ構成は以下のような感じとし、sample_docker以下に関しては事前に準備しておきます。

```:ディレクトリ構成(WSL2/Ubuntu)
%%WSL/Ubuntu-20.04/home/usr(ユーザー名)/sample_docker/  ※sample_dockerは自分で作成(mkdirコマンドで)

sample_docker/　　(←今回はこのディレクトリでビルド命令を実施。つまり以下をコンテナにコピーさせる)
       ┣━Dockerfile
       ┗━src
          ┣━main.py(コンテナ実行ファイル)
          ┗━requirement.txt(pipコマンドで入れるライブラリを記載)
```

今回Dockerfileの理解に役立つように、あえて色々検証できるように簡単なサンプルプログラムを書いてみた

```python:main.py
import os
import pandas as pd #無駄にpandasをインポート(pipがうまくいってるか？の確認)

#以下はコンテナ内で作成したフォルダをコンテナ破棄後に確認できるか？の検証用
new_dir_path = './new-dir'
os.mkdir(new_dir_path) #コンテナの中からnew-dirフォルダを作成する

print('Hello World!') #コンテナからこれを実行したらハローワールド出力
```

requirement.txtに関しては、イメージに追加する際に1個1個pipで入れていってもいいのだが、量が増えるとゴチャゴチャするので`requirement.txt`に記載する。(というより個別pipはやめた方がいいと思います)

```:requirement.txt
#普通のpip同様バージョン指定してもOK(今回は書いてないが、本運用ではバージョン指定するのが普通)
jupyterlab
pandas
```

上のファイル構成で自作イメージを作成する為のDockerfileを以下に記載する。
**それぞれの行で何を処理させているのか？のコメントをふんだんに書いている**ので、これでイメージをつかんでいただければと思う。

```Dockerfile:Dockerfile
#FROMでまずはベースイメージを指定
FROM python:3.8-slim-buster

#本Dockerfile内限定で使用できる環境変数(ARG) ※使用時は$付きにする（説明用に無理やり使ってる感じですが気にしない）
ARG dir=/workdir

#先程のARGで指定したpathをコンテナに入った後の初期(作業)ディレクトリとして指定(今回はroot直下にworkdirを作成※無ければ自動作成される)
#このコマンドによっていわゆる「cd /workdir」が実行されたと思っていい(pathが/workdirに移動した)
WORKDIR $dir

#COPYでコンテナ内部にコピーするフォルダ(ファイル)を指定する (コマンド ⇒ COPY コピー元 コピー先)
#※この時すでにWORKDIR $dirによって、コンテナ内での作業ディレクトリは/workdirになっている
#. .で「コピー元はビルド実行の時のpath」、「コピー先は/workdir直下」を示している　(.は現在のディレクトリを示す)
COPY . .

#RUNコマンドでpip命令を実施する(コマンドを実行する場合にRUNを使用する) 
#COPYコマンドにより、srcフォルダ直下にある「requirements.txt」も当然コピー済。
#後はコンテナ内のrequirements.txtのpathを指定してpipすればOK
#--no-cache-dirをつけることでpipにおけるインストールキャッシュを残さずコンテナ容量を軽くできる
RUN pip install -U pip && \
    pip install --no-cache-dir -r /workdir/src/requirements.txt

#VOLUMEでコンテナの指定したフォルダ以下を「永続ディスク」にも保存する
#これでコンテナが削除されても指定した場所に保存されたファイルは破棄されずに残る
VOLUME $dir

#コンテナを実行した際にmain.pyを動作させたいので、CMD命令を書く(ENTRYPOINTとの違いは後述)
#「python ./src/main.py」の半角スペース部分で区切って書いてるだけ
CMD ["python", "./src/main.py"] 

#もし仮にコンテナ起動時にいきなりjupyterLabを起動して検証したい場合は以下のように書けばいい
#ENTRYPOINT ["jupyter-lab", "--no-browser", "--port=8888", "--ip=0.0.0.0", "--allow-root", "--NotebookApp.token=''"]
#CMD ["--notebook-dir=/workdir"] #jupyterの起動初期ディレクトリだけは後で変更できるようにCMDで書いている
```

**【補足】ENTRYPOINT/CMD とはいったいどれを指している？(Dockerfileを使用しない場合)**

docker run *** (コンテナ化対象のイメージを指定) (コマンドライン引数)というコマンドなので、この引数部分がENTRYPOINT/CMDになる。
`ex) docker run -it sample_image(←イメージ) bin/bash(←これがCMDとかに相当)`

### 2-5.書いたDockerfileをビルドし、コンテナ実行して色々確認してみる
各命令が何してるか？はコメントを参照いただくとして、ざっくりいうと以下の３つをやってます。

・作成したDockerfileから自作のイメージを作成する
・作成したイメージを`docker run`により、ビルド～コンテナ起動まで実施
・VOLUMEの保存先を確認し、実際にpythonで作成したフォルダが存在するかを確認

``` Bash:Ubuntu(WSL2)
$ sudo service docker start #もしDockerデーモンを起動してない場合は起動しておく
$ cd /home/usr/sample_docker #Dockerfile設置ディレクトリまで移動
$ docker build ./ -t example_docker #Dockerfileからexample_dockerという名前のイメージをビルドする
$ docker images #イメージが2つビルドされたことを確認(example_dockerとベースイメージのpython3.8の2つ)
#hello_dockerというコンテナ生成～実行(--rmをあえてつけない)
$ docker run --name hello_docker -it example_docker:latest #ハローワールドと出力されること確認
$ docker ps -a #停止しているコンテナを確認(hello_dockerコンテナがあるはず)
$ docker inspect hello_docker #VOLUMEの保存先がどこか？を調べる
$ docker container prune #VOLUMEの保存先を確認したので、停止しているコンテナを全部削除(hello_dockerを削除)
#以下で作成したはずの「new_dir」がコンテナ削除後にも存在するか？を確認(確認先は↑で調べた保存先)
#cdはパーミッションエラーでできないので、サブシェルを使用して確認する  ※\\wsl$経由で直接フォルダを見に行ってもOKです
$ sudo sh -c "cd /var/lib/docker/volumes/3374e71ba31cd04438ac62d24c49f4fa50fcaf6c67b160792167ad1f2a25f791/_data; ls -lsa"
``` 

以下出力を「- - - - - - - - - - - 」で区切ってます
``` :出力
- - - - - - - - - - - 
REPOSITORY       TAG               IMAGE ID       CREATED         SIZE
example_docker   latest            bbea1a9e0beb   21 seconds ago   405MB　　※←自作イメージ。tagは「latest」になる
python           3.8-slim-buster   c706fbd78639   12 days ago      117MB　　※←ベースイメージ

- - - - - - - - - - - 
Hello World! 　　※←当たり前だが「import pandas」でエラーがでてないこともわかる

- - - - - - - - - - - 
※hello_dockerコンテナを確認できる
CONTAINER ID   IMAGE                   COMMAND                  CREATED          STATUS                      PORTS     NAMES
346ef7b998b1   example_docker:latest   "python ./src/main.py"   13 seconds ago   Exited (0) 12 seconds ago             hello_docker

- - - - - - - - - - - 
(以上略)　※以下Mounts欄の「Source」に永続ディスクの場所が書いてある
"Mounts": [
            {
                "Type": "volume",
                "Name": "3374e71ba31cd04438ac62d24c49f4fa50fcaf6c67b160792167ad1f2a25f791",
                "Source": "/var/lib/docker/volumes/3374e71ba31cd04438ac62d24c49f4fa50fcaf6c67b160792167ad1f2a25f791/_data",
(以下略)

- - - - - - - - - - - 
4 drwxr-xr-x 6 root root 4096 May  3 10:22 .
4 drwx-----x 3 root root 4096 May  3 10:22 ..
4 -rw-r--r-- 1 root root 1708 May  3 10:17 Dockerfile
4 drwxr-xr-x 2 root root 4096 May  3 10:22 new-dir　※←たしかにコンテナ削除後も消えてないことがわかる
4 drwxr-xr-x 2 root root 4096 May  3 10:22 src

```

★サブシェルの参考URL
https://uchiida.com/2019/06/sudo-subshell/


### 2-6.CMD命令を上書きで変更する

さて、先程`CMDとかENTRYPOINTは何が違う？`で話たことを実例で確認することにする。
Dockerfileのコマンドには`CMD [ "python", "./src/main.py"] `と書いたので、コンテナ起動後にmain.pyが実行されることになる。

そこで、先程のコマンド実行後、そのまま以下の様なコマンドを打ってみてください。
最大の違いは、docker runの最後の部分にコマンドライン引数を入れていることです。

``` Bash:Ubuntu(WSL2)
$ docker images #イメージが2つあることを一応確認する
#以下のコマンドの最後に引数(/bin/bash)を追記する
#今回はコンテナ停止時にコンテナの削除を自動で行う「--rm」も記載した
$ docker run --name hello_docker --rm -it example_docker:latest /bin/bash
```

``` :出力
root@*****:/workdir#　　←main.pyが実行されず、「/bin/bash」が実行されている
```

「/bin/bash」を実行することで、コンテナの中に入ることができます。
さらにしっかり/workdirとなっているので、Dccokerfileの指定通りの作業ディレクトリにいることもわかります。

つまりDockerfileに書いたCMD命令の`python main.py`は実行されずに、`/bin/bash`が実行されたことが確認できました。
**つまり、CMDコマンドは「後から上書きできる」のです。**

★ENTRYPOINT との違いのポイント
CMD [ "python", "./src/main.py"]　ではなく
ENTRYPOINT [ "python", "./src/main.py"]の場合は、**コマンドで上書きが出来ずに「Hello World!」と出力される**


## 3. おわりに

今回はpythonの簡単な例でクドクド説明してみましたが、イメージ湧きましたでしょうか？
こういう身近な例で説明しているサイトが少ないと感じたので、自分への理解の確認も兼ねて記事にしてみました。

次回はこいつとセットででてくることがある「docker-compose.yml」を今回作成したDockerfileを使いながら説明できればと思います。
それでは今回はここまで。


### 参考文献

https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository

https://zenn.dev/sprout2000/articles/95b125e3359694

https://www.ted027.com/post/docker-debian-difference/

https://www.tohoho-web.com/docker/index.html

https://docs.docker.jp/engine/reference/builder.html#

https://docs.docker.jp/develop/develop-images/dockerfile_best-practices.html