Docker入門 ～Dockerfile編～

# Dockerでローカル開発環境を構築してみよう

XAMPPを使われている方は、ApacheやMySQLを立ち上げる際にStartボタンをぽちっとしていると思います。
Dockerは、そのApacheやMySQLを**コンテナ**という単位で管理して、立ち上げたり色々カスタマイズできたりします。
コンテナは**イメージ**を元に作成され、イメージは**Dockerfile**を元にします。

## なぜDockerを使うのか

色々あると思いますが、まずは環境をいろいろな場所に持ち運びやすくする、ことだと思います。
CircleCIでは、ビルド・テストなどを行う環境としてイメージを指定できます。AWS ECRにイメージを追加しECSでコンテナを起動すれば、それを本番環境にすることもできます。Dockerfileを渡せば他の人に共有することも簡単です。
ローカル開発環境を作ることは、その第一歩だと思います。

# Dockerfileはベースがある

以下は例として使用するDockerfileです。PHPとApacheの環境を作ります。

```:Dockerfile
FROM php:7.3.10-apache

RUN apt-get update \
  && apt-get upgrade -y \
  && apt-get install -y --no-install-recommends \
    curl sendmail zip unzip libz-dev libpq-dev libzip-dev \
    libfreetype6-dev libjpeg62-turbo-dev libpng-dev libssl-dev libmcrypt-dev \
  && rm -rf /var/lib/apt/lists/*

RUN docker-php-ext-install -j$(nproc) pdo_mysql \
  && docker-php-ext-install -j$(nproc) pdo_pgsql \
  && docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \
  && docker-php-ext-install -j$(nproc) gd \
  && docker-php-ext-configure zip --with-libzip \
  && docker-php-ext-install -j$(nproc) zip \
  && pecl install redis \
  && docker-php-ext-enable redis

ENV APACHE_DOCUMENT_ROOT /var/www/public

COPY ./ssl /etc/apache2/ssl

RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \
  && sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf \
  && sed -ri -e 's!/etc/ssl/certs/ssl-cert-snakeoil.pem!/etc/apache2/ssl/localhost+3.pem!g' /etc/apache2/sites-available/default-ssl.conf \
  && sed -ri -e 's!/etc/ssl/private/ssl-cert-snakeoil.key!/etc/apache2/ssl/localhost+3-key.pem!g' /etc/apache2/sites-available/default-ssl.conf \
  && a2enmod rewrite \
  && a2enmod headers \
  && a2enmod ssl \
  && a2ensite default-ssl

RUN echo '#!/bin/sh\nphp artisan "$@"' >> /bin/pa \
  && chmod u+x /bin/pa

WORKDIR /var/www
```

このDockerfileにはベースとしているイメージ（のDockerfile）があります。
https://github.com/docker-library/php/blob/master/7.3/stretch/apache/Dockerfile

Dockerfileはベースとなるものに追記したり上書きしたりしてカスタマイズしていきます。

# Dockerfile詳細

```:Dockerfile
FROM php:7.3.10-apache
```

`FROM <image>`でベースイメージを指定しています。:7.3.10-apacheは**タグ**で、バージョン指定に使います。
タグを指定しないとlatestというタグになりますが、ビルドする時期によってバージョンが変わってしまうためよくありません。

```:Dockerfile
RUN apt-get update \
  && apt-get upgrade -y \
  && apt-get install -y --no-install-recommends \
    curl sendmail zip unzip libz-dev libpq-dev libzip-dev \
    libfreetype6-dev libjpeg62-turbo-dev libpng-dev libssl-dev libmcrypt-dev \
  && rm -rf /var/lib/apt/lists/*
```

`RUN <command>`でコマンドを実行します。ここではPHP extensionのインストールに必要なライブラリをインストールしています。
インストールが終わったら、不要なファイルを`rm -rf /var/lib/apt/lists/*`で削除しています。

