Dockerイメージの理解を目指すチュートリアル

# 概要 - 自分でオレオレ hello-world イメージをビルド

このチュートリアルは、`hello-world` イメージを自分でビルドする例を取り上げます。手を動かしながら、Docker イメージの仕組みや性質の理解を深めます。また、効率的な Docker イメージの作成や Dockerfile の活用を目指すための基礎のほか、（主に開発者向けには）マルチステージ・ビルドも学びます。

ポイントは、Docker イメージ（image）とは、Docker コンテナの実行に必要な概念としてのパッケージ（ファイルやメタ情報の集合体）であることです。仮想マシンイメージのように、実体としての１ファイルではありません。

そして、Docker イメージを構成するのは、抽象的なイメージ・レイヤ（image layer）の集まりです。レイヤとは「層」の意味で、Docker は複数のレイヤ上のファイルシステムを、１つに扱えます。一般的に Docker イメージは、複数のイメージ・レイヤで構成されます。また、イメージ・レイヤは読み込み専用であり、レイヤ間では親子の依存関係を持てます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13002/e5ecf581-b299-d980-1199-4fb12b6d9e75.png)


通常、 Docker イメージを自動構築できるように定義するのが Dockerfile と呼ぶ設定ファイルであり、このファイル内でイメージを構成する命令を書きます。この命令の1つ1つが、概念上のイメージ・レイヤに相当します。

なお、コマンドの実行環境は　Docker CE 19.03、ホスト OS は Linux (amd64環境)、かつストレージドライバはデフォルト（ `overlay2` ）を想定しています。

※ 表現で分かりづらい点がありましたら、ご指摘ください。

# チュートリアル

## 1. Docker イメージとイメージ・レイヤ

Docker イメージとは、親子関係を持つ、複数のイメージ・レイヤ（image layer）によって構成されています。イメージ・レイヤは読み込み専用です。Docker は、複数のイメージ・レイヤに含むファイルやディレクトリの情報を1つに統合する技術を使っています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13002/5becb0d1-2cfa-c2d8-2f69-6c33b4392964.png)


このイメージ・レイヤの中には、Docker コンテナの実行に必要な Linux ファイルシステムとメタ情報を含みます。Linux ファイルシステムというのは、 `/` ディレクトリ以下の `/etc` `/bin` `/sbin` `/usr` などのディレクトリ階層およびファイルです。

Docker では、コンテナとして動かしたいアプリケーションが必要とする、最小限のファイルを Docker イメージの中に入れられます（正確にはイメージ・レイヤ内のファイルシステムに入れられます）。

また、１つ１つのイメージ・レイヤには親子関係を持ちます。より上位にあるイメージ・レイヤからは、親となるイメージ・レイヤ上のファイルシステムも参照できます。つまり、Docker イメージをダウンロードすると、そのイメージが複数のイメージ・レイヤで構成されていたとしても、それを意識せずに利用できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13002/1bb88bcf-9696-99e6-af4e-25e521154cb1.png)




さらに、そのアプリケーションを動かすために必要なデフォルトのコマンドや引数の指定、外に公開するポート番号の情報、ボリューム領域などの情報があります。これらをメタ情報として、同じく Docker イメージ・レイヤの中に入れられます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13002/641c5dc7-301e-5282-fc39-557ddda931bb.png)

このように Docker イメージには「イメージ」という名称が付いていますが、仮想マシン用のディスクイメージであったり、OS のテンプレートを指すイメージとは全く用法・概念が異なりますので注意が必要です。

通常、何らかの Docker イメージを指すときには、そのイメージの最上位に位置するイメージ・レイヤを指します（デフォルトでは latest タグというタグを持つイメージ・レイヤ）。そのイメージ・レイヤに親子関係を持つレイヤがあれば、イメージの取得時など、自動的にまとめてダウンロードしたり、アップロードしたりできます。

以降では、コマンドを実行しながらイメージとイメージ・レイヤについて確認していきます。

## 2. `hello-world` イメージのダウンロード(pull)

