Docker入門：Docker概要,基本操作,マウント,Dockerfile,マルチステージビルド

最近ちょっとDockerを入門しました。
アウトプットも兼ねて、ハンズオンっぽくまとめてみたので公開したいと思います。
まだ「Dockerを触ったことない」という方の入門に使って頂けたら幸いです。

※出力結果のすべては記載できていません。(ハンズオンで一緒に操作して確認しながら進める想定だったため)


# 目次
- 事前準備(環境構築)
- Docker概要
- ハンズオン1：基本操作
- ハンズオン2：マウント
- ハンズオン3：Dockerfile
- ハンズオン4：マルチステージビルド
- おわり

# 事前準備(環境構築)
公式ドキュメントなど参考に構築してください。
[Docker Documentation - Get started with Docker](https://docs.docker.com/)


[Docker Hub](https://hub.docker.com/)アカウントをお持ちであれば、[Play with Docker](https://labs.play-with-docker.com/)が便利です。
インストールなしでDockerを試すことができます。Login、Start、ADD NEW INSTANCEするだけです。
参考：[Docker 入門にはインストールなしで使える「Play with Docker」がいいと思う](https://qiita.com/tag1216/items/ec7228440f6eaf849cd6)


Windowsであれば、自分も以下の記事を書いています。良ければ参考にしてみてください。
[VirtualBoxとVagrantでDocker環境構築](https://qiita.com/shiro01/items/c5a956de9e638acb3beb)
※以降の内容は、上記記事で環境構築した前提で進めています。
※別の方法で構築した場合は IPアドレス など適宜読み替えてください。


# Docker概要
ハンズオンに入る前にDockerとは？を説明したいと思います。
とはいえ、Dockerで検索すると良記事がたくさん見つかるので大雑把にざっくりと説明します。

## Dockerとは？
”コンテナ”を利用した基盤ソフトウェアです。
コンテナ型仮想化技術と言われたりします。

[基盤ソフトウェア](http://www.ocw.titech.ac.jp/index.php?module=Archive&action=KougiInfo&GakubuCD=226&GakkaCD=226715&KougiCD=75028&Nendo=2011&Gakki=1&lang=JA)とは、
高度なアプリケーションを構築するための基盤となるソフトウェアのことです。
(オペレーティング・システムやミドルウェア、言語処理系など)

## コンテナとは？
OSの基本コマンドやアプリケーションなどの実行環境全体をパッケージ化して、
それらを分離された空間で実行する技術です。

ホストOS上で独立したプロセスとして実行されます。
コンテナはDocker独自の技術ではなく、Linuxカーネルのcgroupやnamespaceなどを使用して実現しています。

[cgroup](https://ja.wikipedia.org/wiki/Cgroups)：プロセスグループのリソース(CPU、メモリ、ディスクI/Oなど)の利用を制限・隔離する機能。
[namespace](https://linuxjm.osdn.jp/html/LDP_man-pages/man7/namespaces.7.html)：名前空間内のプロセスに対して、専用のリソースを持っているかのように見せる仕組み。

コンテナを利用したソフトウェアはDocker以外にもいくつか存在します。
例：[Linuxコンテナ(LXC)、LXD](https://linuxcontainers.org/ja/)、[Virtuozzo](https://ja.wikipedia.org/wiki/Parallels_Virtuozzo_Containers)、[OpenVZ](https://openvz.org/)


## 物理基盤、ハイパーバイザー型、コンテナの比較
簡単にですが比較した図を用意してみました。
ざっくりなので省略してしまっている箇所もあるかもですが、イメージをつかんでもらえればと思います。

![物理基盤、ハイパーバイザ型との比較.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200585/bdcc59d4-c9cc-4b40-708e-01cfa230791c.png)


## Dockerの構成要素
Dockerの構成要素がざっくりわかるように図にしてみました。
こちらも省略している箇所がありますが、イメージをつかんでもらえればと思います。

![2019-09-14_16h17_11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200585/81442c22-8b81-79d3-37ef-aad1adc18991.png)

以下簡単にですが、各要素の説明です。
・Dockerエンジン：アプリケーションのパッケージ化やコンテナを実行する。Dockerの主機能。
・Dockerクライアント：Dockerを操作するためのコマンドライン・インターフェース（CLI）。Dockerエンジンに含まれる。
・Dockerイメージ：OSやアプリケーションを含んだテンプレート。コンテナ作成のもとになる。
・Dockerコンテナ：アプリケーションの実行するための分離された空間。イメージから作成される。
・レジストリ：イメージを公開・共有する為のイメージ保管場所。リポジトリという単位で保管されている。
・リポジトリ：同じ種類のDockerイメージの集まり(タグが付与されバージョン分けされたDockerイメージの集まり)

レジストリにはDocker公式のDocker Hub以外にも
Amazon Elastic Container Registry、Google Container Registry、プライベートレジストリなどがあります。

## Dockerに向くシステム、向かないシステム
[参考にした本](https://www.amazon.co.jp/dp/4295005525)にDockerに向くシステム、向かないシステムについて記載があったので紹介したいと思います。

### Dockerに向くシステム
・システムの改変、開発が頻発
・比較的、可用性を厳密に求めない
・サポートのないOSSを利用する
・システムの問題を自社で解決できる

例：
オンラインゲームサーバー
サーバーホスティング
広告・検索ビッグデータ基盤


### Dockerに向かないシステム
・システムの改変をほとんど行わない
・商用のOSとアプリが稼働し保守が必要
・可用性、信頼性、機密性が求められる
・システム停止が社会的影響を及ぼす

例：
原子力発電所制御システム
銀行・証券オンライン基盤
航空・鉄道オンライン基盤

　
　
ざっくりですが、以上でDocker概要は終わりにしたいと思います。
次項からハンズオンをやっていきます。

#Dockerコマンドについて
Dockerコマンドには新旧で多くの違いがあります。このハンズオンでは新しいコマンドを紹介します。
そのため、過去にDockerを学んだことがある方には若干の違和感があるかと思います。

```console:違い例
コンテナ一覧表示
$ docker container ls  # 新
$ docker ps            # 旧
```

以下はコマンドラインを表しています。`]$`、`]#`以降がコマンドになります。

```console:コマンドラインについて
[vagrant@handson ~]$ 
[root@test01 /]# 
```

以下のようにコマンド後の`#`はコメントです。実行時は入力不要です。

```console:コマンドラインについて
[vagrant@handson ~]$ docker search centos           # イメージの検索
```

以下のようにコマンドが長い場合は`\`で改行しています。

```console:コマンドラインについて
[vagrant@handson ~]$ docker container stats -a --no-stream \
                       --format "table {{.ID}}\t{{.Name}}\t{{.CPUPerc}}\t{{.MemPerc}}\t{{.MemUsage}}\t{{.PIDs}}"
```

# ハンズオン1：基本操作
Dockerコマンドを使用したイメージの取得やコンテナの起動など、基本操作をやっていきます。

## イメージの検索、取得、一覧表示
まずは、コンテナのもとになるイメージの取得と取得したイメージの確認からやっていきます。

```console:イメージの検索、取得、一覧表示
[vagrant@handson ~]$ docker search --filter "is-official=true" centos    # イメージの検索

[vagrant@handson ~]$ docker image pull centos       # イメージの取得

[vagrant@handson ~]$ docker image ls                # 取得したイメージ一覧表示
[vagrant@handson ~]$ docker image ls --no-trunc     # イメージIDを短縮せず表示
[vagrant@handson ~]$ docker image ls -q             # イメージIDのみ表示
[vagrant@handson ~]$ docker image ls --no-trunc -q  # 同時に指定可能
```

`docker search`では`--filter "is-official=true"`をオプションで指定することでDocker Official Imagesのみ表示しています。

## イメージ削除、イメージ取得(タグ指定あり)
`docker search`では表示されませんが、イメージにはタグ(TAG)が付けられています。
`docker image pull`でタグを指定せずに取得するとイメージの最新版(latest)が取得されます。
タグは[DockerHub](https://hub.docker.com/search?type=image)で検索すると確認できます。

イメージを削除する場合は`docker image rm <イメージ名>:<タグ名>`で削除します。

```console:イメージ削除、イメージ取得(タグ指定あり)
[vagrant@handson ~]$ docker image rm centos:latest  # イメージの削除
[vagrant@handson ~]$ docker image pull centos:7     # タグを指定してイメージ取得
```

イメージは同じでタグが違うものを削除した場合は、タグのみが削除されます。
イメージの削除はスペース区切りで複数指定可能です。

## コンテナを起動してコマンド実行

```console:コンテナを起動してコマンド実行
[vagrant@handson ~]$ docker container run -i -t centos:7 date              # コンテナを起動しコマンド実行
[vagrant@handson ~]$ docker container run -it centos:7 date -d tomorrow    # `-i`と`-t`は`-it`でも可
[vagrant@handson ~]$ docker container run -it centos:7 date "+%Y/%m/%d" -d "30 days"
```

-i：コンテナの STDIN にアタッチ。標準入力を受け付ける。
-t：疑似ターミナル (pseudo-TTY) を割り当て。

## コンテナ一覧表示(停止中のコンテナを含む全コンテナ一覧)
```console:コンテナ一覧表示
[vagrant@handson ~]$ docker container ls -a    # コンテナ一覧表示
```

コマンド実行に使用したコンテナが表示されます。

## コンテナを名前付きで起動
コンテナ一覧で表示される`NAMES`は指定することができます。

```console:コンテナを名前付きでコンテナ起動
[vagrant@handson ~]$ docker container run -it --name test01 centos:7 date  # コンテナを名前付きで起動
[vagrant@handson ~]$ docker container ls -a

[vagrant@handson ~]$ docker container run -it --name test01 centos:7 date  # 使用されているコンテナ名を指定するとエラーになる
```

##停止中のコンテナ削除
`docker container rm <コンテナ名 or CONTAINER ID>`で停止中のコンテナを削除できます。
`docker container prune`を使用すると停止中の全コンテナを削除できます。

```console:停止中のコンテナ削除
[vagrant@handson ~]$ docker container rm test01    # 停止中のコンテナの削除 (スペース区切りで複数指定可能)
[vagrant@handson ~]$ docker container ls -a

[vagrant@handson ~]$ docker container prune    # 停止中の全コンテナの削除
Are you sure you want to continue? [y/N] y     # y を応答
[vagrant@handson ~]$ docker container ls -a
```

## コンテナ起動(削除オプション付き)
コマンドを実行するだけの一時的なコンテナを毎回削除するのはめんどうです。
コンテナが停止すると自動でコンテナを削除する`--rm`オプションがあります。

```console:コンテナ起動(削除オプション付き)
[vagrant@handson ~]$ docker container run --rm -it \
                       --name rmtest01 centos:7 cat /etc/redhat-release  # コンテナ停止で削除オプション付き
[vagrant@handson ~]$ docker container ls -a    # 起動したコンテナが存在しないことを確認
```

## コンテナ起動、ログイン
これまではコンテナを起動しコマンドを実行するのみでしたが、
次はコンテナにログインしてみたいと思います。

```console:コンテナ起動、ログイン
[vagrant@handson ~]$ docker container run --rm -it --name test01 centos:7 /bin/bash  # コンテナを起動しログイン
[root@dd6149b65301 /]#             # コンソールが表示される
[root@bad50470fb4f /]# hostname    # DockerによってコンテナIDがホスト名になる
bad50470fb4f
[root@dd6149b65301 /]# exit        # コンテナから離脱、コンテナは停止する
```

## ホスト名を指定してコンテナ起動、ログイン
ホスト名を指定しない場合、コンテナIDがホスト名になりますが、
ホスト名は起動時に指定することができます。

ログイン後にいくつかコンテナ内でコマンドを実行してみます。

```console:ホスト名を指定してコンテナ起動、ログイン
[vagrant@handson ~]$ docker container run -it --name test01 -h test01 centos:7 /bin/bash
[root@test01 /]#
[root@test01 /]# hostname    # ホスト名が起動時に指定したtest01になっている
test01

[root@test01 /]# echo "Docker container test01" > /test01.txt    # ファイルを追加してみる。
[root@test01 /]# cat /test01.txt
Docker container test01

[root@test01 /]# ping -c 3 192.168.10.10    # Dockerのホストマシン(Vagrantで起動したゲストマシン)と通信できる
[root@test01 /]# ping -c 3 google.com       # 外部サイトにもアクセスできる

[root@test01 /]# ip addr                    # IPアドレスの確認コマンドがインストールされていない
bash: ip: command not found
[root@test01 /]# yum install -y iproute     # yumでインストール可能
[root@test01 /]# ip addr                    # コンテナのIPアドレスを確認
```

## ログイン中のコンテナを起動した状態で離脱
ログインしたコンテナで`exit`を実行するとコンテナが停止してしまいます。
`Ctrl + p` を入力後 `Ctrl + q` を入力することでコンテナを起動した状態で離脱できます。

```console:ログイン中のコンテナを起動した状態で離脱
`Ctrl + p` を入力後 `Ctrl + q` を入力しコンテナから離脱

[vagrant@handson ~]$ docker container ls     # 起動中のコンテナを表示
```

## コンテナでの作業ログ確認
コンテナ内で作業した内容を確認することができます。

```console:コンテナでの作業ログ確認
[vagrant@handson ~]$ docker container logs test01 | less       # lessの終了は q を入力
[vagrant@handson ~]$ docker container logs -t test01 | less    # 時刻付きで表示
```

ログの実態はホストOS上の以下のファイルに格納されています。コンテナを削除すると消えます。
`/var/lib/docker/containers/<CONTAINER ID>/<CONTAINER ID>-json.log`


## コンテナからイメージ作成
コンテナからイメージを作成することができます。
コンテナ上で作業した内容を含んだ状態のイメージが作成できます。

「ホスト名を指定してコンテナ起動、ログイン」で起動したコンテナからイメージを作成してみます。

```console:イメージをコンテナから作成
[vagrant@handson ~]$ docker container commit test01 centos:test01    # コンテナから新しいイメージ作成
[vagrant@handson ~]$ docker image ls                                 # 新しいコンテナが作成されていることを確認

[vagrant@handson ~]$ docker container run -it --name test02 -h test02 centos:test01 /bin/bash  # 作成したイメージでコンテナ起動
[root@test02 /]# cat /test01.txt    # test01のコンテナで作成したファイルが存在するか確認
Docker container test01
[root@test01 /]# ip addr    # test01でインストールしたコマンドも存在するか確認

[root@test02 /]# echo "Docker container test02" > /test02.txt    # ファイルを追加してみる。
[root@test02 /]# cat /test02.txt
Docker container test02
[root@test02 /]# exit

[vagrant@handson ~]$ docker container ls -a
[vagrant@handson ~]$ docker container commit test02 centos:test02    # 停止したコンテナからもイメージ作成可能
[vagrant@handson ~]$ docker image ls

[vagrant@handson ~]$ docker container run -it --name test03 -h test03 centos:test02 /bin/bash
[root@test03 /]# cat /test01.txt test02.txt    # test01,test02で作成したファイルが存在することを確認
Docker container test01
Docker container test02
[root@test03 /]# exit
```

## 起動中のコンテナの削除
停止済みのコンテナについてはオプションなしで削除できます。
起動中のコンテナは`-f`を付けることで強制的に削除できます。

```console:起動中のコンテナの削除
[vagrant@handson ~]$ docker container ls -a
[vagrant@handson ~]$ docker container rm test01 test02 test03    # 停止しているコンテナのみ削除され、起動中のコンテナは失敗する
test02
test03
Error response from daemon: You cannot remove a running container <CONTAINER ID>. Stop the container before attempting removal or force remove

[vagrant@handson ~]$ docker container ls -a           # 削除に失敗したコンテナが残っている
[vagrant@handson ~]$ docker container rm -f test01    # 起動中のコンテナを強制削除
[vagrant@handson ~]$ docker container ls -a
```

## 停止中コンテナの起動、再接続
停止中のコンテナは再度起動することができます。
また、起動したコンテナには再接続することもできます。※`/bin/bash`が動いている場合

```console:停止中コンテナの起動、再接続
[vagrant@handson ~]$ docker container run -it --name test04 -h test04 centos:test02 /bin/bash
[root@test04 /]# exit    # exitでログアウトするとコンテナは停止する

[vagrant@handson ~]$ docker container ls -a
[vagrant@handson ~]$ docker container start test04    # 停止したコンテナの起動
[vagrant@handson ~]$ docker container attach test04   # コンテナに再接続
[root@test04 /]# exit
```

## イメージの強制削除
コンテナのもとになっているイメージはオプションなしでは削除できません。
`-f`を付けることによって強制的に削除することができます。(コンテナ停止中の場合)

```console:イメージの強制削除
[vagrant@handson ~]$ docker image ls
[vagrant@handson ~]$ docker image rm centos:test02    # 存在するコンテナのもとになっているイメージは削除が失敗する
Error response from daemon: conflict: unable to remove repository reference "centos:test02" (must force) - container <CONTAINER ID> is using its referenced image <IMAGE ID>
[vagrant@handson ~]$ docker image ls                     # イメージが削除されていないことを確認
[vagrant@handson ~]$ docker image rm -f centos:test02    # 存在するコンテナのもとになっているイメージを強制削除する
[vagrant@handson ~]$ docker image ls                     # イメージが削除される

[vagrant@handson ~]$ docker container ls -a            # コンテナ自体は残る
[vagrant@handson ~]$ docker container start test04     # 起動できる
[vagrant@handson ~]$ docker container attach test04    # 接続もできる
[root@test04 /]#
[root@test04 /]# exit

[vagrant@handson ~]$ docker container rm test04
```

## 起動中のコンテナのイメージ強制削除、コンテナの停止
起動中のコンテナのもとになっているイメージは削除することができません。
※強制削除してもイメージ自体は残る。

コンテナを停止することで削除することができます。

```console:起動中のコンテナのイメージ強制削除、コンテナの停止
[vagrant@handson ~]$ docker container run -it --name test05 -h test05 centos:test01 /bin/bash

`Ctrl + p` を入力後 `Ctrl + q` を入力しコンテナから離脱

[vagrant@handson ~]$ docker image ls
[vagrant@handson ~]$ docker image rm -f centos:test01
Untagged: centos:test01
[vagrant@handson ~]$ docker image ls    # コンテナ起動中のイメージを削除するとREPOSITORY、TAGが<none>のイメージが残り、イメージ自体は残る

[vagrant@handson ~]$ docker container ls
[vagrant@handson ~]$ docker container stop test05    # コンテナの停止
[vagrant@handson ~]$ docker image rm -f <IMAGE ID>   # 停止することで削除できる
[vagrant@handson ~]$ docker image ls

[vagrant@handson ~]$ docker container ls -a
[vagrant@handson ~]$ docker container rm test05
```

## 離脱した状態でコンテナ起動
コンテナにログイン中は`Ctrl + p` を入力後 `Ctrl + q` を入力するとコンテナから離脱できますが、
`-d`を指定することで離脱した状態でコンテナを起動できます。

```console:離脱した状態でコンテナ起動
[vagrant@handson ~]$ docker container run -itd \
                       --name testd01 -h testd01 centos:7 /bin/bash    # コンテナをバックグラウンドで起動する
[vagrant@handson ~]$ docker container ls
```

## 起動中のコンテナでコマンド実行

`exec`を使うと起動中のコンテナでコマンドが実行できます

```console:起動中のコンテナでコマンド実行
[vagrant@handson ~]$ docker container exec -it testd01 ps -ef       # コンテナでコマンド実行
[vagrant@handson ~]$ docker container exec -it testd01 /bin/bash    # exec でログインもできる
[root@test05 /]#
[root@test05 /]# ps -ef
[root@test05 /]# exit
exit
[vagrant@handson ~]$ docker container ls    # exitで抜けてもrunで起動した /bin/bash とは別のためコンテナは起動したままになる
[vagrant@handson ~]$ docker container rm -f testd01
```

`attach`を使うことで起動中のコンテナに再接続できますが、`exit`で離脱するとコンテナを誤って停止してしまうことがあります。
上記の例であるように`exec`で`/bin/bash`を起動して接続するのがおすすめです。
`/bin/bash`を新たに起動してログインするため、`/bin/bash`が起動していないコンテナにもログインできます。

## コンテナ情報表示
`inspect`でコンテナの詳細情報を確認することができます。
環境変数やIPアドレスなども確認することができます。

```console:コンテナ情報表示
[vagrant@handson ~]$ docker container run -itd --name test01 -h test01 centos:7 /bin/bash
[vagrant@handson ~]$ docker container inspect test01    # コンテナの情報表示

[vagrant@handson ~]$ docker container rm -f test01
test01
```

## コンテナのリソース確認
`stats`でコンテナのリソース状況を確認することができます。

```console:コンテナのリソース確認
[vagrant@handson ~]$ docker container run -itd --name test01 -h test01 centos:7 /bin/bash
[vagrant@handson ~]$ docker container stats -a --no-stream
[vagrant@handson ~]$ docker container stats -a --no-stream \
                       --format "table {{.ID}}\t{{.Name}}\t{{.CPUPerc}}\t{{.MemPerc}}\t{{.MemUsage}}\t{{.PIDs}}"
[vagrant@handson ~]$ docker container rm -f test01
```

オプションや表示のフォーマットについてはドキュメント参照
https://docs.docker.com/engine/reference/commandline/stats/

## ホストOSとコンテナ間のファイルやディレクトリのコピー
ホストOSと起動中のコンテナ間でファイルやディレクトリのコピーができます。

コンテナからホストOSへのコピー：
　`docker container cp <コンテナ名>:<コンテナ内コピー対象パス> <ホストOSコピー先パス>`
ホストOSからコンテナへのコピー：
　`docker container cp <ホストOSコピー対象パス> <コンテナ名>:<コンテナ内コピー先パス>`

```console:ホストOSとコンテナ間のファイルコピー例
[vagrant@handson ~]$ docker container run -itd --name test01 -h test01 centos:7 /bin/bash
[vagrant@handson ~]$ docker container cp test01:/etc/redhat-release ./        # test01コンテナからホストOSにファイルコピー
[vagrant@handson ~]$ ls
[vagrant@handson ~]$ docker container cp test01:/etc/redhat-release ./hoge.txt    # test01コンテナから名前指定でホストOSにファイルコピー
[vagrant@handson ~]$ ls
[vagrant@handson ~]$ docker container cp ./hoge.txt test01:/
[vagrant@handson ~]$ docker container exec -it test01 ls /        # コピーしたhoge.txtが確認できる
[vagrant@handson ~]$ docker container rm -f test01
```

# ハンズオン2：マウント
マウント機能を利用することで、ホストOSのフォルダをコンテナと共有できたり、コンテナ間でボリュームを共有することができます。
マウントのタイプは以下の3種類あります。それぞれ簡単にですが紹介します。

- [bind](https://docs.docker.com/storage/bind-mounts/)
- [volume](https://docs.docker.com/storage/volumes/)
- [tmpfs](https://docs.docker.com/storage/tmpfs/)

## bind
ホストOS上の任意のデバイスファイルやディレクトリをコンテナに見せることができます。
コンテナ起動時に以下のオプションを指定することでマウントできます。
`--mount type=bind,src=<ホストOS上のディレクトリ>,dst=<コンテナ上のディレクトリ>`
各ディレクトリは絶対パスを指定します。(ホストOS、コンテナともに)

```console:bindでマウント
[vagrant@handson ~]$ mkdir hostdir
[vagrant@handson ~]$ echo "Create file in host." > hostdir/hostfile01.txt
[vagrant@handson ~]$ ls hostdir/
hostfile01.txt
[vagrant@handson ~]$ cat hostdir/hostfile01.txt
Create file in host.

[vagrant@handson ~]$ docker container run -it --rm --name testbind01 -h testbind01 \
                       --mount type=bind,src=/home/vagrant/hostdir,dst=/ctdir01 centos:7 /bin/bash
[root@testbind01 /]#

[root@testbind01 /]# ls /ctdir01/
hostfile01.txt
[root@testbind01 /]# cat /ctdir01/hostfile01.txt    # ホストOSで作成したファイルが参照可能
Create file in host.

[root@testbind01 /]# echo 'Create file in container.' > /ctdir01/containerfile01.txt    # コンテナでファイル作成可能
[root@testbind01 /]# ls /ctdir01/
containerfile01.txt  hostfile01.txt
[root@testbind01 /]# cat /ctdir01/containerfile01.txt
Create file in container.

[root@testbind01 /]# mkdir /ctdir01/dir01    # ディレクトリも作成可能
[root@testbind01 /]# ls ctdir01/
containerfile01.txt  dir01  hostfile01.txt

[root@testbind01 /]# echo 'Create file  in dir01 in container.' > /ctdir01/dir01/containerfile01.txt
[root@testbind01 /]# cat /ctdir01/dir01/containerfile01.txt
Create file  in dir01 in container.

[root@testbind01 /]# rm -rf /ctdir01/dir01    # 削除可能
[root@testbind01 /]# ls /ctdir01/
containerfile01.txt  hostfile01.txt

[root@testbind01 /]# exit
exit

[vagrant@handson ~]$ cat hostdir/hostfile01.txt hostdir/containerfile01.txt
Create file in host.
Create file in container.
```

`readonly`を付けることで読み込み権限でマウントすることもできます。

```console:bindでマウント(readonly)
[vagrant@handson ~]$ docker container run -it --rm --name testbind02 -h testbind02 \
                       --mount type=bind,src=/home/vagrant/hostdir,dst=/ctdir01,readonly centos:7 /bin/bash
[root@testbind02 /]#

[root@testbind02 /]# ls /ctdir01/
containerfile01.txt  hostfile01.txt
[root@testbind02 /]# cat ctdir01/hostfile01.txt ctdir01/containerfile01.txt
Create file in host.
Create file in container.

[root@testbind02 /]# rm -rf ctdir01/
rm: cannot remove 'ctdir01/hostfile01.txt': Read-only file system
rm: cannot remove 'ctdir01/containerfile01.txt': Read-only file system

[root@testbind02 /]# rm -f /ctdir01/hostfile01.txt
rm: cannot remove '/ctdir01/hostfile01.txt': Read-only file system
[root@testbind02 /]# rm -f /ctdir01/containerfile01.txt
rm: cannot remove '/ctdir01/containerfile01.txt': Read-only file system
[root@testbind02 /]# exit
exit
```

`inspect`でコンテナ情報表示することで、マウント情報も確認できます。
後続のvolume、tmpfsでも同様に確認してみてください。

```console:マウント情報確認
[vagrant@handson ~]$ docker container run -itd --name testbind03 -h testbind03 \
                       --mount type=bind,src=/home/vagrant/hostdir,dst=/ctdir01,readonly centos:7 /bin/bash

[vagrant@handson ~]$ docker container inspect testbind03 |grep -A10 -e Mounts    # inspectでマウント情報が確認できる

[vagrant@handson ~]$ docker container rm -f testbind03
testbind03
```


## volume
ホストOS上のDockerで管理されるボリューム(/var/lib/docker/volumesに作成される)をコンテナに提供することができます。
コンテナ起動時に以下のオプションを指定することでマウントできます。(無い場合は作成されマウントされる)
`--mount type=volume,src=<ボリューム名>,dst=<コンテナ上のディレクトリ>`

```console:volumeでマウント
[vagrant@handson ~]$ docker container run -itd --name testvol01 -h testvol01 \
                       --mount type=volume,src=vol01,dst=/ctdir01 centos:7 /bin/bash

[vagrant@handson ~]$ docker volume ls    # ボリュームを確認
DRIVER              VOLUME NAME
local               vol01

[vagrant@handson ~]$ docker container exec -it testvol01 \
                       bash -c "ls | grep ctdir01"
ctdir01
[vagrant@handson ~]$ docker container exec -it testvol01 \
                       bash -c "echo 'File in container volume' > /ctdir01/testvolfile01.txt"
[vagrant@handson ~]$ docker container exec -it testvol01 \
                       cat /ctdir01/testvolfile01.txt
File in container volume

[vagrant@handson ~]$ sudo ls /var/lib/docker/volumes/
metadata.db  vol01
[vagrant@handson ~]$ sudo ls /var/lib/docker/volumes/vol01/_data
testvolfile01.txt
[vagrant@handson ~]$ sudo cat /var/lib/docker/volumes/vol01/_data/testvolfile01.txt
File in container volume
```

readonlyを付けることで読み込み権限でマウントすることもできます。
また、作成されたボリュームは別のコンテナでもマウントすることができます。

```console:volumeでマウント(readonly)
[vagrant@handson ~]$ docker container run -itd --name testvol02 -h testvol02 \
                       --mount type=volume,src=vol01,dst=/ctdir02,readonly centos:7 /bin/bash
[vagrant@handson ~]$ docker container exec -it testvol02 \
                       cat /ctdir02/testvolfile01.txt    # testvol01で作成したファイルが参照できる
File in container volume

[vagrant@handson ~]$ docker container exec testvol02 rm -f /ctdir02/testvolfile01.txt
rm: cannot remove '/ctdir02/testvolfile01.txt': Read-only file system

[vagrant@handson ~]$ docker container exec -it testvol02 \
                       bash -c "echo 'File2 in container volume' > /ctdir02/testvolfile02.txt"
bash: /ctdir02/testvolfile02.txt: Read-only file system

[vagrant@handson ~]$ docker container inspect testvol01 |grep -A10 -e Mounts
[vagrant@handson ~]$ docker container inspect testvol02 |grep -A10 -e Mounts

[vagrant@handson ~]$ docker container ls -a

[vagrant@handson ~]$ docker container rm -f testvol01 testvol02
testvol01
testvol02

[vagrant@handson ~]$ sudo cat /var/lib/docker/volumes/vol01/_data/testvolfile01.txt    # コンテナを消してもボリュームは消えない
File in container volume
```

`--volumes-from`オプションでもコンテナのボリュームを共有することができます。
コンテナ内のマウント先は、共有するボリュームのコンテナと同じ場所です。(今回の場合は`/ctdir01`)

```console:コンテナのボリュームを共有
[vagrant@handson ~]$ docker container run -itd --name testvol01 -h testvol01 \
                       --mount type=volume,src=vol01,dst=/ctdir01 centos:7 /bin/bash

[vagrant@handson ~]$ docker container run -itd --name testvol03 -h testvol03 --volumes-from testvol01 centos:7 /bin/bash
[vagrant@handson ~]$ docker container exec -it testvol03 \
                       bash -c "echo 'File in container volume. use volumes-from.' > /ctdir01/testvolfile03.txt"
[vagrant@handson ~]$ docker container exec -it testvol03 cat /ctdir01/testvolfile03.txt
File in container volume. use volumes-from.

[vagrant@handson ~]$ docker container exec -it testvol01 cat /ctdir01/testvolfile03.txt
File in container volume. use volumes-from.

[vagrant@handson ~]$ docker container rm -f testvol01 testvol03
testvol01
testvol03
```

作成したボリュームは以下のコマンドで削除できます。

```console:ボリューム削除
[vagrant@handson ~]$ docker volume ls
[vagrant@handson ~]$ docker volume rm vol01
vol01
```

## tmpfs
ホストOSのメモリの一部をファイルシステムとしてコンテナに提供することができます。
インメモリのファイルシステム(tmpfs)を提供できるため、非常に高速なファイルシステムを実現できます。
ただし、メモリ上にあるデータは、一時的な利用に限ります。(消えてしまう？)
コンテナ起動時に以下のオプションを指定することでマウントできます。
`--mount type=tmpfs,dst=<コンテナ上のディレクトリ>`

```console:tmpfsでマウント
[vagrant@handson ~]$ docker container run -it --rm --name testtmpfs01 -h testtmpfs01 \
                       --mount type=tmpfs,dst=/ctdir01 centos:7
[root@testtmpfs01 /]#
[root@testtmpfs01 /]# ls -l | grep ctdir01    # tmpfs-modeオプションを指定しない場合、1777(world-writable)になる。
drwxrwxrwt.   2 root root    40 Jul 20 07:13 ctdir01

[root@testtmpfs01 /]# exit
```

`tmpfs-mode`でフォルダの権限を変更できます。
`tmpfs-mode`以外にサイズを制限できるオプション(`tmpfs-size`)もあったりします。(今回は紹介のみ)

```console:tmpfs-modeでフォルダ権限変更
[vagrant@handson ~]$ docker container run --rm -it --name testtmpfs02 -h testtmpfs02 \
                       --mount type=tmpfs,dst=/ctdir01,tmpfs-mode=1755 centos:7
[root@testtmpfs02 /]#
[root@testtmpfs02 /]# ls -l | grep ctdir01
drwxr-xr-t.   2 root root    40 Jul 20 08:06 ctdir01

[root@testtmpfs01 /]# exit
```

メモ：
drwxrwxrwtはスティッキービットがついています。(実行権部分の「t」)
ファイルやディレクトリを書き込めますが、所有者だけ（rootは除く）しか削除できません。
参考：[スティッキービット（Sticky Bit） - 特殊なアクセス権](https://kazmax.zpp.jp/linux_beginner/stickybit.html)


## マウント補足
上記3つのマウント方法の説明では`--mount`を使用しましたが、`-v (--volume)`や`--tmpfs`でもマウントすることができます。
明示的であることやサポートしているオプションの違いなどから、ドキュメントでは`--mount`の使用が推奨されているようです。


# ハンズオン3：Dockerfile
コンテナから新しいイメージが作成できることをハンズオン1で試しました。
ベースイメージからファイルやコマンドを追加した新しいイメージが作成できることが確認できたと思います。
Dockerfileを使用すると、それら新しいイメージ作成作業を自動化することができます。
イメージ作成には`docker image build`コマンドを使用します。
参考：[docker image build](https://docs.docker.com/engine/reference/commandline/build/)　([日本語](http://docs.docker.jp/engine/reference/commandline/build.html))

ハンズオン3：Dockerfileでは、
Dockerfileを使ってDockerイメージをいくつか作成してみます。

※以降で作成するファイルは**「文字コード：UTF-8、改行コード：LF」**で作成してください。


## Dockerfile01：簡単な例
まずは、ハンズオン1で作成したイメージを、Dockerfileを使用して作成してみたいと思います。
ハンズオン1では`centoos:7`のイメージをベースイメージとして、`test01.txt`の作成と`iproute`のインストールをしていました。

### Dockerfileの作成
Dockerfileを以下のように作成します。

```console:フォルダ構成
install_iproute
　└ Dockerfile01
```

```dockerfile:Dockerfile01
FROM centos:7
WORKDIR /home
RUN echo "Dockerfile test01" > /home/test01.txt \
    && yum install -y iproute \
    && rm -rf /var/cache/yum/* \
    && yum clean all
```

Dockerfileでは様々なDockerfile命令を使用してイメージを作成しており、今回は以下の命令を使用しています。

**[FROM](https://docs.docker.com/engine/reference/builder/#from)：**
　ベースイメージを指定します。
　使用するイメージについて[ドキュメント](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/)では[Alpine image](https://hub.docker.com/_/alpine/)を推奨しているようです。
　ここでは、ハンズオン1と合わせるために`centos:7`を使用しています。
**[WORKDIR](https://docs.docker.com/engine/reference/builder/#workdir)：**
　Dockerfileで記述される命令の作業ディレクトリを設定します。 
　指定したディレクトリが存在しない場合は作成されます。
　Dockerfileで複数回使用でき、相対パスが指定されている場合は前のWORKDIR命令のパスに相対します。 
**[RUN](https://docs.docker.com/engine/reference/builder/#run)：**
　ベースイメージに対してコマンドを実行します。
　Dockerではレイヤという概念があり、RUN命令を複数実行するとレイヤが増えます。
　レイヤの増加はDockerイメージサイズの肥大化につながるため、
　`&&`(1つ目のコマンドが正常終了した場合に2つ目のコマンドを実行する)や`\`(改行)を使用して一つのRUN命令で実行しています。
　`rm`や`yum clean`はDockerイメージの容量を減らすため、不要ファイルなどを削除しています。

### Dockerfileからイメージのビルド。コンテナ起動。

```console:ビルド、コンテナ起動確認
[vagrant@handson install_iproute]$ docker image build --no-cache -f ./Dockerfile01 -t centos:df01 .
[vagrant@handson install_iproute]$ docker image ls
[vagrant@handson install_iproute]$ docker container run -it --rm --name dftest01 -h dftest01 centos:df01 /bin/bash
[root@dftest01 home]# ip a
[root@dftest01 home]# exit
```

####ビルドに使用したコマンドについて補足
ビルドコマンドは`docker build [OPTIONS] PATH`の形式で実行します。(PATHの部分にはURLが入る場合もあります)
`--no-cache`：ビルドにキャッシュを使わない。
`-f <Dockerfile名>`：使用するDockerfile名を指定します。指定がない場合は`PATH/Dockerfile`が使用されます。
`-t <リポジトリ>:<タグ>`：ビルドして作成されるイメージに付与するリポジトリとタグです。
PATH：「ビルドで使用するファイルが保存されている場所」を指定します。

## Dockerfile02：データベースコンテナ作成
Dockerfileを使用してテスト用のデータベースを作成してみます。
コンテナを起動するとテスト用のデータベース、テーブルが作成された状態にしたいと思います。
また、設定ファイルも配置して日本語に対応した状態にします。
※データベースにそこまで詳しくないため、こういうこともできるという参考程度に。。

### Dockerfileとその他ファイル作成
Dockerfileとその他使用するファイルを以下のように作成します。

```console:フォルダ構成
mariadb
　└ sql
　　　└ 01_create_table_goods.sql
　　　└ 02_insert_into_goods.sql
　└ Dockerfile02
　└ server.cnf
　└ envfile.txt
```

```dockerfile:Dockerfile02
FROM mariadb:10.4
ENV LANG=C.UTF-8
COPY server.cnf /etc/mysql/mariadb.conf.d
COPY sql/* /docker-entrypoint-initdb.d/
```

```console:server.cnf
[mysqld]
character-set-server=utf8mb4

[client]
default-character-set=utf8mb4
```

```sql:sql/01_create_table_goods.sql
create table test_db.goods
(
  goods_id bigint not null auto_increment,
  name varchar( 255 ) not null,
  category varchar( 255 ),
  memo varchar( 255 ),
  index (goods_id),
  primary key (goods_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

```sql:sql/02_insert_into_goods.sql
insert into test_db.goods (name, category) values ("PC0001", "PC");
insert into test_db.goods (name, category) values ("PC0002", "PC");
insert into test_db.goods (name, category) values ("MB0001", "MobilePhone");
insert into test_db.goods (name, category) values ("MB0002", "MobilePhone");
```

```console:envfile.txt
MYSQL_ROOT_PASSWORD=root
MYSQL_DATABASE=test_db
MYSQL_USER=master
MYSQL_PASSWORD=master
```

ベースイメージとしてDocker Official Imagesの[mariadb](https://hub.docker.com/_/mariadb)を使用しています。
既存のイメージを使用できるのはDockerのメリットです。
ただし、[ドキュメント](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#from)では可能な限りOfficial Imagesを使用することが推奨されています。

Dockerfile02で新たに使用したDockerfile命令はENV命令とCOPY命令です。

**[ENV](https://docs.docker.com/engine/reference/builder/#env)：**
　環境変数を設定します。ここではLANGを設定しています。
　設定した環境変数はコンテナ起動時に`--env`オプションで変更することもできます。

**[COPY](https://docs.docker.com/engine/reference/builder/#copy)：**
　ファイルまたはディレクトリをコピーし、ファイルシステムに追加します。
　使用したmariadbのイメージでは、指定のディレクトリにsqlファイルを配置することで、
　コンテナ起動時に実行することができるので、`COPY`命令を使用してsqlファイルを配置しています。

`COPY`命令と似た命令に[ADD](https://docs.docker.com/engine/reference/builder/#add)命令があります。
`ADD`命令の場合、圧縮ファイルを解凍しつつコピーできるなど動作に違いがあります。

ベストプラクティスとして、コピーするだけであればシンプルな`COPY`命令の使用が[ドキュメント](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#add-or-copy)では推奨されています。

### Dockerfileからイメージのビルド。コンテナ起動

```console:ビルド、コンテナ起動確認。
[vagrant@handson mariadb]$ docker image build --no-cache -f ./Dockerfile02 -t mariadb:db01 .
[vagrant@handson mariadb]$ docker image ls
[vagrant@handson mariadb]$ docker container run --name mariadb01 -h mariadb01 \
                             -p 3306:3306 \
                             --mount type=volume,src=mysql,dst=/var/lib/mysql \
                             -e MYSQL_ROOT_PASSWORD=root \
                             -e MYSQL_DATABASE=test_db \
                             -e MYSQL_USER=master \
                             -e MYSQL_PASSWORD=master \
                             -d mariadb:db01

[vagrant@handson mariadb]$ docker container ls
[vagrant@handson mariadb]$ docker volume ls

[vagrant@handson mariadb]$ docker container exec -it mariadb01 /bin/bash
root@mariadb01:/# mysql -u master -p
Enter password: <MYSQL_PASSWORD で指定した値を入力>


MariaDB [(none)]> show variables like "chara%";

MariaDB [(none)]> show databases;
MariaDB [(none)]> use test_db
MariaDB [test_db]> show tables;

MariaDB [test_db]> select * from goods;

MariaDB [test_db]> exit
root@mariadb01:/# exit
```

mariadbイメージでは環境変数を指定することで、コンテナ起動時にデータベースを作成したりすることができます。
環境変数は`--env`または`-e`オプションで指定できます。今回は以下の環境変数を指定します。

**MYSQL_ROOT_PASSWORD**
　必須の変数です。MariaDBのrootユーザーに設定されるパスワードを指定します。

**MYSQL_DATABASE**
　イメージの起動時に作成されるデータベースの名前を指定できます。

**MYSQL_USER、MYSQL_PASSWORD**
　新しいユーザーを作成し、そのユーザーのパスワードを設定します。
　MYSQL_DATABASEで指定されたデータベースのスーパーユーザー権限が付与されます。
　ユーザーを作成するには、両方の変数が必要です。

マウント機能(--mountオプション)を使用してコンテナの外でデータディレクトリを管理しています。
コンテナで作成したファイルなどはコンテナが削除された際に一緒に削除されてしまいます。
mariadbイメージでは--mountオプションを指定しない場合、デフォルトでランダムな文字列でボリューム作成されるようです。

`-p <ホストOS側ポート>:<Docker側ポート>`：ポートを紐づけています。

上記では環境変数を一つずつ設定していますが、`--env-file`オプションを使用するとファイルで指定することができます。

```console:--env-fileを使った場合のコンテナ起動確認
上記で作成したコンテナ、ボリュームを削除します。
[vagrant@handson mariadb]$ docker container rm -f mariadb01
[vagrant@handson mariadb]$ docker volume rm mysql

[vagrant@handson mariadb]$ docker container run --name mariadb01 -h mariadb01 \
                             -p 3306:3306 \
                             --mount type=volume,src=mysql,dst=/var/lib/mysql \
                             --env-file ./envfile.txt \
                             -d mariadb:db01

[vagrant@handson mariadb]$ docker container ls
[vagrant@handson mariadb]$ docker volume ls

[vagrant@handson mariadb]$ docker container exec -it mariadb01 /bin/bash
root@mariadb01:/# mysql -u master -p
Enter password: <MYSQL_PASSWORD で指定した値を入力>

MariaDB [(none)]> show variables like "chara%";

MariaDB [(none)]> show databases;
MariaDB [(none)]> use test_db
MariaDB [test_db]> show tables;

MariaDB [test_db]> select * from goods;

MariaDB [test_db]> exit
root@mariadb01:/# exit
```


## Dockerfile03：Dockerfile02で作成したデータベースに接続するWebサーバー
ここではDockerfile02で作成したデータベースに接続するWEBサーバーを作成してみたいと思います。
今回はDockerの開発言語でもあるGolangで、DBにアクセスして表示するだけの簡単なWEBサーバーを作成してみます。
※Golangも入門中のため、コードは参考程度でよろしくお願いします。

### Dockerfileとその他ファイル作成
Dockerfileとその他の使用するファイルを以下のように作成します。

```console:フォルダ構成
simple_server
　└ goapp
　　　└ server.go
　　　└ tmpl01.html
　└ Dockerfile03
```

```dockerfile:Dockerfile03
FROM golang:1.13.5
COPY goapp /go/src/goapp
WORKDIR /go/src/goapp
RUN go get -u github.com/go-sql-driver/mysql
ENTRYPOINT ["go","run","server.go"]
```

```golang:server.go
package main

import (
	"database/sql"
	"fmt"
	_ "github.com/go-sql-driver/mysql"
	"html/template"
	"net/http"
)

type Goods struct {
	goodsId  int    `db:goods_id`
	name     string `db:name`
	category string `db:category`
	memo     string `db:memo`
}

func main() {
	fmt.Println("Start Sample Server")
	server := http.Server{
		Addr: ":8080",
	}
	http.HandleFunc("/goods", goods)
	server.ListenAndServe()
}

func goods(w http.ResponseWriter, r *http.Request) {
	goodsSelectQuery := "SELECT * FROM goods;"
	goodsRows := selectQuery(goodsSelectQuery)
	var goodsSlice []Goods
	for goodsRows.Next() {
		var goods Goods
		goodsRows.Scan(&goods.goodsId, &goods.name, &goods.category, &goods.memo)
		goodsSlice = append(goodsSlice, goods)
	}
	fmt.Println(goodsSlice)

	t, err := template.ParseFiles("tmpl01.html")
	if err != nil {
		panic(err)
	}

	err = t.Execute(w, goodsSlice)
	if err != nil {
		panic(err)
	}
}

func selectQuery(query string) *sql.Rows {
	// DB接続
	db, err := sql.Open("mysql", "master:master@tcp(192.168.10.10:3306)/test_db?charset=utf8mb4")
	if err != nil {
		panic(err.Error())
	}
	defer db.Close()

	// ステートメント作成
	stmt, err := db.Prepare(query)
	if err != nil {
		panic(err.Error())
	}
	defer stmt.Close()

	// クエリの実行
	rows, err := stmt.Query()
	if err != nil {
		panic(err.Error())
	}
	return rows
}
```

```html:tmpl01.html
<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>sample server</title>
  </head>
  <body>
    <ul>
    {{ range . }}
      <li>{{ . }}</li>
    {{ else }}
      データなし
    {{ end}}
    </ul>
  </body>
</html>
```


ベースイメージとしてDocker Official Imagesの[golang](https://hub.docker.com/_/golang)を使用しています。
Dockerfile03で新たに使用したDockerfile命令はENTRYPOINT命令のみです。

**[ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint)：**
　コンテナを起動した際に、実行されるコマンドを設定します。
　今回はGolangで作成したWebサーバーを起動するコマンドを設定しています。

ENTRYPOINT命令と似た命令に[CMD](https://docs.docker.com/engine/reference/builder/#cmd)命令があります。
詳細について以下の記事が分かりやすかったので、ここでは紹介のみで詳細は省きたいと思います。
[[docker] CMD とENTRYPOINT の違いを試してみた](https://qiita.com/hihihiroro/items/d7ceaadc9340a4dbeb8f)


### Dockerfileからイメージのビルド。コンテナ起動。

```console:ビルド、コンテナ起動確認。
[vagrant@handson simple_server]$ docker image build --no-cache -f ./Dockerfile03 -t golang:ss01 .
[vagrant@handson simple_server]$ docker image ls
[vagrant@handson simple_server]$ docker container run -d \
                                    --name ss01 -h ss01 \
                                    -p 8080:8080 \
                                    golang:ss01

[vagrant@handson simple_server]$ docker container ls
[vagrant@handson simple_server]$ curl localhost:8080/goods
```

以下のURLにホストPCのブラウザからアクセスして表示を確認できます。
(IPアドレスはVagrantfileで指定した仮想環境のアドレス)
http://192.168.10.10:8080/goods


# ハンズオン4：マルチステージビルド
イメージのサイズは小さい方が良いとされています。
[AWS](https://aws.amazon.com/jp/ecr/pricing/)、[GCP](https://cloud.google.com/container-registry/pricing)、[Azure](https://azure.microsoft.com/ja-jp/pricing/details/container-registry/)などが提供するリポジトリはイメージ容量が課金対象になるため、容量削減がコスト削減にもつながります。
[マルチステージビルド](https://docs.docker.com/develop/develop-images/multistage-build/)を使うと、必要なファイルのみイメージに含めることが可能になるため容量削減に便利です。

Dockerfile03では`go run`コマンドでソースコードをビルドせずに実行していました。
Golangはコンパイル言語のため、ビルド後はソースファイルなどが不要になり容量削減が可能です。
Dockerfile03の例を変更して、ビルドしたファイルのみを含めむDockerイメージを作成してみたいと思います。

また、今回は容量がとても小さな[Alpine image](https://hub.docker.com/_/alpine/)で作成してみます。

## Dockerfileとその他ファイル作成
Dockerfile03のフォルダに今回使用するDockerfile03bを作成しています。
その他のファイルは同じファイルを使用します。

```console:フォルダ構成
simple_server
　└ goapp
　　　└ server.go
　　　└ tmpl01.html
　└ Dockerfile03　　　← 今回は使用しない
　└ Dockerfile03b　　 ← 追加
```

```dockerfile:Dockerfile03b
FROM golang:1.13.5-alpine AS gobuild01
COPY goapp /go/src/goapp
WORKDIR /go/src/goapp
RUN apk update &&\
  apk upgrade &&\
  apk add --no-cache git &&\
  go get -u github.com/go-sql-driver/mysql &&\
  go build server.go

FROM alpine:3.10.3
COPY --from=gobuild01 /go/src/goapp/server /goapp/
COPY --from=gobuild01 /go/src/goapp/tmpl01.html /goapp/
WORKDIR /goapp
ENTRYPOINT ["./server"]
```

マルチステージビルドではステージを分けてDockerイメージを作成します。
FROM命令を複数記述しそれぞれがステージになります。異なるベースイメージも使用できます。

今回は2つのステージに分けています。
第1ステージがGolangコードのビルド、第2ステージが最終的なDockerイメージ作成です。
第1ステージでビルドで作成したファイルなどを、第2ステージで作成するDockerイメージにコピーすることで、
必要なファイルのみが含まれるDockerイメージを作成しています。

別ステージからのコピーはCOPY命令を使用します。
`COPY --from=<コピー元別名(またはステージ番号)> <コピー元ステージのファイル、フォルダ> <コピー先ステージのファイル、フォルダ>`

第1ステージのFROM命令では`AS`を使って別名を付けています。
別名を付けなかった場合は、Dockerfile内の各ステージが上から順に 0 から始まる整数番号になります。
別名を付けることでDockerfile内のステージの順番を後で並べ替えてもCOPYが壊れずにすみます。

## Dockerfileからイメージのビルド。コンテナ起動。

```console:ビルド、コンテナ起動確認。
[vagrant@handson simple_server]$ docker image build --force-rm --no-cache -f ./Dockerfile03b -t golang:ss02 .
[vagrant@handson simple_server]$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
golang              ss02                dc35acf77661        18 minutes ago      16.1MB
<none>              <none>              ad6f9e36afc6        18 minutes ago      390MB
golang              ss01                f24356734000        2 hours ago         806MB

[vagrant@handson simple_server]$ docker container run \
                                    -d --name ss02 -h ss02 \
                                    -p 8081:8080 \
                                    golang:ss02

[vagrant@handson simple_server]$ docker container ls
[vagrant@handson simple_server]$ curl localhost:8081/goods
```

サイズが16.1MBの小さなイメージが作成できました。

以下のURLにホストPCのブラウザからアクセスして表示を確認できます。(IPアドレスはVagrantfileで指定した仮想環境のアドレス)
http://192.168.10.10:8081/goods


##マルチステージビルド補足
###補足1：ごみイメージの削除
マルチステージビルドの場合、最終的に作成するDockerイメージ以外が`<none>`という名前で残ってしまいます。
以下のように[docker image prune](https://docs.docker.com/engine/reference/commandline/image_prune/)コマンドで簡単に削除できます。

```console:イメージ削除
$ docker image ls --filter dangling=true    # 対象の表示
$ docker image prune    # 削除
```

###補足2：特定のビルドステージで止める
`docker image build`で`--target`オプションを指定することで、特定のステージで止めることができます。

```console:Dockerfile03bのgobuild01ステージで止める
[vagrant@handson simple_server]$ docker image build --target gobuild01 \
                                   --force-rm --no-cache -f ./Dockerfile03b -t golang:ssb02 .
[vagrant@handson simple_server]$ docker container run \
                                   -it \
                                   --rm \
                                   --name ssb02 -h ssb02 \
                                   -p 8082:8080 \
                                   golang:ssb02 \
                                   /bin/sh
/go/src/goapp # ls
server       server.go    tmpl01.html
```

※alpineイメージでは`/bin/bash`が入っていないので`/bin/sh`または`/bin/ash`でログインしています。

###補足3：外部イメージをステージとして使用
Dockerfileで記述したステージ以外にも、既に作成済みのイメージがステージとして使用できます。
使用方法は`COPY --from=`でローカルまたはDockerレジストリのDockerイメージを指定するだけです。
※今回は紹介のみ。

```docker:外部イメージをステージとして使用する場合のCOPY命令の例
COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf
```

###補足4：apkコマンド
Alpineではyum、aptではなくapkコマンドを使用してパッケージをインストールします。
コマンドをいくつか紹介します。

```console:apkコマンド一覧
$ apk --version    # apkコマンドのバージョン表示。
$ apk --help       # apkコマンドのヘルプ表示。
$ apk update       # 利用可能なパッケージのインデックス(パッケージ一覧)を更新。
$ apk upgrade      # インストール済みパッケージの更新。
$ apk search       # パッケージの検索。
$ apk add          # パッケージのインストール。--no-cacheでキャッシュを使用しない。
$ apk del          # パッケージのアンインストール。
$ apk info         # インストール済みパッケージ一覧表示。パッケージ名指定でパッケージの詳細表示。
```
参考：[Alpine Linux package management](https://wiki.alpinelinux.org/wiki/Alpine_Linux_package_management)

#おわり
Dockerではコンテナで動かす対象の理解も必要になるので学ぶことが多いですね。
まだまだ学習中の内容も多く触れられなかった内容も多々ありますが、今回は以上です。


# 参考
[Docker実践ガイド 第2版 (impress top gear) 古賀政純](https://www.amazon.co.jp/dp/4295005525)
[コンテナはなぜ安全（または安全でない）なのか](https://sil.hatenablog.com/entry/why-container-is-secure)
[Dockerでプログラマが最低限知るべきことが、最速でわかるチュートリアル](https://qiita.com/woz/items/43462398765f8036f3a8)
[VM・Dockerといった仮想化技術を理解する](https://qiita.com/r-tominaga/items/8ac588d603802572185f)
[DockerHubのイメージのタグ一覧をコマンドで取得する](https://www.mazn.net/blog/2015/12/26/1548.html)
[「それコンテナにする意味あんの？」迷える子羊に捧げるコンテナ環境徹底比較 #cmdevio2019](https://dev.classmethod.jp/cloud/aws/cmdevio2019-container/)
[docker container / image コマンド新旧比較](https://qiita.com/zembutsu/items/6e1ad18f0d548ce6c266)
[Wikipedia - ハイパーバイザ](https://ja.wikipedia.org/wiki/%E3%83%8F%E3%82%A4%E3%83%91%E3%83%BC%E3%83%90%E3%82%A4%E3%82%B6)
[Docker Documentation](https://docs.docker.com/)
[Docker Hub](https://hub.docker.com/)
[Docker Commandline](https://docs.docker.com/engine/reference/commandline/docker/)
[Best practices for writing Dockerfiles](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/)
[Dockerfile を書くベスト・プラクティス](http://docs.docker.jp/engine/userguide/eng-image/dockerfile_best-practice.html)
[Alpine Linux入門 -内部構造とapkでパッケージインストール編-](https://blog.stormcat.io/post/entry/alpine-entry-apk/)
[Docker内部で利用されているLinuxカーネルの機能 (namespace/cgroups)](https://qiita.com/wellflat/items/7d62f2a63e9fcddb31cc)
[いまさらだけどDockerに入門したので分かりやすくまとめてみた](https://qiita.com/gold-kou/items/44860fbda1a34a001fc1)
[MySQLの文字コード事情](https://www.slideshare.net/tmtm/mysql-62004569)