概要 - 自分でオレオレ hello-world イメージをビルド
このチュートリアルは、hello-world
イメージを自分でビルドする例を取り上げます。手を動かしながら、Docker イメージの仕組みや性質の理解を深めます。また、効率的な Docker イメージの作成や Dockerfile の活用を目指すための基礎のほか、(主に開発者向けには)マルチステージ・ビルドも学びます。
ポイントは、Docker イメージ(image)とは、Docker コンテナの実行に必要な概念としてのパッケージ(ファイルやメタ情報の集合体)であることです。仮想マシンイメージのように、実体としての1ファイルではありません。
そして、Docker イメージを構成するのは、抽象的なイメージ・レイヤ(image layer)の集まりです。レイヤとは「層」の意味で、Docker は複数のレイヤ上のファイルシステムを、1つに扱えます。一般的に Docker イメージは、複数のイメージ・レイヤで構成されます。また、イメージ・レイヤは読み込み専用であり、レイヤ間では親子の依存関係を持てます。
通常、 Docker イメージを自動構築できるように定義するのが Dockerfile と呼ぶ設定ファイルであり、このファイル内でイメージを構成する命令を書きます。この命令の1つ1つが、概念上のイメージ・レイヤに相当します。
なお、コマンドの実行環境は Docker CE 19.03、ホスト OS は Linux (amd64環境)、かつストレージドライバはデフォルト( overlay2
)を想定しています。
※ 表現で分かりづらい点がありましたら、ご指摘ください。
チュートリアル
1. Docker イメージとイメージ・レイヤ
Docker イメージとは、親子関係を持つ、複数のイメージ・レイヤ(image layer)によって構成されています。イメージ・レイヤは読み込み専用です。Docker は、複数のイメージ・レイヤに含むファイルやディレクトリの情報を1つに統合する技術を使っています。
このイメージ・レイヤの中には、Docker コンテナの実行に必要な Linux ファイルシステムとメタ情報を含みます。Linux ファイルシステムというのは、 /
ディレクトリ以下の /etc
/bin
/sbin
/usr
などのディレクトリ階層およびファイルです。
Docker では、コンテナとして動かしたいアプリケーションが必要とする、最小限のファイルを Docker イメージの中に入れられます(正確にはイメージ・レイヤ内のファイルシステムに入れられます)。
また、1つ1つのイメージ・レイヤには親子関係を持ちます。より上位にあるイメージ・レイヤからは、親となるイメージ・レイヤ上のファイルシステムも参照できます。つまり、Docker イメージをダウンロードすると、そのイメージが複数のイメージ・レイヤで構成されていたとしても、それを意識せずに利用できます。
さらに、そのアプリケーションを動かすために必要なデフォルトのコマンドや引数の指定、外に公開するポート番号の情報、ボリューム領域などの情報があります。これらをメタ情報として、同じく Docker イメージ・レイヤの中に入れられます。
このように 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 言語で書かれ た 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つのレイヤですが、イメージによっては複数のレイヤで構成されます。
- Docker Hub 上にあるイメージ・レイヤ
-
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)しました。
このように、 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 、 参考2 )
画面を末尾までスクロールすると、次のようなセクションが見えます。
"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
というバイナリしかないことがわかります。
(ちなみに、今回の例では1つのファイルしかありませんが、例えば ubuntu
や centos
など Linux ディストリビューションのイメージをダウンロードすると、各イメージ用のディレクトリ内には ./bin/
./sbin/
./var/
など各ディストリビューション用の /
以下ファイルシステムが展開されています。そして、それらで Docker コンテナを実行すると、ホスト上とコンテナ内で異なる Linux ディストリビューションが動作しているように "見える" のですが、実際にはホスト上の Linux Kernel 上で、Docker は指定した Doker イメージのディストリビューション、たとえば centos
であれば centos のファイルシステムをマウントし、デフォルトではその中に含まれる /bin/bash を PID 1 とする名前空間内で実行しています。 コンテナ実行時、1つの Linux 上で、複数の Linux が動作している訳ではありません。)
今回実行した hello-world
はレイヤが1つしかないため、ホスト上ではこの1つのディレクトリ内に hello-world
Docker イメージの内容物を全て含みます。そのため、複数のイメージ・レイヤで構成する Docker イメージがあれば、ホスト上に複数のディレクトリが存在します。
さらに、イメージにはメタ情報を含みます。 hello-world
では CMD
命令で /hello
を実行する命令がありました。このメタ情報にもイメージ・レイヤを必要とします(なお、メタ情報は概念としてのイメージ・レイヤであり、ホスト上では実体としてのファイルやディレクトリはありません)。
このようにして、Docker エンジンは Docker のイメージ・レイヤを抽象的な Docker イメージという単位で扱えるようにしています。Docker コンテナ実行時、ホスト上ではバラバラのファイルやディレクトリを、1つのファイルシステムに統合して操作可能なようにしています。
以上のことからも、Docker イメージとは、ホスト上で1つの実体としてのファイルが存在していないことが分かります。
さて、この 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 に書かれている通りの文字列です。
以上のことから、 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 イメージ内にあるファイルシステム内で、プログラムを特別な状態として起動します。
つまり、hello-world
を Docker コンテナで動かすとは、
-
hello-world
という名前の Docker イメージを準備する -
hello-world
のイメージ内にある、何らかのプログラムを実行する - イメージ内の定義(CMD命令)では
/hello
を自動的に実行する - ただし、
/hello
をコンテナとして(名前空間等に制約を受けて)実行
以上の動作を行います。
また、コンテナには、コンテナ用の読み書きできる Docker イメージ・レイヤが自動的に作成されます。このイメージ・レイヤは通常のイメージ用と同じく、親子関係を持ちます。そのため、同じホスト上で複数のコンテナを実行しても、元々存在する Docker イメージ以上の容量を必要としない利点があります。
それでは 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
が存在するディレクトリが、コンテナを実行するプロセス空間で /
としてマウントします。
つまり、 /hello
しか存在しないファイルシステム上で、Linux のユーザ空間内で hello
のプロセスしか実行していないように見える特別な状態が存在しています。これが Docker コンテナです。そして、コンテナは、この名前空間内で PID 1 として hello
を実行します。 hello
が画面に文字列を出力した後 exit
状態となり、コンテナそのものが実行終了( exited )となります。
このように、
- 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 … 実行時に指定しなければ自動的にランダムな文字列( 形容詞+計算機科学系の貢献者 )
この 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 でビルドする流れは以下の図のようになります。
ここからは、自分専用の 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 イメージ指定は、実体が何も無い特別な指定です )
そして、ここで 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 /
イメージをビルドします。 -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"]
この状態で、イメージ・レイヤは概念的にこのような重なりとなっています。
それでは再度 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
補足説明1: Using cache
とは、Docker がイメージをビルドする時、既に Dockerfile
で書かれたイメージ内容が一致する場合に、デフォルトの挙動はキャッシュを利用します( build 時に --no-cache
オプションでキャッシュを使わない指定もできます)。
補足説明2: Running in 98bb18184d82
とは、ビルド中にこの CMD ["/hello"]
を書き込むためのコンテナ(中間コンテナと呼びます)を自動実行しています。そして、そのコンテナ用のイメージ・レイヤに書き込まれた内容をイメージ・レイヤに変換する処理、コミット(docker commit
)を行い、中間コンテナが自動削除 Removing intermediate container 98bb18184d82
されています。
補足説明3:なお 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
をコピーしたイメージ・レイヤ)は共有していることが分かります。
このようにイメージが親子関係を持ち、共通するイメージ・レイヤはイメージ間で共有できます。複数の派生バージョンがある場合も、上手く利用すると、ホスト上で容量をあまり消費せずに利用できます。
6. my-hello-world
イメージをソースコードからビルドする(開発者向け)
※以下では日本語が扱える環境を想定しています
公式イメージ hello-world
に含まれるバイナリ hello
は C 言語で記述されています。GitHub上のソースコード を参考に、日本語でメッセージを表示する my-hello-world
を作成します。
今回は、ホスト上に C 言語の開発環境を構築するのではなく、Docker イメージ内に GCC とソースコードを入れ、コンパイルします。そして、成果物のバイナリファイルだけを、コンテナ内に格納し、それを実行するための Docker イメージを構築します。この一連の流れでマルチステージ・ビルドを使います。
まず、作業用ディレクトリを作成し、移動します。
$ mkdir my-hello-world
$ cd my-hello-world
次に、次のような hello.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
バイナリを実行するよう指定します。
このマルチステージ・ビルドを使うことで、最終成果物の my-hello-world
イメージには /hello
バイナリしか存在しない、最小限の Docker イメージを作成できます。
あとは、実際にビルドしましょう。
$ 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
ステージのビルド課程で作成された中間イメージです。
注目すべきは2つのイメージ容量の違いです。中間イメージの容量( 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 コンテナ実行とは、ホスト上にある複数のディレクトリやファイルを1つにファイルシステム内にマウントして見えるようにし、そのファイルシステム内にあるプログラムを特別な状態(名前空間の分離など)で起動するものです。
- Docker コンテナで追加されたイメージ・レイヤをコミットし、新しいイメージ・レイヤを作成できます。
通常は Dockerfile を使い、docker build コマンドで、Docker イメージを自動構築します。
さらに詳しい情報や、Docker について、Dockerfile の詳しい書き方は、以下のスライド資料をご覧ください。
最新版ドキュメントの日本語訳や、以下のセクションが参考になります。
- Dockerfile のベスト・プラクティス — Docker-docs-ja 19.03 ドキュメント
- Docker で開発 — Docker-docs-ja 19.03 ドキュメント
Enjoy!
参考文章
- About storage drivers | Docker Documentation
- イメージ、コンテナ、ストレージ・ドライバについて — Docker-docs-ja 17.06 ドキュメント
- Explaining Docker Image IDs
- Issues using /bin/sh in CMD command in scratch docker container #17896