Docker イメージを使うには、Docker Hub ( https://hub.docker.com/ ) 等から `docker pull` コマンドでダウンロードするか、 `docker build` コマンドを使い、自分で作成（ビルド）します。

Docker Hub とは、公式 Docker イメージを含む、様々な Docker イメージが公開・共有したり、共同作業（コラボレーション）するための場所です（誰でも利用できるので、Docker イメージの「公開レジストリ」と呼ばれています）。GitHub がソースコードを共有したり共同作業したりできるのと、同じような位置づけです。

この Docker Hub から、`hello-world` という名前の Docker イメージをダウンロードします。このイメージには [C 言語で書かれ](https://github.com/docker-library/hello-world/blob/7ecae6978055d2fb6960e2a29d24a2af612e2716/hello.c) た `hello` という名前の、説明用文字を表示するだけのバイナリが入っています。

ダウンロードは `docker pull hello-world` を実行しましょう。

```
$ docker pull hello-world
Using default tag: latest
latest: Pulling from library/hello-world
0e03bdcc26d7: Pull complete
Digest: sha256:d58e752213a51785838f9eed2b7a498ffa1cb3aa7f946dda11af39286c3db9a9
Status: Downloaded newer image for hello-world:latest
docker.io/library/hello-world:latest
```

表示されているメッセージ内容を、上から順に見ていきます。

* `Using default tag: latest`
  * Docker イメージには「タグ」という概念があります。主にバージョンを表記するために利用されることが多いです。1つのイメージに対し、複数のタグを割り当て可能です。
  * `docker pull <イメージ名>:<タグ名>` が正確な書式ですが、タグ名を省略すると自動的に `latest` のタグが適用されます。
  * つまり `docker pull hello-world` は `docker-pull hello-world:latest` と同じです。
* `latest: Pulling from library/hello-world`
  * `hello-wolrd` の前に `library/` という名前空間（ディレクトリ名）が自動的に付与されています。この `library` は Docker 公式イメージ専用の名前空間です。
  * `docker pull` などを実行時、名前空間の指定がなければ、デフォルトで公式イメージ（library）のイメージを取得します。 
* `0e03bdcc26d7: Pull complete`
  * Docker Hub 上にあるイメージ・レイヤ `0e03bdcc26d7` のダウンロード状況が、完了しました。
  * hello-world は1つのレイヤですが、イメージによっては複数のレイヤで構成されます。
* `Digest: sha256:d58e752213a51785838f9eed2b7a498ffa1cb3aa7f946dda11af39286c3db9a9`
  * この `hello-world` イメージ（正確には、 hello-world:latest のタグを持つイメージ・レイヤ）のハッシュ値です。
  * 中身が同じであれば、タグが変わっても、このハッシュ値は変わりません。
  * ダウンロードはタグ指定だけでなく、 `docker pull sha256:ハッシュ値`の指定も可能です。
* `Status: Downloaded newer image for hello-world:latest`
  * `hello-world:latest` の最新イメージのダウンロードが完了した、という状態を表示しています。
* `docker.io/library/hello-world:latest`
  * 最終的にダウンロードが完了したイメージの情報です。
  * `docker.io` はレジストリ Docker Hub 、名前空間（イメージを格納するレポジトリ）は `library`（公式イメージ）、その中の `hello-world` イメージの、タグ名 `latest`（最新）をダウンロード（pull）しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13002/cc4c9cc4-c8d2-e074-f5e0-deb96d2108b2.png)


このように、 `docker pull` コマンドを実行するだけで、様々な処理が行われており、その経過は画面上に表示されているのが分かります。

## 3. `hello-world` イメージを詳しく調べる

ダウンロードした `hello-world:latest` イメージを確認します。ローカルにダウンロード済みのイメージを確認するには、 `docker images` を実行します。

```
$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
hello-world         latest              bf756fb1ae65        5 months ago        13.3kB
```

このようにイメージの情報を表示しています。

* REPOSITORY … リポジトリ名です。ここでは `hello-world` です。（リポジトリ名が事実上のイメージ名と考えて構いませんが、正確には倉庫のような保管場所としてのリポジトリがあり、その中に「イメージ名:タグ」という荷札なりシールが貼られた段ボール箱があるかのように想像ください。）
* TAG … イメージに付いているタグです。ここでは `latest` です。
* IMAGE ID … イメージが持つ固有のイメージ ID（64桁）です。ここではショート ID （12桁）の情報が出ています。
* CREATED … そのイメージがいつ作成されたかです。 `5 months ago` とありますので、5ヶ月前です。
* SIZE … このイメージの実体としてディスク上で消費している容量です。 13.3 kB を使用します。

次に `docker inspect hello-world:latest` を実行し、このイメージの詳細情報を見ていきます。

```
$ docker inspect hello-world:latest
[
    {
        "Id": "sha256:bf756fb1ae65adf866bd8c456593cd24beb6a0a061dedf42b26a993176745f6b",
        "RepoTags": [
            "hello-world:latest"
        ],
        "RepoDigests": [
            "hello-world@sha256:d58e752213a51785838f9eed2b7a498ffa1cb3aa7f946dda11af39286c3db9a9"
        ],
        "Parent": "",
```

ここでは、以下のことがわかります。

* Id … このイメージの ID （64文字）です。
* RepoTags … このイメージに割り振られたタグが `hello-world:latest` と分かります。
* RepoDigests … このイメージ内容に対するハッシュ値です。タグは変えられますが、この値は内容が変わらない限り同一です。
* Parent … 親イメージの情報です。 `""` の記述は依存関係を持つ親イメージがありません。つまりこの `hello-world:latest` イメージは、この1つのイメージ・レイヤによって構成されています。

画面をもう少しスクロールすると "Cmd" セクションが見えます。これは、コンテナ実行時に引数が無ければ、コンテナ内でどのコマンドを実行するのか指定します。

```
            "Cmd": [
                "/bin/sh",
                "-c",
                "#(nop) ",
                "CMD [\"/hello\"]"
            ],
```

この `CMD ["/hello"]` という記述から、このコンテナを実行するとコンテナ内のパス `/hello` を実行することが分かります。

（ちなみに、 "Cmd" を含む `ContainerConfig` セクションは、コンテナの内容をイメージにコミットする時の情報です。 "Cmd" セクションに `/bin/sh` の記述がありますが、あくまでもイメージのコミット（作成時）の内部的な記録であり、CMD 命令で `/bin/sh` を実行する意図はありません。 [参考1](https://github.com/moby/moby/blob/9842c7a741ee480c71bfd8e71e1f22893d010411/image/image.go#L32) 、 [参考2](https://stackoverrun.com/ja/q/9984570) ）

画面を末尾までスクロールすると、次のようなセクションが見えます。

```
        "GraphDriver": {
            "Data": {
                "MergedDir": "/var/lib/docker/overlay2/94f82ed22188e11a7f2a75b015929aea7c3eaa5e170c9ca19c966bf978147f19/merged",
                "UpperDir": "/var/lib/docker/overlay2/94f82ed22188e11a7f2a75b015929aea7c3eaa5e170c9ca19c966bf978147f19/diff",
                "WorkDir": "/var/lib/docker/overlay2/94f82ed22188e11a7f2a75b015929aea7c3eaa5e170c9ca19c966bf978147f19/work"
            },
            "Name": "overlay2"
        },
```

`UpperDir` と書かれたパスが、この Docker を実行しているホスト上で、 `hello-world` イメージの実体を保存しているディレクトリです。この画面上では `94f...` で始まる文字列ですが、環境によってランダムな文字列に変わります。

それでは、コンテナの中を `ls -l <ディレクトリ名>` のコマンドで調べましょう。ディレクトリ名は環境によって異なるのでご注意ください。なおコマンドの実行には root 権限が必要です。環境によっては `sudo ls -l ...`を実行ください。

```
# ls -al /var/lib/docker/overlay2/94f82ed22188e11a7f2a75b015929aea7c3eaa5e170c9ca19c966bf978147f19/diff
合計 24
drwxr-xr-x 2 root root  4096  6月 13 12:39 .
drwx------ 3 root root  4096  6月 13 12:39 ..
-rwxrwxr-x 1 root root 13336  1月  3 10:21 hello
```

`hello` という名前のファイルが見えます。このことから、 `hello-world:latest` イメージのファイルシステムは、 `hello` というバイナリしかないことがわかります。

（ちなみに、今回の例では１つのファイルしかありませんが、例えば `ubuntu` や `centos` など Linux ディストリビューションのイメージをダウンロードすると、各イメージ用のディレクトリ内には `./bin/` `./sbin/` `./var/` など各ディストリビューション用の `/` 以下ファイルシステムが展開されています。そして、それらで Docker コンテナを実行すると、ホスト上とコンテナ内で異なる Linux ディストリビューションが動作しているように "見える" のですが、実際にはホスト上の Linux Kernel 上で、Docker は指定した Doker イメージのディストリビューション、たとえば `centos` であれば centos のファイルシステムをマウントし、デフォルトではその中に含まれる /bin/bash を PID 1 とする名前空間内で実行しています。 コンテナ実行時、１つの Linux 上で、複数の Linux が動作している訳ではありません。）


今回実行した `hello-world` はレイヤが１つしかないため、ホスト上ではこの１つのディレクトリ内に `hello-world` Docker イメージの内容物を全て含みます。そのため、複数のイメージ・レイヤで構成する Docker イメージがあれば、ホスト上に複数のディレクトリが存在します。

さらに、イメージにはメタ情報を含みます。 `hello-world` では `CMD` 命令で `/hello` を実行する命令がありました。このメタ情報にもイメージ・レイヤを必要とします（なお、メタ情報は概念としてのイメージ・レイヤであり、ホスト上では実体としてのファイルやディレクトリはありません）。

このようにして、Docker エンジンは Docker のイメージ・レイヤを抽象的な Docker イメージという単位で扱えるようにしています。Docker コンテナ実行時、ホスト上ではバラバラのファイルやディレクトリを、１つのファイルシステムに統合して操作可能なようにしています。

以上のことからも、Docker イメージとは、ホスト上で１つの実体としてのファイルが存在していないことが分かります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13002/67b5aabe-bce2-be06-d620-c97714d32f5d.png)

さて、この `hello` をコンテナではなく、直接実行してみます。フルパスで `/var/lib/docker/overlay2/<ディレクトリ名>/diff/hello` を実行します。　※このパスは後のステップで使いますので、エディタ等に控えておきます。

```
# /var/lib/docker/overlay2/94f82ed22188e11a7f2a75b015929aea7c3eaa5e170c9ca19c966bf978147f19/diff/hello

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/
```

実行すると「Hello from Docker!」に続く文字列を表示します。これは [ソースコード hello.c](https://github.com/docker-library/hello-world/blob/7ecae6978055d2fb6960e2a29d24a2af612e2716/hello.c) に書かれている通りの文字列です。

以上のことから、 `hello-world:latest` イメージを調べると、この `hello` というバイナリを実行することが分かります。そして、このバイナリは amd64/x86_64 向けにコンパイルされていますので、Linux 上でそのまま実行できることを確認しました。

（なお、CPU amd64/x86_64 用にコンパイルしたバイナリは実行できますが、他の CPU アーキテクチャ向けのバイナリは amd64/x86_64 上では実行できません。例えば Raspberry Pi は ARM というアーキテクチャですので、この hello バイナリは Raspberry Pi でそのまま動きません。バイナリとして実行できない以上、仮に Docker イメージに入れたとしても、この次のセクションで説明する Docker コンテナとして実行はできません。Docker はハードウェアのエミュレーションを行いませんし、ハードウェアを仮想化するような技術でもありません。）

## 4. `hello-world` Docker コンテナの実行

次に `hello-world` イメージを使い、Docker コンテナ（以下、コンテナと省略）を実行します。実行の前に、コンテナとは何かを簡単にお復習いします。一言で言うならば、特別な状態で Linux のプロセスを起動します。Docker は Linux カーネルが持つ名前空間（namespace）の分離技術や cgroup によるリソース制限、その他 Docker Engine の実装により、 Docker イメージ内にあるファイルシステム内で、プログラムを特別な状態として起動します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13002/600fd3e6-fd8d-0e4a-e1b1-e1c647bfb0c2.png)

つまり、`hello-world` を Docker コンテナで動かすとは、 

* `hello-world` という名前の Docker イメージを準備する
* `hello-world` のイメージ内にある、何らかのプログラムを実行する
* イメージ内の定義(CMD命令)では `/hello` を自動的に実行する
* ただし、`/hello` をコンテナとして（名前空間等に制約を受けて）実行

以上の動作を行います。

また、コンテナには、コンテナ用の読み書きできる Docker イメージ・レイヤが自動的に作成されます。このイメージ・レイヤは通常のイメージ用と同じく、親子関係を持ちます。そのため、同じホスト上で複数のコンテナを実行しても、元々存在する Docker イメージ以上の容量を必要としない利点があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13002/023392de-e020-fdce-3573-37e2c57e402e.png)

