Dockerについて基本から最近追加された機能までまとめ


# Dockerとは

コンテナベースのアプリケーションを仮想化したもの。軽量なVMの様に見えるがこれまでの(VirtualBoxなど)VMでは実現が難しい、不可能であったユースケースを解決してくれる。

- ホストOSとリソースを共有するのでリソースの管理がVMより効率的
- 基本的に状態を持たないのでポータビリティが非常に高く、特定の環境に依存することがない
- 軽量なのでVMと比較し複数のインスタンスを実行することができる
- DockerHubなどのレジストリを利用することで既存のイメージをダウンロードして実行することができる

# コンテナとVM
## VM

- VMはハイパーバイザを通してホストOSに対してのシステムコールを解釈させるなどの必要がある
- それぞれのVMには全て独立したOS・アプリケーション・ライブラリが必要

## コンテナ

- ホストのカーネルは実行されるコンテナと共有される（コンテナは常にホストと同じカーネルを使う必要がある）
- この例ではアプリケーションYとZはライブラリBを共有している
- コンテナ内で実行されるプロセスはホストで実行されるプロセスと同等で、ハイパーバイザの実行に伴うオーバーヘッドが存在しない

![docker5_1.png](https://qiita-image-store.s3.amazonaws.com/0/32836/3f37e014-9b86-5d3d-e3e0-dfafee20eee0.png "docker5_1.png")![docker5_2.png](https://qiita-image-store.s3.amazonaws.com/0/32836/a1088017-9a91-c25f-a89f-d6a8504e030c.png "docker5_2.png")

## Dockerの実行環境について

Dockerは純粋な仮想化は行わないため64bit Linuxでしか基本的に動作しないが、Docker for MacやDocker for Windowsなど透過的に使えるソフトウェアが開発されている。
内部ではVMを経由してDockerを実行しているためLinuxでDockerを直接利用するよりパフォーマンスの面で劣る。
https://www.docker.com/products/docker

#Dockerの実行

インストール後に

```console
$ docker run hello-world
```

でDockerについての情報を表示するイメージがダウンロードされ、イメージを元にしたコンテナが起動・実行される。


次に実用的な例として、
```console
$ docker run -it ubuntu /bin/bash
```

でubuntuのbashが立ち上がる。runのあとにコマンドを記述することでコンテナの中でそのコマンドが実行される。コンテナ内のカレントディレクトリについては `Dockerfile` の `WORKDIR` で定義されたものになる（後述）。

`-it` はtty付きのインタラクティブセッションを要求するオプションで、対話的なコマンドを実行したいときは付ける必要がある。

Ubuntuのイメージでbashを起動した状態で別のコンソールから `docker ps` コマンドを実行することでコンテナの状態を確認することができる。

```console
$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
50ffa6334a98        ubuntu              "/bin/bash"         2 seconds ago       Up 1 seconds                           reverent_bose
```

この場合reverent_boseが自動的に振られたコンテナの名前である。（--nameオプションで明示的に指定もできる。衝突した場合は起動できない）

次にコンテナ内でファイルを作成し

```console
root@7d31a9f7251c:/# touch /tmp/hoge
```

外部からコンテナの状態を確認してみる。
`docker diff` を実行すると

```console
$ docker diff reverent_bose
C /tmp
A /tmp/hoge
```

と表示される。これはDockerがファイルシステムにUFS(union file system)を使用して複数のファイルシステムを階層的にマウントしており、ファイルシステム上の差分について管理されるようになっているからである。

更にこの状態で `docker commit` コマンドを実行する。

```console
$ docker commit reverent_bose test/create_file
sha256:e6816e42b5d88aa4fb03cfe5e20d56502134ae0e76cf0cb71239dbc0f6b56a16

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
test/create_file    latest              e6816e42b5d8        4 seconds ago       127.1 MB
ubuntu              latest              c73a085dc378        3 hours ago         127.1 MB
```

`docker images` を確認すると `test/create_file` という新たなイメージが作成されていることがわかる。
この `test/create_file` からコンテナを起動すると、ubuntuイメージから起動したコンテナに加えて `/tmp/test` ファイルが存在する状態になっている。

# `Dockerfile` でのイメージの作成

上記のような手順でイメージを作成するのはDockerを使う上ではバッドノウハウとされている。
人が徐々に手を加えて作成されたイメージは再現性が低くなってしまい、イメージに変更を加えるのが難しくなってしまうためである。
そのため、Dockerのイメージは一般的に `Dockerfile` を用いて作成される。

`Dockerfile` の細かい構文については以下のドキュメントに書かれている。（https://docs.docker.com/engine/reference/builder/ ）
独自DSLだが基本的にはシェルスクリプトの形式で記述する。


次に、上記で作ったイメージを作成する `Dockerfile` を記述する。

適当なディレクトリを作成し、 `Dockerfile` の作成

```Dockerfile
FROM ubuntu:latest

RUN touch /tmp/hoge
```

その後次のコマンドを実行する。

```console
$ docker build -t test/create_file .
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM ubuntu:latest
 ---> c73a085dc378
Step 2 : RUN touch /tmp/hoge
 ---> Running in ce59c6b29dae
 ---> 4275ce1b43b8
Removing intermediate container ce59c6b29dae
Successfully built 4275ce1b43b8
```

すると先ほど `docker commit` で作成したものと同じイメージが作成される。

次は、 `cowsay` を実行するためのイメージを別途作成する。新しい `Dockerfile` を作成し以下のように記述する。

```Dockerfile
FROM ubuntu:latest

RUN apt-get update && apt-get install -y cowsay fortune
```

```console
$ docker build -t test/docker-cowsay .
```

`cowsay` を実行する。

```console
$ docker run test/docker-cowsay /usr/games/cowsay hoge
 ______
< hoge >
 ------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
```

これで `cowsay` が入ったイメージを実行できるが、更に簡単に `cowsay` を実行させる方法がある。

`Dockerfile` で `ENTRYPOINT` という構文を使う。

```Dockerfile
FROM ubuntu:latest

RUN apt-get update && apt-get install -y cowsay fortune

ENTRYPOINT ["/usr/games/cowsay"]
```

`Dockerfile` を変更した場合はイメージを再度作成する必要があるため `docker build` を実行する。

```console
$ docker build -t test/docker-cowsay .
```

```console
$ docker run test/docker-cowsay hoge
 ______
< hoge >
 ------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
```

これで `cowsay` コマンドを簡易に実行するイメージの作成ができた。
ただし、このままでは他のコマンドを実行したい場合などに不便である。

そのためDockerではよくあるパターンとして `entrypoint.sh` などといったシェルスクリプトを `ENTRYPOINT` に指定し、その中で実行する処理を分岐させることが多い。
以下が `entrypoint.sh` の例である。

```entrypoint.sh
#!/bin/bash

if [ $# -eq 0 ]; then
    /usr/games/fortune | /usr/games/cowsay
else
    /usr/games/cowsay "$@"
fi
```

```console
$ chmod +x entrypoint.sh
```

```Dockerfile
FROM ubuntu:latest
 
RUN apt-get update && apt-get install -y cowsay fortune
ADD entrypoint.sh /

ENTRYPOINT ["/entrypoint.sh"]
```


```console
$ docker build -t test/docker-cowsay .
```

Dockerのイメージを作成する際の注意点として、DockerはDockerfileの行ごとに状態の差分を保存しているためDockerfileに変更を加えた場合、その下に書かれている処理は全て再度実行される。

例えば最初の処理でapt-getコマンドを実行していた場合、それより下の行を変更して `docker build` を行った際、apt-getコマンドが実行されるのは1回だけである。しかし、apt-getコマンドより上にADDなどが記述されており、対象ファイルが変更されていた場合はその後の処理が全て再度実行されてしまう。

上記で作成したイメージだが、このイメージからコンテナを起動すると

```console
$ docker run test/docker-cowsay hoge
 ______
< hoge >
 ------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
$ docker run test/docker-cowsay
 _______________________________________
/ "Life, loathe it or ignore it, you    \
| can't like it."                       |
|                                       |
| -- Marvin, "Hitchhiker's Guide to the |
\ Galaxy"                               /
 ---------------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
```

と処理が分岐される。

# 公式イメージの利用

Dockerには公式イメージが提供されており（記事内で使っていたubuntuも公式イメージの1つ）、Dockerを実行する環境だけあればアプリケーションをホストマシンにインストールすることなく実行することができる。

今回はRedisのイメージを利用する。

```console
$ docker run --name myredis -d redis
Unable to find image 'redis:latest' locally
latest: Pulling from library/redis

6a5a5368e0c2: Pull complete
2f1103ce5ca9: Pull complete
086a40c85e01: Pull complete
9a5e9d112ec4: Pull complete
dadc4b601bb4: Pull complete
b8066982e7a1: Pull complete
2bcdfa1b63bf: Pull complete
Digest: sha256:38e873a0db859d0aa8ab6bae7bcb03c1bb65d2ad120346a09613084b49185912
Status: Downloaded newer image for redis:latest
8d41189f1fab405166e4757fe09b697b09ba7654efd66a13626d395472cc4a61
```

-dオプションを付けてバックグラウンドでRedisコンテナを起動させる。

これに対して接続するクライアントとして、Redisイメージから別のコンテナを立ち上げる

```console
$ docker run --rm -it --link myredis:redis redis /bin/bash
root@ef3efabd8d3f:/data# redis-cli -h redis -p 6379
redis:6379> PING
PONG
redis:6379> set "abc" 123
OK
redis:6379> get "abc"
"123"
redis:6379> exit
root@ef3efabd8d3f:/data# exit
exit
```

これで実際にRedisに接続し、実際にデータを保存することができる。

コンテナ同士が接続についてだが、Dockerは `–link myredis:redis` というオプションでmyredisと名前を付けたコンテナに接続できるようになっており `myredis:redis` という指定はmyredisコンテナにredisというホスト名でネットワークが解決できるようになるという指定をしている。（linkについて詳しくは後述）

# データの永続化

Dockerは基本的にコンテナが終了したらデータが削除されるため、データの永続化はボリュームを使う必要がある。記述方法としてはDockerfileの中でVOLUMEを使うか、docker run に -v オプションを渡すの2通りがある。

VOLUME構文ではセキュリティ上の問題からDockerをインストールしたディレクトリにしかマウントできず、 -v はホストのディレクトリを指定しない場合Dockerをインストールしたディレクトリからマウントされ、ホストのディレクトリを指定した場合はホストの任意のディレクトリを指定することができる。（ -v ホストディレクトリ:コンテナ内のディレクトリ）

```console
$ docker run --name myredis -v /data -d redis
```

```console
$ docker run --rm -it --link myredis:redis redis /bin/bash
root@ef3efabd8d3f:/data# redis-cli -h redis -p 6379
redis:6379> PING
PONG
redis:6379> set "abc" 123
OK
redis:6379> get "abc"
"123"
redis:6379> exit
root@ef3efabd8d3f:/data# redis-cli -h redis -p 6379 save
OK
root@ef3efabd8d3f:/data# exit
exit
```

```console
$ docker run --rm --volumes-from myredis -v $(pwd)/backup:/backup ubuntu cp /data/dump.rdb /backup/
$ ls backup/
dump.rdb
```

これでコンテナの中からRedisのバックアップが取れているのが確認できる。

# ネットワーク

Dockerのコンテナ同士を繋ぐネットワークには非推奨となっている–linkオプションを使うものと、現在推奨されている独自のネットワークを作る方式の2つがある。

## link

`--net` オプションを指定せずデフォルトのネットワークを使った場合、 `-–link` オプションを使ってコンテナ同士を指定して繋ぐことができる。

前述の公式イメージの利用でRedisの公式イメージを使いサーバとクライアントを接続するのに使ったもので、クライアント側から `--link myredis:redis` と書いているのがlinkの指定である。

具体的な処理としては `-–link` オプションを指定したコンテナの中の `/etc/hosts` にmyredisコンテナのIPが記述される。

このために、linkを指定する場合は事前にlinkされる側のコンテナを立ち上げておく必要がある。

## Network

Dockerでは `–-net` で使うネットワークを指定でき、デフォルトではbridgeという名前の仮想ネットワークを利用する。これらのネットワークは docker network ls で確認できる。

```console
$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
a3d6eab2c163        bridge              bridge              local
989fd66a14a8        host                host                local
1a1dceb67c16        none                null                local
```

デフォルトではこの3つが存在しており、 `-–net=host` を指定することでDockerホストのネットワークを利用することも可能である。

ネットワークは `docker network create` コマンドで作成することができ、作成したネットワークを利用することでそのネットワークの中では「コンテナ名」もしくは「コンテナ名.ネットワーク名」の形でホスト名を解決することができる。

これは現在のDocker1.12ではDocker内部で独自に設定されたDNSで解決される。

```console
$ docker network create testnet01
960970f91402b22d8f70976221aef6a70b55c46727a1272e4b44106ef394163d
$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
a3d6eab2c163        bridge              bridge              local
989fd66a14a8        host                host                local
1a1dceb67c16        none                null                local
960970f91402        testnet01           bridge              local
$ docker run --net=testnet01 --name myredis -d redis
f3c088c27494c1144c74eff3ec6f525e497049d2cf66a86addb0e9fa4e11ff69
$ docker run --rm -it --net=testnet01 redis /bin/bash
root@5662a7cea39d:/data# redis-cli -h myredis -p 6379
myredis:6379> PING
PONG
myredis.testnet01:6379> exit
root@5662a7cea39d:/data# redis-cli -h myredis.testnet01 -p 6379
myredis.testnet01:6379> PING
PONG
```

先ほどはlinkを使ってコンテナ同士を繋いでいたが今回はコンテナ名を指定するだけで接続ができている。

以前は `–-link` を使ってコンテナ同士のネットワークを構築するのメジャーであったが、今後はこちらのnetworkを作成する方法が推奨されている。

# Docker Compose

Docker Composeは複数のコンテナを使うアプリケーションを定義・実行するためのツールである。

Docker Composeはアプリケーションのサービスの設定にComposeファイルと呼ばれるyaml形式のファイルを使う。そして、コマンドを実行した際にComposeファイルで設定されたサービスの作成・起動を行う。

Composeは開発環境・テストは当然、CI環境にも適していている（後述）。

`docker-compose.yml` は次のように記述する。

```docker-compose.yml
version: '2'
services:
  web:
    build: .
    depends_on:
      - redis
    ports:
      - "8080:80"
  redis:
    image: redis
```

この例ではカレントディレクトリのDockerfileからイメージのビルドとコンテナの起動、ホストの8080番ポートをコンテナの80番ポートにフォワーディング、そのコンテナに連携するRedisコンテナを起動といった処理を行う。

また、docker-compose v2では起動時に新たなネットワークを自動的に作成するのでComposeファイルに定義したサービス名でそれぞれのコンテナにアクセスできる。

depends_onで依存するサービスを定義でき、この場合はRedisが起動してからwebで指定されたコンテナが立ち上がる。

参考にwebサービスにApacheをインストールしたコンテナを作成して実行する。

```Dockerfile
FROM centos:6.7

RUN yum -y update && yum -y install wget && yum clean all
RUN yum -y install httpd && yum clean all

EXPOSE 80

ENTRYPOINT ["/usr/sbin/httpd"]
CMD ["-D", "FOREGROUND"]
```


```console
$ docker-compose up
```

upコマンドで定義しているコンテナを同時に起動することができる。イメージが存在しない場合は image の場合はネットワークから自動的にダウンロードし、 build の場合は指定した `Dockerfile` を用いてビルドが実行される。

`http://localhost:8080/` でApacheのトップ画面にアクセスができる。また、 `docker network ls` で新たなネットワークができているのが確認できる。
そのネットワークを指定してコンテナを立ち上げることでRedisに接続できるのが確認できる。

```console
$ docker run --rm -it --net=hoge_default redis /bin/bash
root@f29e0596cb01:/data# redis-cli -h redis
redis:6379>PING
PONG
```

立ち上げたDocker Composeは `docker-compose down` で終了する。この際に自動的に作られたネットワークも削除される。

また、`docker-compose run` コマンドを使うことで特定のサービスとそれに依存するコンテナを起動し、特定のサービスのexit codeを受け取ることができる。
これはテストなどに用いると便利である。

テストの際は上記、もしくは

```console
$ docker-compose up -d
$ ./run_tests
$ docker-compose stop
$ docker-compose rm -f
```

などとして実行するのが推奨されている。

# マルチステージビルド

Dockerの17.05 (ce)からmulti-stage buildという機能が追加された。
マルチステージビルドを用いることでDockerfileで複数のイメージの作成を定義・命名し、イメージ間でファイルのコピーなどができるようになる。

ビルド環境とランタイム環境でイメージのサイズが大きく変わるような場合には特に有用で、具体的にはgolangのベースイメージとalpine linuxベースのイメージではイメージのサイズの差が大きいので実行環境でのイメージの容量削減に繋がる。

DockerHubで公開されているgolangの公式イメージはtag 1.9で286MBである。一方alpine linuxのイメージのサイズは2MBであるため、golangのイメージでコンパイルしたソフトウェアのバイナリのみをalpine liuxにコピーすることで数百MB単位でのイメージのサイズの削減が期待できる。

Dockerfileの例としては以下のようになる

```Dockerfile
FROM golang:latest AS build
ADD . /go/src/github.com/yuki-ycino/go_sample
WORKDIR /go/src/github.com/yuki-ycino/go_sample
RUN go build -o app

FROM alpine AS execute
COPY --from=build /go/src/github.com/yuki-ycino/go_sample/app /app
ENTRYPOINT ["/app"]
```

複数の `FROM` をDockerfileに記述し、ASを用いてDockerfile内で扱う名前を定義する。
その後、 `COPY --from=image` と書くことで複数のイメージ間でのファイルのコピーが可能となる。

# Dockerを使ったCIについて

Jenkinsのコンテナを使ってDockerコンテナのCIを回すことが多い（弊社でも利用しており、オライリー本にも載っている）ので紹介する。

プロジェクトに変更がpushされたタイミングでJenkinsがリポジトリのチェックアウト・イメージのビルド・テストの実行を行うよう設定する。

Jenkinsコンテナ内でDockerを実行するにはJenkinsコンテナにDockerホストのソケットをマウントする方法と、Docker in Dockerの2つがあるが、今回は前者について説明する。

ホストマシンのDockerのソケットファイルをDockerコンテナとして起動しているJenkinsコンテナにマウントすることで、JenkinsはホストマシンのDockerを使うことができるようになる。
その状態でJenkins上からホストマシンのDockerを用いてテストを行う。

![docker144_2.png](https://qiita-image-store.s3.amazonaws.com/0/32836/d3196e7e-52f8-943d-f0ef-b67aa8c9591a.png "docker144_2.png")![docker144_1.png](https://qiita-image-store.s3.amazonaws.com/0/32836/0b22e313-2905-ac03-8988-3b3dd4773f3a.png "docker144_1.png")

まず、JenkinsのイメージにDockerをインストールする。
その後Jenkinsのコンテナを起動し、Docker Composeでソケットのマウントやデータの永続化を行う。

```./jenkins/Dockerfile
FROM jenkinsci/jenkins:latest

USER root
 
RUN echo "Asia/Tokyo" > /etc/timezone && dpkg-reconfigure -f noninteractive tzdata

RUN apt-get update && apt-get install -y locales
RUN echo ja_JP.UTF-8 UTF-8 >> /etc/locale.gen && locale-gen && update-locale LANG=ja_JP.UTF-8 LANGUAGE="ja_JP:ja"
ENV LANG ja_JP.UTF-8

RUN curl -sSL https://get.docker.com | sh

USER jenkins
 
ADD plugins.txt /usr/share/jenkins/plugins.txt
RUN /usr/local/bin/plugins.sh /usr/share/jenkins/plugins.txt

USER root
```

また、plugins.txt に必要なプラグインを定義しておいて plugins.sh を実行することで必要なプラグインを事前にインストールすることができる。

```docker-compose.yml
version: '3'
services:
  jenkins:
    restart: always
    build: ./jenkins
    ports:
      - 8080:8080
      - 50000:50000
    volumes:
      - ./docker_mount/jenkins:/var/jenkins_home/
      - /var/run/docker.sock:/var/run/docker.sock
```

これで `docker-compose up` を実行することで、8080番ポートでJenkinsが立ち上がり、CIが可能となる。

Jenkinsのイメージに対してDockerをインストールし、docker-compose.ymlで `/var/run/docker.sock` をコンテナにマウントするよう定義することでコンテナ内でdockerコマンドを実行した際にホストのDockerが実行されるようになっている。

これでDocker化されたポータビリティの高いCI環境を構築することができる。

# イメージのプッシュ
## DockerHub

DockerHubは公式のレジストリサービスであり、Dockerは一般的にDockerHubを使ってイメージの公開を行う。

DockerHubでアカウントを取得すればdocker loginコマンドでサインインでき、その状態で docker push ユーザ名:イメージ名 コマンドを実行することでイメージを公開することができる。

DockerHubでリポジトリの設定でプライベートにする・GitHubが更新されたタイミングで自動的にDockerHubでイメージをビルドするなどといったこともできる。

DockerHub以外にも似たようなサービスにquery.ioなどいったものが存在する。

## レジストリの構築

Dockerのレジストリはregistryイメージを使うことで自分で構築することができる。基本的にAPIはDockerHubと互換性があるものになっている。

デフォルトではGUIがなく、HTTPS化しないとクライアント側からinsecure registriesに登録しないと操作できない。

GUIについては konradkleine/docker-registry-frontend:v2 などいくつか個人が開発した別のイメージで提供されているものが存在する。

# Packer

PackerはDockerとは直接の関係はないが、Vagrantを開発しているHashiCorpが開発しているマシンイメージに対する問題を解決するためのアプリケーションである。

VirtualBox・VMWare・Amazon EC2・DigitalOceanなどのマシンのイメージが必要な環境に対してJSONで設定を記述することで、それぞれのマシンイメージを元となるイメージからプロビジョニングを実行できる。

Packerの対象の1つにDockerがあり、既存のAnsibleなどプロビジョニングソフトウェアを用いてDockerイメージを作成することができる。

ただし、Dockerの利点であるUFSによるキャッシュが効かず、Ansibleでのプロビジョニングは基本的にDockerに対して最適化などをしていないためにイメージのサイズが肥大化してしまう。
試した結果相性はあまりよくないという結果となった。

また、Dockerは基本的に1コンテナ1プロセスで運用し、コンテナ同士がオーケストレーションをしてサービスを構築するという思想のため、Ansibleなどが必要となるほどプロビジョニングが複雑化することはあまりないと考えられる。