Docker を使う（docker build でオリジナルのイメージを作成する）

[前回の記事](https://qiita.com/landwarrior/items/fd918da9ebae20486b81) の続きです。
今回は、 `docker build` を使っていきたいと思います。
とは言っても、一から説明するととても大変なので、事例を紹介しつつ、かいつまんで説明しようと思います。

# 何ができるのか？

要するに、オリジナルの docker イメージを作ります。
[Docker Hub](https://hub.docker.com/) では、たくさんのイメージが公開されています。
当然それらを使うのですが、公開されている docker イメージをそのまま使っても、自分が書いたソースコードは docker イメージ内にないのでそのままでは動かせないわけです。

そこで、自分のソースコードをあらかじめ埋め込んだイメージを作成し、そのイメージでコンテナを起動することによって、高速に、何度でも同じ環境を構築できる、ということですね。

# docker build コマンド

ここでようやく、 `docker build` コマンドが出てきます。
ただ、 `docker build` をするためには Dockerfile というファイルが必要になります。
この Dockerfile 内に、作成するイメージをどう構築するかを記述します。

# 今回のテーマ

今回は、 CentOS のイメージに httpd をインストールして Web サーバーとして起動したいと思います。
本当はそんなことしないとは思いますが・・・
いいサンプルが思いつかず、こうなりました。

で、今回用意するのは以下の3つです。

- Dockerfile
- entrypoint.sh
- index.html

順を追って説明します。

## Dockerfile

`docker build` を使う際に必須のものです。
ファイル名はこれじゃなくてもいいのですが、デフォルトがこの名称なのでこのままいきます。
詳しい説明は自分もあまり理解していないためできないので、参考書を購入し勉強しましょう・・・

今回使用する Dockerfile の中身はこちら。

```bash

# ベースイメージの指定
FROM centos:centos7.5.1804
# ディレクトリ作成を実行
RUN mkdir /var/html > /dev/null 2>&1
# httpd のインストール
RUN yum install -y httpd
# ファイルのコピー
COPY index.html /var/html
COPY entrypoint.sh /usr/bin
# docker のネットワーク上でコンテナのポートを開放
EXPOSE 8000
# docker run 時に絶対に実行されるコマンドを定義
ENTRYPOINT ["bin/bash", "/usr/bin/entrypoint.sh"]
# docker run 時の引数でコマンドを省略した時に実行されるコマンドを定義
CMD ["/usr/sbin/httpd", "-D", "FOREGROUND"]
```

なるべく色んなものを詰め込んでみました。
分かる範囲で説明します。

#### FROM

まあ、書いてある通りです。
Docker Hubで公開されているイメージを基にしています。
常に最新のものを使用する場合は
`FROM centos:latest` と書きますが、そんなことをする人はいないと思います。
ベースのバージョンが変わるとその後に実行するものが動かなくなる可能性がありますからね。

#### RUN

ビルド時に実行されるものです。
ここが間違っていたりするとイメージは作成できません。
なお、上記の例では Shell 形式で記述しています。
この形式で記述すると、 /bin/sh で実行されるそうです。

#### COPY

ファイルをコンテナ内に配置する際に使用します。
上記の書き方だと、 `docker build` を実行したカレントディレクトリに entrypoint.sh と index.html が存在している必要があります。

#### EXPOSE

コンテナのポートを開放します。
このポートを介してコンテナと通信をします。
ここでは、 8000 番ポートを開けています。

#### ENTRYPOINT

イメージ作成後、実際に `docker run` でコンテナを起動する際に実行されるコマンドです。
上記の例では、 Exec 形式で記述しています。
この形式では、実行したいコマンドを JSON 配列で指定します。
また、シェルを介さずに直接実行されるそうです。
そのため上記では /bin/bash を用いて entrypoint.sh を実行するように記載しています。

#### CMD

イメージ作成後、実際に `docker run` でコンテナを起動する際に実行されるコマンドです。
ENTRYPOINT と異なる点は、こちらのコマンドは `docker run` 時に引数でコマンドを指定すると、 CMD で指定したコマンドは実行されない点です。

次は entrypoint.sh の説明です。

## entrypoint.sh

Dockerfile 内で呼び出して実行しているシェルですが、中身は以下のように書いています。

```bash
#!/bin/bash
set -e

conf () {
    echo "ServerRoot \"/etc/httpd\""
    echo "Listen 8000"
    echo "ServerName `hostname`"
    echo "Include conf.modules.d/*.conf"
    echo "User apache"
    echo "Group apache"
    echo "ServerAdmin root@localhost"
    echo "<Directory />"
    echo "    AllowOverride none"
    echo "    Require all denied"
    echo "</Directory>"
    echo "DocumentRoot \"/var/html\""
    echo "<Directory \"/var/www\">"
    echo "    AllowOverride None"
    echo "    Require all granted"
    echo "</Directory>"
    echo "<Directory \"/var/html\">"
    echo "    Options Indexes FollowSymLinks"
    echo "    AllowOverride None"
    echo "    Require all granted"
    echo "</Directory>"
    echo "<IfModule dir_module>"
    echo "    DirectoryIndex index.html"
    echo "</IfModule>"
    echo "<Files \".ht*\">"
    echo "    Require all denied"
    echo "</Files>"
    echo "ErrorLog \"logs/error_log\""
    echo "LogLevel warn"
    echo "<IfModule log_config_module>"
    echo "    LogFormat \"%h %l %u %t \\\"%r\\\" %>s %b \\\"%{Referer}i\\\" \\\"%{User-Agent}i\\\"\" combined"
    echo "    LogFormat \"%h %l %u %t \\\"%r\\\" %>s %b\" common"
    echo "    <IfModule logio_module>"
    echo "      LogFormat \"%h %l %u %t \\\"%r\\\" %>s %b \\\"%{Referer}i\\\" \\\"%{User-Agent}i\\\" %I %O\" combinedio"
    echo "    </IfModule>"
    echo "    CustomLog \"logs/access_log\" combined"
    echo "</IfModule>"
    echo "<IfModule alias_module>"
    echo "    ScriptAlias /cgi-bin/ \"/var/www/cgi-bin/\""
    echo "</IfModule>"
    echo "<Directory \"/var/www/cgi-bin\">"
    echo "    AllowOverride None"
    echo "    Options None"
    echo "    Require all granted"
    echo "</Directory>"
    echo "<IfModule mime_module>"
    echo "    TypesConfig /etc/mime.types"
    echo "    AddType application/x-compress .Z"
    echo "    AddType application/x-gzip .gz .tgz"
    echo "    AddType text/html .shtml"
    echo "    AddOutputFilter INCLUDES .shtml"
    echo "</IfModule>"
    echo "AddDefaultCharset UTF-8"
    echo "<IfModule mime_magic_module>"
    echo "    MIMEMagicFile conf/magic"
    echo "</IfModule>"
    echo "EnableSendfile on"
    echo "IncludeOptional conf.d/*.conf"
}
# 既存のコンフィグを書き換え
conf > /etc/httpd/conf/httpd.conf

exec "$@"
```

別にシェルで書く必要はないのですが・・・
httpd.conf を書き換えるためのシェルです。
シェルでやることのメリットとしては、内部で `hostname` のコマンド結果を使って動的に ServerName を変更できることですかね・・・
もっといいシチュエーションがあると思いますが、今回はこんなこともできるよ、という紹介です。
`Listen` だけ、 8000 番ポートを指定するように変更しています。
これは、コンテナで開放するポートを 8000 番と指定しているので、合わせてあります。

また、最後に `exec "$@"` と記載していますがこれがミソで、実際にビルドしたイメージを使って `docker run` を実行した際、 Dockerfile に記載した通りだとすると以下のようなコマンドでコンテナが起動します。

```
bin/bash /usr/bin/entrypoint.sh /usr/sbin/httpd -D FOREGROUND
```
そう、 ENTRYPOINT と CMD はどちらも実行されるので、横並びに実行されてしまいます。
そのため、 entrypoint.sh の引数として `/usr/sbin/httpd -D FOREGROUND` を受け取り、これをシェルの中で実行するような形としています。
他にもやりようはあるのかもしれませんが、なぜかこのように書いています。
一般的ではないのかも。。。
あくまで ENTRYPOINT を使う例として無理矢理シェルを実行しているための処置だと思ってください。

## index.html

こちらはただ自前で用意しただけの HTML です。
entrypoint.sh で DocumentRoot を変更しているので、そこに合わせて配置しているだけにすぎません。
今回の例では、中身は以下のようにしてみました。

```html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
</head>
<body>
    <h1>こんにちは、世界！</h1>
</body>
</html>
```

# ビルドしてみる

ではでは、3つのファイルを用意したので実際にビルドしてみましょう。
`/var/docker/centos` 配下にファイルを配置しているものとして説明します。
こんな感じ。

```
[root@localhost centos]# pwd
/var/docker/centos
[root@localhost centos]# ls
Dockerfile  entrypoint.sh  index.html
```

コマンドを実行します。

```
[root@localhost centos]# docker build -t testcent .
```

`-t` は、名前とタグを決めます。
書式は `name:tag` となります。今回は `testcent` という名前にしました。
タグは指定していないので、 `latest` に勝手になります。
最後のドット `.` は、カレントディレクトリにある Dockerfile を使うときにこうなります。
Dockerfile の場所が異なる場合は、そのディレクトリを指定します。
なお、ファイル名が Dockerfile ではない場合は、 `-f` オプションを使って指定すればよいはずです。
（試してはいないです）

上手くできれば、最後に `Successfully` うんちゃらと出ているはず。
ここで、 `docker images` を実行してみます。

```
[root@localhost centos]# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
testcent            latest              f7e4a3bceea6        3 minutes ago       326MB
centos              centos7.5.1804      cf49811e3cdb        2 months ago        200MB
```
ベースとなった centos の docker イメージと、自分でビルドした testcent があることが分かります。

# ビルドしたイメージでコンテナを起動してみる

以下のようにコマンドを実行します。

```
[root@localhost centos]# docker run -d -p 80:8000 testcent
```
`-d` オプションは、バックグランドでコンテナを実行し続けます。
`-p {ホストのポート}:{コンテナのポート}` と指定することで、ホストのポートへのアクセスをコンテナのポートに飛ばすことができます。
Dockerfile でコンテナに 8000 番ポートをあけるように記載しているので、ホストの 80 番ポートをコンテナの 8000 番ポートに繋げました。

コンテナの状態を確認してみます。

```
[root@localhost centos]# docker ps --no-trunc
CONTAINER ID                                                       IMAGE               COMMAND                                                           CREATED             STATUS              PORTS                  NAMES
2570dda5f952162c34eb397ff8f934c23cb0ebf7b603dde166bad54150c6b22d   testcent            "bin/bash /usr/bin/entrypoint.sh /usr/sbin/httpd -D FOREGROUND"   4 minutes ago       Up 4 minutes        0.0.0.0:80->8000/tcp   cranky_blackwell
```

`--no-trunc` オプションで省略せずに表示させているため横長ですが・・・
Dockerfile で指定した `ENTRYPOINT` と `CMD` が実行されていますね。
この状態で、 `localhost:80` へアクセスしてみましょう。

```
[root@localhost centos]# curl http://localhost:80
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
</head>
<body>
    <h1>こんにちは、世界！</h1>
</body>
</html>
```
ホストの 80 番ポートはコンテナの 8000 番ポートへ飛び、8000 番ポートで httpd が Listen しているので、 index.html を表示することができました。

# 最後に

長い間下書き状態で放置していたので、端折ってしまっている部分があるかも・・・
また、 httpd が分からないと難しい部分もあったかもしれません。
正直なところ、自分で LAMP 環境を構築できる程度の知識は必要です。
ちゃんと学ぼうと思ったら、 Virtualbox に CentOS の ISO イメージファイルを使って一からインストールをしてみたり yum install で httpd を入れてみたり、 iptables やら SELinux やら、 systemctl やら firewalld やら、ifconfig やら ip addr show やら、よく使うような基本的なところは勉強しておきたいですね。
自分もそんなにやってませんが・・・(´･ω･｀)

次回も記事を書こうかと思いますが、今度は Docker Compose か Docker Swarm あたりにしようかと思います。
もう次からは超絶難しくなるので、 Docker Compose はすっ飛ばして Docker Swarm にするかも。
Kubernetes は・・・職場で使ってないので使ったことがなく・・・
Docker Swarm の記事が書けたら、 Kubernetes を勉強してみたいなぁと思ってます。