それでは `hello-world` コンテナを実行します。 `docker run hello-world` を実行しましょう。

```
$ docker run hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.
...省略...
For more examples and ideas, visit:
 https://docs.docker.com/get-started/
```

このように、直接 `hello` のバイナリを実行したのと同じ処理（文字列の表示）をしたのが分かります。

重要なのは、この `hello` バイナリはホスト上に存在している点です。先ほど見た、Docker イメージの実体としての hello が置かれているパスにあります。ただし、コンテナとして実行していますので、 `hello` は PID 名前空間が分離されるため `hello` しか存在しないプロセス空間です。さらに mount 名前空間の分離により、 `hello` が存在するディレクトリが、コンテナを実行するプロセス空間で `/` としてマウントします。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13002/fcea10b1-a67e-8b9a-b38b-b3971d15bcd1.png)



つまり、 `/hello` しか存在しないファイルシステム上で、Linux のユーザ空間内で `hello` のプロセスしか実行していないように見える特別な状態が存在しています。これが Docker コンテナです。そして、コンテナは、この名前空間内で PID 1 として `hello` を実行します。 `hello` が画面に文字列を出力した後 `exit` 状態となり、コンテナそのものが実行終了（ exited ）となります。 


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13002/df059baf-fb90-8713-60d5-45a24ac43f2c.png)


