Docker for WindowsでDockerを学ぶ (バージョンCE 17.06.2)

# 概要

Dockerの使い方を学習したまとめ記事です。内容は以下の4つのパートに分けています。

* その1: 基本的なコマンドの使い方
* その2: Dockerfileからイメージをビルド
* その3: 複数コンテナの連携
* その4: docker-composeの使い方

**環境**

* Windows 10 Professional
    * PowerShell 5.1
* Docker for Windows Community Edition Version 17.06.2-ce-win27

**参考**

* [Get started with Docker for Windows] (https://docs.docker.com/docker-for-windows/)
* [開発環境をDockerに乗せる方法とメリットを3ステップで学ぶチュートリアル] (https://qiita.com/KeitaMoromizato/items/ae1a57fc62b41b942d71)
* [[Docker] 初心者が知っておくと便利かもしれない15の知識] (https://qiita.com/enta0701/items/b872eef6d910908c0e6c)

# インストール

ダウンロードページよりWindows版のインストーラーをダウンロードして実行します。特に選択する項目はなくすぐに終了しました。インストール終了画面のメッセージ(Close and log out)にしたがってインストール終了後に一度ログインしなおします。

**インストール終了**

![d1.png](https://qiita-image-store.s3.amazonaws.com/0/22772/633dca5c-99dc-1869-97fb-291f7d8ada37.png)

**Welcome画面**

再ログイン後にWelcome画面が立ち上がります。

![d2.png](https://qiita-image-store.s3.amazonaws.com/0/22772/2dc77702-398a-f9d6-4c29-3d9dc83b79f8.png)

## インストール後の設定

**リソースの割り当て**

タスクトレイにあるDockerアイコンを右クリックしコンテキストメニューから"Settings..."を選択します。
左ペインのAdvancedを選択しDockerで使用できるリソースを割り当てます。

![d3.png](https://qiita-image-store.s3.amazonaws.com/0/22772/6bdb232b-cb50-abb9-87b1-c5b6bd67c931.png)

**共有ドライブの設定**

Shared Drivesからコンテナから利用できるドライブを設定します。今のところ(2017/10)ディレクトリ単位での設定はできないようです。ApplyボタンをクリックするとWindowsアカウントのパスワードを求められますので入力して設定が完了します。

![d4.png](https://qiita-image-store.s3.amazonaws.com/0/22772/47847542-a2b1-7744-d8ae-98c19ef39096.png)

## バージョンの確認

この記事ではdockerコマンドの実行にPowerShellを使用しています。PowerShellの起動は<kbd>Win + R</kbd>で「ファイル名を指定して実行」の欄に"powershell"と入力します。

**PowerShellのバージョンの確認**

```bat
PS> $PSVersionTable

Name                           Value
----                           -----
PSVersion                      5.1.15063.608
PSEdition                      Desktop
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
BuildVersion                   10.0.15063.608
CLRVersion                     4.0.30319.42000
WSManStackVersion              3.0
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
```

**dockerのバージョン確認**

```bat
PS> docker version
Client:
 Version:      17.06.2-ce
 API version:  1.30
 Go version:   go1.8.3
 Git commit:   cec0b72
 Built:        Tue Sep  5 19:57:19 2017
 OS/Arch:      windows/amd64

Server:
 Version:      17.06.2-ce
 API version:  1.30 (minimum version 1.12)
 Go version:   go1.8.3
 Git commit:   cec0b72
 Built:        Tue Sep  5 19:59:19 2017
 OS/Arch:      linux/amd64
 Experimental: true
```

**docker-composeのバージョン確認**

```bat
PS> docker-compose version
docker-compose version 1.14.0, build c7bdf9e3
docker-py version: 2.3.0
CPython version: 2.7.13
OpenSSL version: OpenSSL 1.0.2j  26 Sep 2016
```

**docker-machineのバージョン確認**

* この記事では使用しません。

```bat
PS> docker-machine version
docker-machine.exe version 0.12.2, build 9371605
```

# その1: 基本的なコマンドの使い方

[ubuntu] (https://hub.docker.com/_/ubuntu/)のイメージを使って基本的なdockerコマンドの使い方を確認します。

## イメージのダウンロード (pull)

名前:タグで指定するイメージをレジストリからダウンロードします。

```bat
PS> docker pull ubuntu:17.10
```

## イメージを一覧表示 (images)

```bat
PS> docker images
```

## イメージIDだけ表示 (images -q)

```bat
PS> docker images -q
```

## コンテナの実行 (run)

名前:タグで指定するイメージから新しいコンテナ作成して実行します。コンテナに名前を付けたい場合はオプションに`--name`で指定します。

```bat
PS> docker run --name my-ubuntu -it ubuntu:17.10 bash
```

実行後にコンテナを削除したい場合はオプションに`--rm`を指定します。

```bat
PS> docker run --rm -it ubuntu:17.10 bash
```

## 実行中のコンテナを一覧表示 (ps)

```bat
PS> docker ps
```

## すべてのコンテナを一覧表示 (ps -a)

```bat
PS> docker ps -a
```

## コンテナIDだけ表示 (ps -q)

```bat
PS> docker ps -q
```

## 実行中のコンテナを停止 (stop)

停止するコンテナのIDまたはコンテナ名を指定します。この例ではコンテナ名(my-ubuntu)を指定しています。

```bat
PS> docker stop my-ubuntu
```

## 停止中のコンテナを起動 (start)

停止中のコンテナをコンテナのIDまたはコンテナ名を指定して再起動します。この例ではコンテナ名(my-ubuntu)を指定しています。

```bat
PS> docker start my-ubuntu
```

## 実行中のコンテナで指定するコマンドを実行 (exec)

この例では実行中のコンテナで`bash`を実行します。

```bat
PS> docker exec -it my-ubuntu bash
```

この例では`apt-get update`でパッケージを更新します。

```bat
PS> docker exec my-ubuntu apt-get update
```

## コンテナの削除 (rm)

コンテナを削除するにはコンテナIDまたはコンテナ名を指定します。この例ではコンテナIDを指定しています。

```bat
PS> docker ps -a -q
d035184f4ba2

PS> docker rm d035184f4ba2
```

## イメージの削除 (rmi)

イメージを削除するにはイメージIDまたは名前:タグを指定します。この例ではイメージIDを指定しています。

```bat
PS> docker images -q
2e8302ea9a68

PS> docker rmi 2e8302ea9a68
```

Dockerfileやdocker-compose.ymlでビルドを行っているとREPOSITORY:TAGが`<none>:<none>`というイメージが出来ていることがあります。
このイメージを一覧表示するには以下のコマンドを実行します。

```bat
PS> docker images --filter "dangling=true"
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
<none>              <none>              c09af2b1125c        17 hours ago        197MB
<none>              <none>              45f8e04c9735        17 hours ago        420MB
```

一括削除するにはオプション`-q`を付けてイメージIDだけを出力するようにします。

```bat
PS> docker rmi $(docker images -q --filter "dangling=true")
```


## 停止中のコンテナをすべて削除 (container prune)

```bat
PS> docker container prune -f
```

他にPowerShellの式展開「`$(式)`」を使う方法があります。

```bat
PS> docker rm $(docker ps -a -q)
```


# その2: Dockerfileからビルドする

Dockerfileからカスタマイズしたイメージを作成します。

## Dockerfileの作成

適当なディレクトリに作成し、そこに下記の内容のDockerfileを作成します。

```text:Dockerfile
FROM ubuntu:16.04

ENV LANG ja_JP.utf8
ENV TZ Asia/Tokyo

RUN apt-get update \
&& apt-get install -y vim locales tzdata \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* \
&& localedef -i ja_JP -c -f UTF-8 -A /usr/share/locale/locale.alias ja_JP.UTF-8 \
&& ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \
&& echo $TZ > /etc/timezone \
&& dpkg-reconfigure --frontend noninteractive tzdata

CMD ["/bin/bash"]
```

* もともとUbuntu 17.10で確認していたのですがタイムゾーンの設定がうまく出来なかったので16.04に下げました。

## イメージのビルド (build)

オプション`-t, --tag`でイメージにユーザー名/名前:タグを指定できます。

```bat
PS> docker build -t rubytomato/ubuntu:0.0.1 .
```

任意のファイル名のDockerfileを使いたい場合はオプション`-f`で指定します。

```bat
PS> docker build -f Dockerfile-test -t rubytomato/ubuntu:0.0.1 .
```

ビルドしたイメージの確認

```bat
PS> docker images
REPOSITORY          TAG                 IMAGE ID            CREATED              SIZE
rubytomato/ubuntu   0.0.1               7f75f1fbe69a        About a minute ago   194MB
```

## コンテナの実行

この例ではオプション`-d`を指定してコンテナをデタッチしています。

```bat
PS> docker run --name my-ubuntu -it -d rubytomato/ubuntu:0.0.1
```

デタッチしたコンテナにはattachコマンドでアタッチすることができます。

```bat
PS> docker attach my-ubuntu
```

アタッチすればコンテナ内で任意の操作を行うことができます。

バージョンの確認

```bat 
# cat /etc/os-release
NAME="Ubuntu"
VERSION="16.04.3 LTS (Xenial Xerus)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 16.04.3 LTS"
VERSION_ID="16.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"
VERSION_CODENAME=xenial
UBUNTU_CODENAME=xenial
```

localeの確認

```bat
# locale
LANG=ja_JP.utf8
LANGUAGE=
LC_CTYPE="ja_JP.utf8"
LC_NUMERIC="ja_JP.utf8"
LC_TIME="ja_JP.utf8"
LC_COLLATE="ja_JP.utf8"
LC_MONETARY="ja_JP.utf8"
LC_MESSAGES="ja_JP.utf8"
LC_PAPER="ja_JP.utf8"
LC_NAME="ja_JP.utf8"
LC_ADDRESS="ja_JP.utf8"
LC_TELEPHONE="ja_JP.utf8"
LC_MEASUREMENT="ja_JP.utf8"
LC_IDENTIFICATION="ja_JP.utf8"
LC_ALL=
```

timezoneの確認

```bat
# date
2017年 10月  3日 火曜日 19:21:09 JST
```

## イメージをDocker Hubへプッシュする

先にDocker Hubでリポジトリを作成しておきます。

![h1.png](https://qiita-image-store.s3.amazonaws.com/0/22772/6d90fe2d-2d89-d57d-8a1c-2bf194379a3b.png)

リポジトリ名を入力します。今回はubuntuをカスタマイズしたイメージなので"ubuntu"としました。

![h2.png](https://qiita-image-store.s3.amazonaws.com/0/22772/fdc1f274-4b09-24b2-d762-72bd3bd01f4a.png)

リポジトリが出来たので次に作成したイメージをプッシュします。

![h3.png](https://qiita-image-store.s3.amazonaws.com/0/22772/494a8e52-9b95-fd54-0a62-674692246d41.png)

プッシュするためにログインします。

```bat
PS> docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: rubytomato
Password: **********
Login Succeeded
```

イメージをプッシュします。

```bat
PS> docker push rubytomato/ubuntu:0.0.1
The push refers to a repository [docker.io/rubytomato/ubuntu]
//...省略
```

プッシュに成功したら、Docker HubのDashboardページで確認します。

![h4.png](https://qiita-image-store.s3.amazonaws.com/0/22772/93a3e003-b81e-8ec8-5dc7-f4e68a0bc06e.png)

Docker Hubにプッシュしておけば、あとは他のイメージと同様にダウンロードすることができます。

```bat
PS> docker pull rubytomato/ubuntu:0.0.1
```


# その3: 複数のコンテナを連携させる

dockerコマンドを使って複数のコンテナから成るアプリケーションの実行環境を構築します。
動作検証用のアプリケーションはSpring Bootで開発したRest APIアプリケーションです。

|コンテナ名          |イメージ:タグ             |ポート     |
|:------------------|:------------------------|:----------|
|mysql-server       |mysql:5.7.19             |3306       |
|app-server         |openjdk:8u131-jdk-alpine |9000       |
|nginx-server       |nginx:1.13.5-alpine      |80:80      |


## ネットワークの作成

これから作成する各コンテナが接続するネットワークを作成します。

```bat
PS> docker network create --driver bridge my-network
```

**ネットワークの一覧**

NAMEがbride,host,noneはデフォルトで作成されているネットワークです。

```bat
PS> docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
37b1a0338435        bridge              bridge              local
077244cb331b        host                host                local
ec11b19b4f20        my-network          bridge              local
4b6c82e8b7a5        none                null                local
```

**ネットワークの状態**

作成した直後のネットワークの状態を確認します。まだこのネットワークに接続しているコンテナは無いのでContainersは空になっています。

```bat
PS> docker network inspect my-network
[
    {
        "Name": "my-network",
        "Id": "ec11b19b4f20b50851d829cfb8ae2d734e08c8b823d45b074a2c28575de46e8f",
        "Created": "2017-10-03T22:43:37.5049737Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.18.0.0/16",
                    "Gateway": "172.18.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {},
        "Labels": {}
    }
]
```

**ネットワークの削除**

```bat
PS> docker network rm my-network
```


## DBコンテナの作成

**イメージのダウンロード**

officialの[mysql-server] (https://hub.docker.com/r/mysql/mysql-server/)で公開されているイメージを利用します。

```bat
PS> docker pull mysql:5.7.19
```

**コンテナの実行**

データを永続化したいのでオプション`-v`でデータディレクトリを共有します。
この例では`D:/dev/docker-volume/mysql-5.7.19/data`とコンテナ上の`/var/lib/mysql`をマップしています。

ちなみにPowerShellで改行するにはバッククォート(`)を使用します。

```bat
PS> docker run --name mysql-server `
-v D:/dev/docker-volume/mysql-5.7.19/data:/var/lib/mysql `
-e MYSQL_ROOT_PASSWORD=password `
-e TZ=Asia/Tokyo `
--network my-network `
-d `
mysql:5.7.19 --character-set-server=utf8 --collation-server=utf8_general_ci --explicit-defaults-for-timestamp=1 --default-time-zone='+9:00'
```

**ログの確認**

データディレクトリを共有すると（環境にもよると思いますが）初回のMySQLの起動に時間が掛かります。これは物理的なデータファイルの作成に時間がかかっているためのようですが、psコマンドでコンテナのステータスを確認すると起動しているように見えます。

```bat
PS> docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
9dc09ba82ff8        mysql:5.7.19        "docker-entrypoint..."   9 seconds ago       Up 8 seconds        3306/tcp            mysql-server
```

しかし実際には、起動中なためMySQL Clientなどで接続しようとすると失敗します。このため実際に起動完了しているかどうかはログを確認します。

```bat
PS> docker logs --follow --tail 20 mysql-server
```

このような感じのログが出力されていればMySQL Serverの起動が完了していると思います。

```text
//...省略

[Note] mysqld: ready for connections.
Version: '5.7.19'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server (GPL)
[Note] Executing 'SELECT * FROM INFORMATION_SCHEMA.TABLES;' to get a list of tables using the deprecated partition engine. You may use the startup option '--disable-partition-engine-check' to skip this check.
[Note] Beginning of list of non-natively partitioned tables
[Note] End of list of non-natively partitioned tables
```

**MySQL Clientから接続**

別のコンテナを立ち上げてそこから実行するMySQL Clientで接続します

```bat
PS> docker run `
--network my-network `
-it `
--rm `
mysql:5.7.19 sh -c 'exec mysql -h mysql-server -P 3306 -uroot -p'
```

オプションが反映されていることを確認します。

```sql
mysql> show variables like '%character%';
+--------------------------+----------------------------+
| Variable_name            | Value                      |
+--------------------------+----------------------------+
| character_set_client     | latin1                     |
| character_set_connection | latin1                     |
| character_set_database   | utf8                       |
| character_set_filesystem | binary                     |
| character_set_results    | latin1                     |
| character_set_server     | utf8                       |
| character_set_system     | utf8                       |
| character_sets_dir       | /usr/share/mysql/charsets/ |
+--------------------------+----------------------------+
8 rows in set (0.01 sec)

mysql> show variables like '%zone%';
+------------------+--------+
| Variable_name    | Value  |
+------------------+--------+
| system_time_zone | JST    |
| time_zone        | +09:00 |
+------------------+--------+
2 rows in set (0.00 sec)
```

**動作検証用アプリケーションで使用するDBの作成**

補足の動作検証用アプリケーションで使用するDBの内容で、データベース、ユーザー、テーブルを作成します。

**コンテナの停止、起動**

コンテナを停止するにはstopコマンドを使用します。

```bat
PS> docker stop mysql-server
```

コンテナが作成できていれば以降はstartコマンドで起動することができます。

```bat
PS> docker start mysql-server
```


## APPコンテナの準備

Javaアプリケーションを実行するためのコンテナです。また、コンテナ実行時にJavaアプリケーションを実行するのではなく、コンテナとは別のタイミングで実行するようにします。

**イメージのダウンロード**

officialの[openjdk] (https://hub.docker.com/_/openjdk/)で公開されているイメージを利用します。

```bat
PS> docker pull openjdk:8u131-jdk-alpine
```

バージョンの確認

```
PS> docker run --rm openjdk:8u131-jdk-alpine java -version
openjdk version "1.8.0_131"
OpenJDK Runtime Environment (IcedTea 3.4.0) (Alpine 8.131.11-r2)
OpenJDK 64-Bit Server VM (build 25.131-b11, mixed mode)
```

**コンテナの実行**

アプリケーションのプロジェクトディレクトリを共有しコンテナ内からアプリケーションのビルドおよび起動・停止が行えるようにします。

この例では`D:/dev/IdeaProjects/demo`にあるプロジェクトディレクトリをコンテナ上の`/app`にマップしています。

```bat
PS> docker run `
--name app-server `
-v D:/dev/IdeaProjects/demo:/app:rw `
--network my-network `
-it `
-d `
openjdk:8u131-jdk-alpine sh
```

**アプリケーションの起動**

Windows環境側で実行可能なjarをビルドしておけば、下記のコマンドでアプリケーションを起動できます。

```bat
PS> docker exec -it app-server java -jar '-Dspring.profiles.active=docker' /app/target/demo.jar
```

**コンテナ内でビルド、起動・停止を行う**

コンテナにアクセスします。

```bat
PS> docker exec -it app-server sh
```

mavenプラグインを使ってアプリケーションを起動する方法

```
# cd /app
# ./mvnw spring-boot:run -Drun.profiles=docker
```

ビルドしたjarを使ってアプリケーションを起動する方法

```
# cd /app
# ./mvnw clean package -Dmaven.test.skip=true
# java -jar -Dspring.profiles.active=docker target/demo.jar
```


## WEBコンテナの準備

**イメージのダウンロード**

officialの[nginx] (https://hub.docker.com/_/nginx/)で公開されているイメージを利用します。

```bat
PS> docker pull nginx:1.13.5-alpine
```

動作確認のため、コンテナを実行してブラウザでページにアクセスしてみます。

```bat
PS> docker run --name my-nginx --rm -d -p 80:80 nginx:1.13.5-alpine nginx-debug -g 'daemon off;'
```

ブラウザからlocalhostにアクセスして下図のページが表示されることを確認します。

![n1.png](https://qiita-image-store.s3.amazonaws.com/0/22772/6754d60b-2fc4-f10a-2714-6cf59060d276.png)

確認出来たらコンテナを停止させます。

```bat
PS> docker stop my-nginx
```

**コンテナの実行**

クライアントからのアクセスをAPPサーバーへプロキシするのでjava-appコンテナと連携します。
この例ではAPPサーバーへプロキシさせるconfファイルを`D:/dev/docker-volume/nginx-1.13.5/conf.d`に配置し、このディレクトリとコンテナ上の`/etc/nginx/conf.d`とマップさせています。

```bat
PS> docker run `
--name nginx-server `
-v D:/dev/docker-volume/nginx-1.13.5/conf.d:/etc/nginx/conf.d:ro `
--network my-network `
-p 80:80 `
-d `
nginx:1.13.5-alpine
```

**設定ファイル**

アプリケーションサーバーへのプロキシ設定を下記内容のdefault.confファイルに記述しました。

```text:default.conf
server {
    listen 80;
    server_name localhost;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Host $host;

    location /app/ {
        proxy_pass http://app-server:9000/app/;
        proxy_redirect off;
        proxy_cookie_path /app /;
    }
}
```

## 3つのコンテナが立ち上がった状態

上記の内容で実行したコンテナが立ち上がっている状態です。

```bat
PS> docker ps
CONTAINER ID        IMAGE                      COMMAND                  CREATED             STATUS              PORTS                NAMES
b98e5fce9bb4        nginx:1.13.5-alpine        "nginx -g 'daemon ..."   2 minutes ago       Up 2 minutes        0.0.0.0:80->80/tcp   nginx-server
c0bfc8bef336        openjdk:8u131-jdk-alpine   "sh"                     4 minutes ago       Up 4 minutes                             app-server
2c2eb4d0e040        mysql:5.7.19               "docker-entrypoint..."   6 minutes ago       Up 6 minutes        3306/tcp             mysql-server
```

作成したネットワークに上記3つのコンテナが接続されていることが確認できます。

```bat
PS> docker network inspect my-network
[
    {
        "Name": "my-network",
        "Id": "ec11b19b4f20b50851d829cfb8ae2d734e08c8b823d45b074a2c28575de46e8f",
        "Created": "2017-10-03T22:43:37.5049737Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.18.0.0/16",
                    "Gateway": "172.18.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "2c2eb4d0e040d523a911b7f875b6a77799cc38d2b502533c6fc999d2dd828b4e": {
                "Name": "mysql-server",
                "EndpointID": "54bcfb53d937fbc2d56f533335cb7239327c2dbe62cbf7428c8aaef3be40bac7",
                "MacAddress": "02:42:ac:12:00:02",
                "IPv4Address": "172.18.0.2/16",
                "IPv6Address": ""
            },
            "b98e5fce9bb4ac0ef13b6d899bc69b2bcd0ec7a93f601568b2e6b4c52916a75a": {
                "Name": "nginx-server",
                "EndpointID": "7fb9c4b8448fab4a28c8d13fe91db75553d734999305b78f9c9e06389ecc32d6",
                "MacAddress": "02:42:ac:12:00:04",
                "IPv4Address": "172.18.0.4/16",
                "IPv6Address": ""
            },
            "c0bfc8bef3361a4cc92c8b6271986b78d12e31f32e08356c01cf5d0d30370743": {
                "Name": "app-server",
                "EndpointID": "471ee5e713b606a0c3d487acd2d27406ba7bd13a9c34efa53e1220ae60e0a594",
                "MacAddress": "02:42:ac:12:00:03",
                "IPv4Address": "172.18.0.3/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    }
]
```

**アプリケーションの動作確認**

ブラウザから下記のアドレスにアクセスしてjsonのレスポンスが返ってくることを確認します。

```
localhost/app/memo/list
```

```
localhost/app/memo/id/1
```

動作確認できたらコンテナ停止、削除します。

```bat
PS> docker stop nginx-server app-server mysql-server
```

```bat
PS> docker rm nginx-server app-server mysql-server
```

# その4: docker-composeの使い方

その3で構築した環境をdocker-composeを使って構築してみます。
(これまでに作成したネットワーク、イメージ、コンテナはすべて削除して行いました。)

## プロジェクトの構造

docker-compose.ymlの他、各コンテナのイメージをビルドするためのDockerfileやコンテナにコピーするファイルを管理するプロジェクトです。
複数メンバーで開発環境を共有する場合、このプロジェクトをVCSで管理するという想定です。各自でdockerをインストールし、このプロジェクトをチェックアウトしdocker-composeで環境を構築します。

```text
/root
 |
 +--- docker-compose.yml
 |
 +--- /mysql
 |      |
 |      +--- Dockerfile
 |      |
 |      +--- /conf
 |      |      |
 |      |      +--- mysqld.cnf
 |      |
 |      +--- /sql
 |             |
 |             +--- 1_schema.sql
 |             +--- 2_init_memo_data.sql
 |
 +--- /app
 |      |
 |      +--- Dockerfile
 |
 +--- /nginx
        |
        +--- Dockerfile
        |
        +--- /conf
        |      |
        |      +--- default.conf
        |
        +--- /repo
               |
               +--- nginx.repo
```

### docker-compose.yml

```yaml
version: "3"

services:
  db:
    container_name: mysql-server
    build:
      context: ./mysql
    volumes:
      - dbdata:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=password
    networks:
      - my-network
    restart: always
  app:
    container_name: app-server
    build:
      context: ./app
    volumes:
      - ${PROJECT_HOME}:/app
    environment:
      - SPRING_PROFILES_ACTIVE=docker
    depends_on:
      - db
    networks:
      - my-network
    restart: always
    tty: true
  web:
    container_name: nginx-server
    build:
      context: ./nginx
    depends_on:
      - app
    ports:
      - 80:80
    networks:
      - my-network
    restart: always

networks:
  my-network:
    driver: bridge

volumes:
  dbdata:
    driver: local
```

app-serverコンテナが参照するローカルのプロジェクトディレクトリを`${PROJECT_HOME}`という変数で定義しています。
この変数の値は環境変数で設定します。

PowerShellで環境変数を定義するにはset-itemを使用します。
（この方法だとPowerShellを立ち上げるたびに定義しないといけないので、PCのシステムプロパティに登録するか$PROFILEに記述した方が便利です。）

```bat
PS> set-item env:PROJECT_HOME -value D:\dev\IdeaProjects\demo
```


### DBコンテナ

#### Dockerfile

コンテナ上の`/etc/mysql/mysql.conf.d`へカスタマイズした設定ファイルを配置するとその内容が反映されます。
また、コンテナ上の`/docker-entrypoint-initdb.d`にshまたはsqlファイルを配置するとそのファイルが実行されます。この例ではデータベース、ユーザー、テーブルの作成および初期データの投入を行っています。

```text
FROM mysql:5.7.19

COPY ./conf/mysqld.cnf /etc/mysql/mysql.conf.d

COPY ./sql/1_schema.sql /docker-entrypoint-initdb.d
COPY ./sql/2_init_memo_data.sql /docker-entrypoint-initdb.d

RUN chmod 644 /etc/mysql/mysql.conf.d/mysqld.cnf

ENV TZ=Asia/Tokyo
```

##### conf

```text:mysqld.cnf
[mysqld]
pid-file        = /var/run/mysqld/mysqld.pid
socket          = /var/run/mysqld/mysqld.sock
datadir         = /var/lib/mysql

# Disabling symbolic-links is recommended to prevent assorted security risks
symbolic-links=0

max_connections = 30
explicit_defaults_for_timestamp = 1

character_set_server = utf8
collation_server = utf8_general_ci

log_output = FILE
general_log = 1
log_queries_not_using_indexes = 1
log_slow_admin_statements = 1
log_syslog = 0
log_timestamps = SYSTEM
long_query_time = 3
slow_query_log = 1

general_log_file = /var/log/mysql/general_query_all.log
log-error = /var/log/mysql/error.log
slow_query_log_file = /var/log/mysql/slow_query.log
```

##### sql

補足の動作検証用アプリケーションで使用するDBに記載しています。

* 1_schema.sql
* 2_init_memo_data.sql

### APPコンテナ

#### Dockerfile

```text
FROM centos:7

RUN yum update -y && yum install -y java-1.8.0-openjdk java-1.8.0-openjdk-devel && yum reinstall -y glibc-common && yum -y clean all

ENV LANG ja_JP.UTF-8
ENV LANGUAGE ja_JP:ja
ENV JAVA_HOME /usr/lib/jvm/java-1.8.0-openjdk

RUN localedef -f UTF-8 -i ja_JP ja_JP.UTF-8
RUN unlink /etc/localtime
RUN ln -s /usr/share/zoneinfo/Japan /etc/localtime

EXPOSE 9000
```

### WEBコンテナ

#### Dockerfile

```text
FROM centos:7

COPY ./repo/nginx.repo /etc/yum.repos.d

RUN yum update -y && yum install -y --enablerepo=nginx nginx && yum reinstall -y glibc-common && yum -y clean all

ENV LANG ja_JP.UTF-8
ENV LANGUAGE ja_JP:ja

RUN localedef -f UTF-8 -i ja_JP ja_JP.UTF-8
RUN unlink /etc/localtime
RUN ln -s /usr/share/zoneinfo/Japan /etc/localtime

COPY ./conf/default.conf /etc/nginx/conf.d

RUN chmod 664 /etc/nginx/conf.d/default.conf

RUN ln -sf /dev/stdout /var/log/nginx/access.log && ln -sf /dev/stderr /var/log/nginx/error.log

EXPOSE 80

STOPSIGNAL SIGTERM

CMD ["nginx", "-g", "daemon off;"]
```

##### conf

APPサーバーへプロキスする設定を記述したconfファイルです。

```text:default.conf
server {
    listen 80;
    server_name localhost;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Host $host;

    location /app/ {
        proxy_pass http://app-server:9000/app/;
        proxy_redirect off;
        proxy_cookie_path /app /;
    }
}
```

##### repo

yumでnginxをインストールするのに必要なrepositoryの設定ファイルです。いまのところ(2017/10)この設定がないとyumでインストールできませんでした。

```text:nginx.repo
[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/centos/7/$basearch/
gpgcheck=0
enabled=1
```

## docker-composeコマンド

### ビルド (build)

docker-compose.ymlのあるディレクトリで下記コマンドを実行してイメージをビルドします。

```bat
PS> docker-compose build
```

キャッシュを使わずにビルドする場合はオプション`--no-cache`を指定します。

```bat
PS> docker-compose build --no-cache
```

### 起動 (up)

```bat
PS> docker-compose up -d
Creating volume "dockerdev_dbdata" with local driver
Creating network "dockerdev_my-network" with driver "bridge"
Creating mysql-server ...
Creating mysql-server ... done
Creating app-server ...
Creating app-server ... done
Creating nginx-server ...
Creating nginx-server ... done
```

イメージのビルドと起動を同時に行うこともできます。ビルドは変更のあるイメージに対してのみ行われます。

```bat
PS> docker-compose up --build -d
```

サービス名を指定して個別に起動する場合、他のサービスと依存関係があれば自動的にそれらのサービスも起動してくれます。

```bat
PS> docker-compose up -d app
Creating mysql-server ...
Creating mysql-server ... done
Creating app-server ...
Creating app-server ... done
```

### 一覧 (ps)

```bat
PS> docker-compose ps
    Name                 Command             State         Ports
-----------------------------------------------------------------------
app-server     /bin/bash                     Up      9000/tcp
mysql-server   docker-entrypoint.sh mysqld   Up      3306/tcp
nginx-server   nginx -g daemon off;          Up      0.0.0.0:80->80/tcp
```

### 任意のコマンドを実行 (exec)

各コンテナは起動していますがapp-serverではまだSpring Bootのアプリケーションは起動していません。
下記のexecコマンドでコンテナに接続してアプリケーションを起動します。なお、ここで指定しているappはコンテナ名ではなくサービス名です。

```bat
PS> docker-compose exec app bash
```

mavenプラグインでアプリケーションのビルドと実行を行います。その3ではプロファイルを指定していましたが、その4では環境変数(SPRING_PROFILES_ACTIVE)に定義しているため省略可能です。

```bat
# cd /app
# ./mvnw spring-boot:run

// ... 省略

INFO 151 --- [  restartedMain] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 9000 (http)
INFO 151 --- [  restartedMain] com.example.demo.DemoApplication         : Started DemoApplication in 6.065 seconds (JVM running for 6.562)
```

**アプリケーションの動作確認**

各コンテナが連携していれば正常なレスポンスが返ってきます。

```
http://localhost/app/memo/list
```

```
http://localhost/app/memo/id/1
```

### 実行中のプロセスの確認 (top)

topコマンドで各サービスのプロセスを確認できます。

```bat
PS> docker-compose top
app-server
 PID    USER   TIME    COMMAND
-------------------------------
12625   root   0:00   /bin/bash

mysql-server
 PID    USER   TIME   COMMAND
-----------------------------
11915   999    0:00   mysqld

nginx-server
 PID    USER   TIME                    COMMAND
----------------------------------------------------------------
12773   root   0:00   nginx: master process nginx -g daemon off;
12828   999    0:00   nginx: worker process
```

### 停止 (down)

```bat
PS> docker-compose down
Stopping nginx-server ... done
Stopping app-server ... done
Stopping mysql-server ... done
Removing nginx-server ... done
Removing app-server ... done
Removing mysql-server ... done
Removing network dockerdev_my-network
```

### ログ (logs)

サービスが起動しないなどの原因を調べたい場合はlogsコマンドでログを確認します。

```bat
PS> docker-compose logs db
```


# 補足

## [posh-docker] (https://github.com/samneirinck/posh-docker)のインストール

PowerShellでdockerコマンドのタブ補完を行ってくれる拡張機能です。Docker for Windowsを使う上で必須ではありませんが便利なのでインストールしています。

> Powershell tab completion for Docker

NuGetが必要です。まだインストールされていない場合は、管理者権限でPowerShellを起動し下記コマンドを実行してインストールします。

```bat
PS> Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force

Name                           Version          Source           Summary
----                           -------          ------           -------
nuget                          2.8.5.208        https://onege... NuGet provider for the OneGet meta-package manager
```

続いて、下記コマンドでposh-dockerのインストールを行います。こちらは管理者権限でなくても大丈夫です。

```bat
PS> Install-Module -Scope CurrentUser posh-docker

信頼されていないリポジトリ
信頼されていないリポジトリからモジュールをインストールしようとしています。このリポジトリを信頼する場合は、Set-PSReposit
ory コマンドレットを実行して、リポジトリの InstallationPolicy の値を変更してください。'PSGallery'
からモジュールをインストールしますか?
[Y] はい(Y)  [A] すべて続行(A)  [N] いいえ(N)  [L] すべて無視(L)  [S] 中断(S)  [?] ヘルプ (既定値は "N"): A
```

インストールできたら最後に下記コマンドを実行します。これでコマンドのタブ補完が有効になります。
この設定を永続化するためにこのコマンドを$PROFILEに書き込む必要があります。

```bat
PS> Import-Module posh-docker
```

$PROFILEに書き込むには下記の内容をPowerShellのコマンドプロンプトから実行します。

```bat
if (-Not (Test-Path $PROFILE)) {
    New-Item $PROFILE –Type File –Force
}

Add-Content $PROFILE "`nImport-Module posh-docker"
```

$PROFILEがすでに存在する場合は、テキストエディタで直接編集することも可能です。プロンプトに`$PROFLIE`と入力して実行するとファイルの存在場所がフルパスが表示されます。

```bat
PS> $PROFILE
```

### 実行ポリシーを変更する

スクリプトの実行ポリシーを変更しないと$PROFILEに加えた設定が実行されない場合があります。その場合は管理者権限で起動したPowerShellから下記のコマンドを実行して実行ポリシーを変更します。

```bat
PS> Set-ExecutionPolicy RemoteSigned
```

現在の実行ポリシーを確認する

```bat
PS> Get-ExecutionPolicy
```

### インストールされているモジュールを確認

```bat
PS> Get-Module –ListAvailable
```

### アンインストール

```bat
PS> Uninstall-Module posh-docker
```

$PROFILEに追加した行を削除

```bat
PS> notepad $PROFILE
```

## 動作検証用アプリケーションで使用するDB

アプリケーションサーバーで実行するSpring Bootアプリケーションが使用するデータベースおよびテーブルの情報です。

**1_schema.sql**

```sql:1_schema.sql
CREATE DATABASE sample_db DEFAULT CHARACTER SET = utf8mb4;

CREATE USER 'test_user'@'%' IDENTIFIED BY 'test_user' PASSWORD EXPIRE NEVER;

GRANT ALL ON sample_db.* TO 'test_user'@'%';

USE sample_db;

CREATE TABLE IF NOT EXISTS memo (
   id BIGINT AUTO_INCREMENT,
   title VARCHAR(255) NOT NULL,
   description TEXT NOT NULL,
   done BOOLEAN DEFAULT FALSE NOT NULL,
   updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
   PRIMARY KEY (id)
) CHARACTER SET = utf8mb4, COLLATE utf8mb4_general_ci;
```

**2_init_memo_data.sql**

```sql:2_init_memo_data.sql
USE sample_db;

START TRANSACTION;

INSERT INTO memo (title, description, done, updated) VALUES ('Memo A', 'aISms0a02jsy47xk4kao28FlqUqnwl', false, '2017-10-01');
INSERT INTO memo (title, description, done, updated) VALUES ('Memo B', 'i5hxoG8rm29quqububMr9gu1OQia75', false, '2017-10-02');
INSERT INTO memo (title, description, done, updated) VALUES ('Memo C', '5L18coGlrHao3yz9xur8c9vpDhfu6o', true, '2017-10-02');
INSERT INTO memo (title, description, done, updated) VALUES ('Memo D', 'Mw8Xu1itnr8fkdn4lbpWJfpe91mg71', false, '2017-10-03');
INSERT INTO memo (title, description, done, updated) VALUES ('Memo E', 'Xl6ngkzic8w05orn6hc7b82hthXJrK', true, '2017-10-03');
INSERT INTO memo (title, description, done, updated) VALUES ('Memo F', '5kspemtnWUQ94lemehf0f0aM482iqo', false, '2017-10-03');
INSERT INTO memo (title, description, done, updated) VALUES ('Memo G', 'lB82Lq8riepbo395UejquBnbu40syd', false, '2017-10-04');
INSERT INTO memo (title, description, done, updated) VALUES ('Memo H', 'ptye0qPrig7tyZh59Sut74Lqnwk4j3', true, '2017-10-05');
INSERT INTO memo (title, description, done, updated) VALUES ('Memo I', '9d7sien1mgyxu37gI6nqHfhFmx95iV', true, '2017-10-06');
INSERT INTO memo (title, description, done, updated) VALUES ('Memo J', 'zPwyxLEp50am18quX0d7bnajeo1k2j', true, '2017-10-06');

COMMIT;
```

## 動作検証用アプリケーション

コンテナの動作検証用にRest APIアプリケーションをSpring Bootで開発しました。
以下にコントローラーのソースコードとpom.xml、application.ymlの内容を記載します。
アプリケーションはmemoテーブルのデータをjsonフォーマットでレスポンスする単純なRest APIです。

エンドポイントは下記の2つだけです。

```
localhost:9000/app/memo/list
```

```
localhost:9000/app/memo/id/{id}
```

**コントローラー**

```java
@RestController
@RequestMapping(path = "memo", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@Slf4j
public class MemoController {

    private MemoService service;

    @Autowired
    public MemoController(MemoService service) {
        this.service = service;
    }

    @GetMapping(path = "id/{id}")
    public ResponseEntity<Memo> id(@PathVariable(value = "id") Long id) {
        Memo memo = service.findById(id);
        return new ResponseEntity<>(memo, HttpStatus.OK);
    }

    @GetMapping(path = "list")
    public ResponseEntity<List<Memo>> title(Pageable page) {
        Page<Memo> memos = service.findAll(page);
        return new ResponseEntity<>(memos.getContent(), HttpStatus.OK);
    }

}
```

**pom.xml**

```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.example</groupId>
	<artifactId>demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>demo</name>
	<description>Demo project for Spring Boot</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.5.7.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>com.fasterxml.jackson.datatype</groupId>
			<artifactId>jackson-datatype-jsr310</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-configuration-processor</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-java8</artifactId>
		</dependency>
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-databind</artifactId>
			<version>2.8.8</version>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<scope>provided</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<finalName>${project.name}</finalName>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludeDevtools>true</excludeDevtools>
					<executable>true</executable>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.3</version>
				<configuration>
					<compilerVersion>1.8</compilerVersion>
					<source>1.8</source>
					<target>1.8</target>
					<encoding>UTF-8</encoding>
					<compilerArgs>
						<arg>-Xlint:all,-options,-path</arg>
					</compilerArgs>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>
```

**application.yml**

```yaml:application.yml
# SPRING CORE
spring:
  application:
    name: demo
  beaninfo:
    ignore: true
# OUTPUT
  output:
    ansi:
      enabled: detect
# PROFILES
  profiles:
    active: dev
# DATASOURCE
  datasource:
    url: jdbc:mysql://localhost:3306/sample_db?useSSL=false
    username: test_user
    password: test_user
    driverClassName: com.mysql.jdbc.Driver
# JPA
  jpa:
    properties:
      hibernate:
        show_sql: true
        format_sql: true
        use_sql_comments: true
  jackson:
    serialization:
      write-dates-as-timestamps: false

# EMBEDDED SERVER CONFIGURATION
server:
  port: 9000
  context-path: /app
logging:
  level:
    root: INFO
    org.springframework: INFO
    com.example: DEBUG

---

spring:
  profiles: docker
  datasource:
    url: jdbc:mysql://mysql-server:3306/sample_db?useSSL=false
    username: test_user
    password: test_user
    driverClassName: com.mysql.jdbc.Driver
```


## Dockerfileの備忘

CMD、RUNの記述形式にはshell形式とexec形式の2通りあります。

**CMD**

shell形式

```
CMD command param1 param2
```

exec形式

```
CMD ["executable","param1","param2"]
```

**RUN**

shell形式

```
RUN <command>
```

exec形式

```
RUN ["executable", "param1", "param2"]
```