```:Dockerfile
RUN docker-php-ext-install -j$(nproc) pdo_mysql \
  && docker-php-ext-install -j$(nproc) pdo_pgsql \
  && docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \
  && docker-php-ext-install -j$(nproc) gd \
  && docker-php-ext-configure zip --with-libzip \
  && docker-php-ext-install -j$(nproc) zip \
  && pecl install redis \
  && docker-php-ext-enable redis
```

PHP Extensionをインストールしています。ベースイメージが用意している`docker-php-ext-install`コマンドなどを使用しています（詳細は後述）。

```:Dockerfile
ENV APACHE_DOCUMENT_ROOT /var/www/public
```

`ENV <name> <value>`でコンテナ内で使える環境変数を設定します。もし同じ環境変数を指定すると、上書きします。
ここでは環境変数APACHE_DOCUMENT_ROOTに/var/www/publicを代入しています。

```:Dockerfile
COPY ./ssl /etc/apache2/ssl
```

`COPY <source> <target>`で、ホストにあるファイルをコンテナ内にコピーします。
ここでは./sslをコンテナ内の/etc/apache2/sslとしてコピーしています。localhostでもhttpsでアクセスできるようにするため、sslディレクトリには鍵と証明書が入っています。
参考：https://qiita.com/walkers/items/b90a97a99bbb27f6550f

```:Dockerfile
RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \
  && sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf \
  && sed -ri -e 's!/etc/ssl/certs/ssl-cert-snakeoil.pem!/etc/apache2/ssl/localhost+3.pem!g' /etc/apache2/sites-available/default-ssl.conf \
  && sed -ri -e 's!/etc/ssl/private/ssl-cert-snakeoil.key!/etc/apache2/ssl/localhost+3-key.pem!g' /etc/apache2/sites-available/default-ssl.conf \
  && a2enmod rewrite \
  && a2enmod headers \
  && a2enmod ssl \
  && a2ensite default-ssl
```

sedコマンドを使ってApache設定ファイルの書き換えをします。ここではドキュメントルートを/var/www/publicへ変更と、先ほどCOPYしたssl鍵や証明書の指定を行なっています。
例えば`'s!/var/www/html!${APACHE_DOCUMENT_ROOT}!g'`は「/var/www/html」を環境変数APACHE_DOCUMENT_ROOTの値に置換しており、`/etc/apache2/sites-available/*.conf`は置換するファイルを指定しています。
a2enmodコマンドはモジュールの有効化、a2ensiteコマンドはdefault-sslという設定ファイルを有効化しています。

```:Dockerfile
RUN echo '#!/bin/sh\nphp artisan "$@"' >> /bin/pa \
  && chmod u+x /bin/pa
```

php artisanコマンドをpaと短くうてるようにしています。
aliasコマンドを使用していないのは、ホストから`docker exec pa`と利用できるようにするためです（aliasはコンテナ内のみで有効のため）。

```:Dockerfile
WORKDIR /var/www
```

`WORKDIR <path>`で作業ディレクトリを指定します。以後、RUN, COPY, CMDなどがここで実行されるようになります。
ここではコンテナのbashを利用する際に、カレントディレクトリが/var/wwwになるように、最後ですがWORKDIRを指定しています。

---

このDockerfileにはありませんが、`CMD`という命令があります。ベースにしたDockerfileには以下があります。

```:Dockerfile
CMD ["apache2-foreground"]
```

コンテナを立ち上げると、CMDに記述したコマンドが実行されます。
CMDは複数書かれると、一番最後に書かれたものが優先されます。自分のDockerfileでベースイメージで指定されたCMDを上書きできるということです。

# ベースイメージが用意しているコマンド