このように、

* Docker イメージを準備する
* Docker コンテナ用の名前空間（PID, mount, …等々）を分離（isolate）した環境を作成
* Docker は、Docker イメージの中にあるファイル（バイナリなどのプログラム）を、その名前空間内で実行
* 実行したプログラムが処理完了すると、Docker コンテナも終了（停止）する

このコンテナ起動から終了までの流れを、Docker のライフサイクルと呼びます。

コンテナの状態を調べるには `docker ps` に `-a` （all、全て）のオプションを付けてみましょう。

```
$ docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                     PORTS               NAMES
e8c0bab26796        hello-world         "/hello"            4 minutes ago       Exited (0) 4 minutes ago                       frosty_bhabha
```

* CONTAINER ID … コンテナに対してランダムに割り当てられる64文字のコンテナ ID です。このコンテナ ID もしくは、コンテナ名でコンテナを操作します。
* IMAGE … コンテナ実行の元となるイメージ名です。
* COMMAND … コンテナ内で実行しているコマンドです。
* CREATED … コンテナがいつ作成されたかです。
* STATUS … コンテナの状態です。
* PORT … コンテナがポートを公開している場合には、ここに表示が出ます。
* NAMES … 実行時に指定しなければ自動的にランダムな文字列（ [形容詞＋計算機科学系の貢献者](https://github.com/moby/moby/blob/master/pkg/namesgenerator/names-generator.go) ）

この `hello-world` コンテナは、このコンテナ専用の名前空間内で `/hello` プログラムを実行し、終了しました。 `docker ps` で表示されるのはコンテナというよりも、コンテナ用のイメージ・レイヤを一覧表示している、と考えた方がスムーズです。

`hello-world` コンテナはとてもシンプルなので、これ以外のコンテナ内での操作は行えません。Linux ディストリビューションに含まれるコマンドやシェルが、このイメージの中に入っていないからです。

最後にこのコンテナを削除します。 `docker rm <コンテナIDまたはコンテナ名>` を実行します。

```
$ docker rm e8
e8
$ docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
```

ちなみに、このようにコンテナ ID は前方一致で指定できます。長いコンテナ ID はコピーしなくても、通常は2桁または3桁の指定でも操作ができるので便利です。そして、 `docker ps -a` を実行すると、コンテナ（用のイメージ・レイヤ）が残っていないことがわかります。

ここで `docker run` に新しいオプション `--rm` を付けて実行してみましょう。

```
$ docker run --rm hello-world
$ docker ps -a
```

このオプションを付けると、コンテナが終了すると、自動的にコンテナ（用のイメージ・レイヤ）を削除します。覚えておくと、一時的にコンテナを実行したい場合に便利なオプションです。

## 5. 自分用の `hello-world` Docker イメージを Dockerfile でビルド

次は自分で Docker イメージを構築（ビルド）します。イメージを作るには `docker build` コマンドを使います。この時 `-t` で作成するイメージ名（とタグ）と、 Dockerfile というイメージ構築命令を記述したファイルのパス（コンテクストと呼びます）を指定します。

一般的に、Dockerfile でビルドする流れは以下の図のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13002/f365e304-930c-6888-5ce2-b8032006a58f.png)
　
ここからは、自分専用の hello-world イメージを作成していきます。

まず、作業用のディレクトリを作成し、移動します。ここでは `myhello` という名前にしています。

```
$ mkdir myhello
$ cd myhello
```

また、先ほど見つけたホスト上の `hello` ファイルを、このディレクトリにコピーします。 以下 ... の部分は、皆さんの環境にあわせて書き換えます。

```
cp /var/lib/docker/overlay2/......./hello .
```

まず、コマンドラインで次のように実行します。どのような結果になりますか？

```
docker build -t myhello -<<EOF
FROM scratch
EOF
```

`FROM scratch` が `Dockerfile` の中での命令の1つです。通常ここでは元になる Docker イメージを指定できます。`scratch` を指定すると、全く何もない空っぽのイメージ・レイヤを作成する命令です。（初期の Docker は、 Docker Hub 上にあった scratch イメージは `/` しか存在しないものをダウンロードしていました。今日の scratch イメージ指定は、実体が何も無い特別な指定です ）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13002/2574bcb3-eae2-6b13-c593-4edf5e1e1f33.png)


そして、ここで Dockerfile が終わってしまっていますので、実行しても次のように何も中身がありませんよ？と表示が出ます。

```
$ docker build -t myhello -<<EOF
> FROM scratch
> EOF
Sending build context to Docker daemon  2.048kB
Step 1/1 : FROM scratch
 --->
No image was generated. Is your Dockerfile empty?
```

次に `Dockerfile` という名称のファイルを同じディレクトリ内に作成してみます。また `COPY` 命令を使い、 `hello` をコンテナ内に置きましょう。 `COPY ./hello /` で、 ホスト上の `./hello` をコンテナ内の `/` にコピーする命令です。

```
cat << EOF > Dockerfile
FROM scratch
COPY hello /
EOF
```

Dockerfile の中身を確認します。

```
$ cat Dockerfile
FROM scratch
COPY hello /
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13002/e6bbc00e-cbbf-5c77-4e16-0d67ad4165ce.png)


イメージをビルドします。 `-t` でタグ `myhello` とします。

```
$ docker build -t myhello .
Sending build context to Docker daemon  16.38kB
Step 1/2 : FROM scratch
 --->
Step 2/2 : COPY hello /
 ---> Using cache
 ---> d1a7687418b6
Successfully built d1a7687418b6
Successfully tagged myhello:latest
```

これで `myhello` イメージが作成できました。イメージ一覧を確認します。

```
$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
myhello             latest              d1a7687418b6        4 minutes ago       13.3kB
hello-world         latest              bf756fb1ae65        5 months ago        13.3kB
```

それでは、このイメージでコンテナを実行しましょう。

```
$ docker run --rm myhello
docker: Error response from daemon: No command specified.
See 'docker run --help'.
```

なぜこのようなエラーになったか分かりますか？

`No command specified` とあります。`myhello` という名称のイメージはでき、コンテナとして実行できる準備は整いました。しかし、そのコンテナとして何を実行するか指定がなかったから、このような出力になりました。

次は、コンテナ内で `/hello` を実行するように、 `docker run --rm myhello /hello` として実行します。

```
$ docker run --rm myhello /hello