ベースにしている[phpイメージのDockerHubページ](https://hub.docker.com/_/php)を見てみます。

> We provide the helper scripts docker-php-ext-configure, docker-php-ext-install, and docker-php-ext-enable to more easily install PHP extensions.

とあります。PHP extensionをインストールするときに便利なコマンドを用意している、とのことです。
説明しているDockerfileでも使用しています。

```:Dockerfile
RUN docker-php-ext-install -j$(nproc) pdo_mysql \
  && docker-php-ext-install -j$(nproc) pdo_pgsql \
  ...
```

このように便利なコマンドを準備していたり、カスタマイズする際に使う環境変数を説明してくれていたりするので、DockerHubのページを見るようにしましょう。 

# 信頼できるイメージ

ベースにするイメージは、公式イメージのみにした方が無難です。
![Web 1920 – 1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230795/a5fa8d90-2113-e146-19e0-c2bf6cccf035.jpeg)

公式イメージはDocker社が品質をチェックしてくれています。公式イメージ以外は悪意のあるソフトや脆弱性がある場合があります。

# 実際にコンテナを立ち上げよう

上記のDockerfileで、実際にコンテナを立ち上げてみます。

まず`image build <path>`コマンドで、Dockerfileからイメージをビルドします。カレントディレクトリにDockerfileをおき、以下のコマンドをうちます。

```
$ docker image build -t my-php-apache:7.3.10 .
```

`-t my-php-apache:7.3.10`はオプションで、my-php-apache:7.3.10というイメージをビルドします。`.`でDockerfileのパスを指定しています。

ビルドが終わったら、`container run <image>`コマンドで立ち上げてみます。`-p 80:80`で、ホストの80ポートでコンテナの80ポートにアクセスできるようにしています。
ホストにApacheを立ち上げるXAMPPとは違いコンテナは仮想環境ですから、ホストとコンテナのポートをバインディングして（繋げて）あげます。

```
$ docker container run -p 80:80 my-php-apache:7.3.10
```

ブラウザでlocalhostにアクセスしてみると、403エラーです。
<img width="494" alt="1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230795/f31a2c63-acc8-0888-a451-a6c364144535.png">
上記のDockerfileではApacheのドキュメントルートを/var/www/publicに変更していました。コンソールを見ると、

```
Warning: DocumentRoot [/var/www/public] does not exist
```

とありますから、403エラーではあるもののApacheが動いていることが確認できます。

コンテナ内にログインして、publicディレクトリを作ってみましょう。まず起動したコンテナの情報をみます。`container ls`コマンドはコンテナの情報をみることができます。

```
$ docker container ls
CONTAINER ID        IMAGE                  COMMAND                  CREATED             STATUS              PORTS                NAMES
346587eac502        my-php-apache:7.3.10   "docker-php-entrypoi…"   10 seconds ago      Up 9 seconds        0.0.0.0:80->80/tcp   bold_johnson
```

コンテナのIDがわかったので、`container exec <containerId> <command>`コマンドでコンテナ内のコマンドを実行します。`-it`はホストとコンテナの入出力を繋げています。

以下は、containerIdを自分のコンテナIDにして実行ください。

```
$ docker container exec -it 346587eac502 bash
root@346587eac502:/var/www# 
```

コンテナのbashにログインできました。publicディレクトリを作り、中にhtmlファイルを置いてみます。

```
root@346587eac502:/var/www# mkdir /var/www/public
root@346587eac502:/var/www# echo '<p>test</p>' > /var/www/public/index.html
```

もう一度ブラウザでlocalhostをみてみると・・・

![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230795/07e687fb-3066-afd4-0b79-283339c58cae.png)

ちゃんと動いてます！

## コンテナライフサイクル

コンテナは色々な状態があり、詳しくは
[Docker入門 コンテナのライフサイクル](https://qiita.com/mtakehara21/items/08b6a3f0a7b44588ed94)
によくまとまっていますが、簡易化すると以下の図のようになります。

![docker.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230795/cf779dfe-9403-4ba4-7c00-f8d2e45751c4.png)

現在my-php-apacheコンテナは起動中です。なので停止〜削除してみます。

```
$ docker container stop 346587eac502
```

ブラウザでlocalhostをみても、Apacheからレスポンスが返ってきません。`docker container ls -a`で状態を確認すると、STATUSがExitedになっています。`-a`は停止しているコンテナも表示するオプションです。

さらに削除してみましょう。

```
$ docker container rm 346587eac502
```

`docker container ls -a`をうってもコンテナの情報が出てきません。削除が完了しました。
これで、起動〜削除まで一通りこなせました。

# 常時実行するコンテナ、しないコンテナ

話は変わりますが、ApacheやMySQLコンテナは、リクエストを待ち受けてもらわなければいけません。よってコンテナはずっと立ち上がっている状態にします。
しかし用途によっては実行結果を受け取ったら、もうコンテナを削除してもよい場合もあります。例えばcomposerをホストにインストールせずに使ってみましょう。composerコマンドを使う時だけcomposerコンテナはあればいいのですから、以下のコマンドで立ち上げます。

```
$ docker container run --rm -it -v $PWD:/app composer install
```

`--rm`は実行後にコンテナをすぐ削除します。
`-v`は**ボリューム**を指定します。詳しくは次回で説明しますが、ホストとコンテナでデータを共有する仕組みです。

composer.jsonがあるディレクトリで、上記のコマンドを実行してみましょう。

```
$ docker container run --rm -it -v $PWD:/app composer install
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 84 installs, 0 updates, 0 removals
  - Installing symfony/polyfill-ctype (v1.12.0): Downloading (100%)
  - Installing phpoption/phpoption (1.5.0): Downloading (100%)
...
Package manifest generated successfully.
```

composer.lockやvendorディレクトリがホスト側に現れ、インストールされていることがわかります。以下のような流れでcomposer installを実行できました。

1. composerがインストールされたコンテナが立ち上がる
2. ボリュームで$PWDのファイルがコンテナの/appに共有される
3. コンテナ内で`composer install`が実行される
4. インストールされた結果が$PWDに共有される
5. composer installコマンド終了後、コンテナが削除される。

## イメージの指定はどこ？

`docker container run --rm -it -v $PWD:/app composer install`はどこでイメージを指定しているの？と思われるカモですが、`composer`がイメージ名です。
ややこしいので割り切って説明しますが、composerイメージで`docker container run composer composer install`と**[打たなくて済むように](https://github.com/composer/docker/blob/8595a2016b538e0f6945b8c2113ec85dcf04868c/1.9/docker-entrypoint.sh)**してくれています。

# コマンドが違う？

他のページでは`docker ps`コマンドを使っている人もいます。`docker ps`と打つと、`docker container ls`と同じく起動中のコンテナ一覧が表示されます。`docker exec`と`docker container exec`も同じです。
これは操作する対象が分かりやすいように、ということで後から長いコマンドが追加されました。

参考
**[docker container / image コマンド新旧比較](https://qiita.com/zembutsu/items/6e1ad18f0d548ce6c266)**

個人的には新しい（長い）コマンドの方が統一感があって好きなのですが、タイプ数が少ない古いコマンドも便利なので、主要なものは両方覚えちゃいましょう。

# 終わりに

今回は簡単なDockerfileとdockerコマンドの紹介でした。
次回は複数のコンテナを管理するDocker-composeと、Dockerボリューム・ネットワークを紹介予定です。
->**[Docker入門 〜Docker-compose, ネットワーク, ボリューム編〜](https://qiita.com/takaday/items/51553b9167fcd4789d61)**

:christmas_tree: FORK Advent Calendar 2019
:arrow_left: 16日目 [【WordPress】今更だけど、WysiWygエディタ（ビジュアルエディタ）で書いた記事にCSSを適用する方法をまとめてみた話](https://qiita.com/Kodak_tmo/items/3791b08cbaa2c46ec5bb)  @Kodak_tmo
:arrow_right: 18日目 [今年の正月はNuxtとFirebaseで作ったカードゲームで甥っ子に尊敬されたい](https://qiita.com/yoh_zzzz/items/377f5ddb922c17da73b3) @yoh_zzzz