Hello from Docker!
This message shows that your installation appears to be working correctly.
...略...
```

今度は正常に実行できました！

ただ、毎回コマンドを指定するのは面倒です。デフォルトで `/hello` を実行するように `CMD` 命令を追加した Dockerfile を準備しましょう。

```
cat << EOF > Dockerfile
FROM scratch
COPY hello /
CMD ["/hello"]
EOF
```

改めて Dockerfile を確認します。

```
$ cat Dockerfile
FROM scratch
COPY hello /
CMD ["/hello"]
```

この状態で、イメージ・レイヤは概念的にこのような重なりとなっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13002/f6b4556b-29c9-f7d3-eecf-2cf13580a9d0.png)



それでは再度 build します。今度は `v2` というタグを付けます。

```
$ docker build -t myhello:v2 .
Sending build context to Docker daemon  16.38kB
Step 1/3 : FROM scratch
 --->
Step 2/3 : COPY hello /
 ---> Using cache
 ---> d1a7687418b6
Step 3/3 : CMD ["/hello"]
 ---> Running in 98bb18184d82
Removing intermediate container 98bb18184d82
 ---> 5e11c5479c11
Successfully built 5e11c5479c11
Successfully tagged myhello:v2
```

補足説明１： `Using cache` とは、Docker がイメージをビルドする時、既に `Dockerfile` で書かれたイメージ内容が一致する場合に、デフォルトの挙動はキャッシュを利用します（ build 時に ` --no-cache` オプションでキャッシュを使わない指定もできます）。

補足説明２： `Running in 98bb18184d82` とは、ビルド中にこの `CMD ["/hello"]` を書き込むためのコンテナ（中間コンテナと呼びます）を自動実行しています。そして、そのコンテナ用のイメージ・レイヤに書き込まれた内容をイメージ・レイヤに変換する処理、コミット（`docker commit`）を行い、中間コンテナが自動削除 `Removing intermediate container 98bb18184d82` されています。

補足説明３：なお `docker commit` でイメージ・レイヤにコミット（変換）するのは、ファイルシステムとメタ情報のみです。コンテナ実行時のログ（Docker用語のログとは、コマンド等を実行した標準出力のこと）はレイヤとは別の場所に保存されており、ログの情報はコミットされません。


それでは、作成した `myhello:v2` イメージのコンテナを実行します。

```
$ docker run --rm myhello:v2

Hello from Docker!
This message shows that your installation appears to be working correctly.
...省略...
```

このように先ほどとは異なり、自動的に `/hello` を実行するイメージを作成できました。

また、 `docker images` コマンドを実行すると、新旧2つのバージョンの `myhello` が同一ホスト上で共存できているのも分かります。

```
# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED              SIZE
myhello             v2                  5e11c5479c11        About a minute ago   13.3kB
myhello             latest              d1a7687418b6        14 minutes ago       13.3kB
```

ここで `docker history myhello:latest` と `docker history myhello:v2` を実行・比較します。何か気づくことがありますか？

```
$ docker history myhello:latest
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
d1a7687418b6        15 minutes ago      /bin/sh -c #(nop) COPY file:801a928f8ba2b08b…   13.3kB
$ docker history myhello:v2
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
5e11c5479c11        2 minutes ago       /bin/sh -c #(nop)  CMD ["/hello"]               0B          
d1a7687418b6        15 minutes ago      /bin/sh -c #(nop) COPY file:801a928f8ba2b08b…   13.3kB     
```

このように、イメージ（レイヤを示す） ID `d1a7687418b6` の部分が重複しているのが分かります。一見すると2つのイメージが別々に存在するかのように見えますが、`latest` と `v2` の違いは、 `5e11c5479c11` （ `CMD ["/hello"]` 命令の有無）だけであること、 `d1a7687418b6` （ `hello` をコピーしたイメージ・レイヤ）は共有していることが分かります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13002/d2f3940f-63b9-1510-793c-7d32634d4231.png)

このようにイメージが親子関係を持ち、共通するイメージ・レイヤはイメージ間で共有できます。複数の派生バージョンがある場合も、上手く利用すると、ホスト上で容量をあまり消費せずに利用できます。

## 6. `my-hello-world` イメージをソースコードからビルドする（開発者向け）

※以下では日本語が扱える環境を想定しています

公式イメージ `hello-world` に含まれるバイナリ `hello` は C 言語で記述されています。[GitHub上のソースコード](https://github.com/docker-library/hello-world) を参考に、日本語でメッセージを表示する `my-hello-world` を作成します。

今回は、ホスト上に C 言語の開発環境を構築するのではなく、Docker イメージ内に GCC とソースコードを入れ、コンパイルします。そして、成果物のバイナリファイルだけを、コンテナ内に格納し、それを実行するための Docker イメージを構築します。この一連の流れでマルチステージ・ビルドを使います。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13002/ffed2cc0-edd0-7cdb-7af7-c792cc34ce52.png)


まず、作業用ディレクトリを作成し、移動します。

```
$ mkdir my-hello-world
$ cd my-hello-world
```

次に、次のような `hello.c` をエディタ等で作成します。

```c
#include <sys/syscall.h>
#include <unistd.h>

#ifndef DOCKER_IMAGE
        #define DOCKER_IMAGE "my-hello-world"
#endif

#ifndef DOCKER_GREETING
        #define DOCKER_GREETING "Dockerから、こんにちは！"
#endif

#ifndef DOCKER_ARCH
        #define DOCKER_ARCH "amd64"
#endif

const char message[] =
        "\n"
        DOCKER_GREETING "\n"
        "このメッセージが表示されていれば、インストールは正常終了しました。\n"
        "\n"
        "メッセージを表示するために、Dockerは以下の手順を処理しました：\n"
        " 1. DockerクライアントはDockerデーモンに接続。\n"
        " 2. DockerデーモンはDocker Hubから\"" DOCKER_IMAGE "\" イメージをダウンロード。\n"
        "    (" DOCKER_ARCH ")\n"
        " 3. Dockerデーモンはダウンロードしたイメージから、実行可能な新しいコンテナを作成し、\n"
        "    今あなたが読んでいるこのメッセージを表示します。\n"
        " 4. Dockerデーモンは出力結果をDockerクライアントに流し、あなたのターミナルに出力します。\n"
        "\n"
        "さらにチャレンジするには、Ubuntu コンテナを次のコマンドで動かしましょう：\n"
        " $ docker run -it ubuntu bash\n"
        "\n"
        "イメージの共有、自動ワークフローなどの機能は、フリーなDocker IDで行えます：\n"
        " https://hub.docker.com/\n"
        "\n"
        "更なる例や考え方は、ドキュメントをご覧ください：\n"
        " https://docs.docker.com/get-started/\n"
        "\n";

int main() {
        //write(1, message, sizeof(message) - 1);
        syscall(SYS_write, STDOUT_FILENO, message, sizeof(message) - 1);

        //_exit(0);
        //syscall(SYS_exit, 0);
        return 0;
}

```

それから、次の `Dockerfile` を作成します。

```
FROM alpine:latest AS build
RUN apk -U add gcc libc-dev
COPY hello.c /
RUN gcc -static -o hello hello.c

FROM scratch AS release
COPY --from=build /hello /
CMD ["/hello"]
```

これはマルチステージ・ビルド機能を使っています。 `build` ステージでは `alpine:latest` の Alpine Linux 環境上で C 言語のコンパイル環境を作り、 `hello.c` をコンパイルします。

次に、 `release` ステージで、 `build` ステージでコンパイルした `hello` をコピーします。そして `CMD` 命令で、コンテナ実行時にこの `hello` バイナリを実行するよう指定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13002/cdbe9de3-5f2e-6745-2088-218fdb845faa.png)


このマルチステージ・ビルドを使うことで、最終成果物の `my-hello-world` イメージには `/hello` バイナリしか存在しない、最小限の Docker イメージを作成できます。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13002/c43bfde1-673e-7cce-8aac-62abbb6f7af8.png)



あとは、実際にビルドしましょう。

```
$ docker build -t my-hello-world:latest .
...省略...
Step 7/7 : CMD ["/hello"]
 ---> Running in d4a997c55ad7
Removing intermediate container d4a997c55ad7
 ---> 248e4fe57d26
Successfully built 248e4fe57d26
Successfully tagged my-hello-world:latest
```

`docker images` コマンドで、イメージが作成されたことを確認します。

```
$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
my-hello-world      latest              248e4fe57d26        35 seconds ago      51.2kB
<none>              <none>              aaf8ce6a03ec        36 seconds ago      146MB
```


ここで `<none>` という名前のイメージができています。これは `build` ステージのビルド課程で作成された中間イメージです。

注目すべきは２つのイメージ容量の違いです。中間イメージの容量（ `SIZE` ）は `146MB` あるのに対して、 `my-hello-world` は `51.2kB` しかありません。マルチステージ・ビルドを活用すると、実利用時に必要なイメージの容量を最低限に抑えられます。

それでは改めて `<none>` イメージの詳細をみてみます。 `docker history <イメージID>` を実行すると、イメージの製作過程が分かります。この例では `aaf8ce6a03ec` がイメージ ID ですが、皆さんの環境ごとに違いますのでご注意ください。

```
$ docker history aaf8ce6a03ec
IMAGE               CREATED              CREATED BY                                      SIZE                COMMENT
aaf8ce6a03ec        About a minute ago   /bin/sh -c gcc -static -o hello hello.c         51.2kB     
d31995be0993        About a minute ago   /bin/sh -c #(nop) COPY file:a4bbbb3d6b55d6c8…   1.88kB    
bd28c3199374        About a minute ago   /bin/sh -c apk -U add gcc libc-dev              140MB      
a24bb4013296        2 weeks ago          /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B         
<missing>           2 weeks ago          /bin/sh -c #(nop) ADD file:c92c248239f8c7b9b…   5.57MB
```

また、 `my-hwllo-world:latest` イメージの `docker history` も確認しましょう。

```
$ docker history my-hello-world
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
248e4fe57d26        2 minutes ago       /bin/sh -c #(nop)  CMD ["/hello"]               0B          
8b646124473a        2 minutes ago       /bin/sh -c #(nop) COPY file:667e96ac9d2f55ef…   51.2kB     
```

こちらは `scratch` という空っぽの Docker イメージからスタートしましたので、 `build` ステージからファイルをコピーした `COPY` 命令と、 `CMD` 命令で `/hello` を実行する指定しかないのが分かります。

あとは、期待通りに表示できるかどうか確認しましょう。

```
$ docker run my-hello-world

Dockerから、こんにちは！!
このメッセージが表示されていれば、インストールは正常終了しました。

メッセージを表示するために、Dockerは以下の手順を処理しました：
 1. DockerクライアントはDockerデーモンに接続。
 2. DockerデーモンはDocker Hubから"my-hello-world" イメージをダウンロード。
    (amd64)
 3. Dockerデーモンはダウンロードしたイメージから、実行可能な新しいコンテナを作成し、
    今あなたが読んでいるこのメッセージを表示します。
 4. Dockerデーモンは出力結果をDockerクライアントに流し、あなたのターミナルに出力します。

さらにチャレンジするには、Ubuntu コンテナを次のコマンドで動かしましょう：
 $ docker run -it ubuntu bash

イメージの共有、自動ワークフローなどの機能は、フリーなDocker IDで行えます：
 https://hub.docker.com/

更なる例や考え方は、ドキュメントをご覧ください：
 https://docs.docker.com/get-started/
```

あとは、 hello.c を再度書き換えてビルドしたり、条件を変えるなどしてお試しください。

ここでは C 言語を例に取り上げ、ホスト上に言語の開発環境を整えなくても、Docker コンテナでコンパイルをしたり、実行できるバイナリをコンテナ化することを確認しました。

もしろん、C 言語だけでなく、任意の言語で、様々なバージョンが混在するような開発環境でも活用できます。1つのホスト上でありながら、ホスト上の依存関係に一切影響を与えることなく、かつ、コンテナ間の環境の違いを気にせず、スムーズな作業への活用が期待できます。


## まとめ

Docker イメージ、イメージ・レイヤ、コンテナの違いについて理解が深まりましたでしょうか。

* 抽象的な Docker イメージとは、イメージ・レイヤ（層）の積み重ねで構成されています。
* Docker コンテナ実行とは、ホスト上にある複数のディレクトリやファイルを１つにファイルシステム内にマウントして見えるようにし、そのファイルシステム内にあるプログラムを特別な状態（名前空間の分離など）で起動するものです。
* Docker コンテナで追加されたイメージ・レイヤをコミットし、新しいイメージ・レイヤを作成できます。
通常は Dockerfile を使い、docker build コマンドで、Docker イメージを自動構築します。

さらに詳しい情報や、Docker について、Dockerfile の詳しい書き方は、以下のスライド資料をご覧ください。

* [コンテナの作り方「Dockerは裏方で何をしているのか？」](https://www.slideshare.net/zembutsu/what-isdockerdoing)
* [Dockerfile を書くためのベストプラクティス解説編](https://www.slideshare.net/zembutsu/explaining-best-practices-for-writing-dockerfiles)

最新版ドキュメントの日本語訳や、以下のセクションが参考になります。

* Dockerfile のベスト・プラクティス — Docker-docs-ja 19.03 ドキュメント
  * https://docs.docker.jp/develop/develop-images/dockerfile_best-practices.html
* Docker で開発 — Docker-docs-ja 19.03 ドキュメント
  * https://docs.docker.jp/develop/index.html

Enjoy!

# 参考文章

* About storage drivers | Docker Documentation
  * https://docs.docker.com/storage/storagedriver/
* イメージ、コンテナ、ストレージ・ドライバについて — Docker-docs-ja 17.06 ドキュメント
  * http://docs.docker.jp/engine/userguide/storagedriver/imagesandcontainers.html
* Explaining Docker Image IDs
  * https://windsock.io/explaining-docker-image-ids/
* Issues using /bin/sh in CMD command in scratch docker container #17896
  * https://github.com/moby/moby/issues/17896