その Dockerfile、本当に必要ですか ? マウントで解決できませんか ?

ローカルにインストールされていないプログラミング言語・ミドルウェアなどを使いたいとき、Docker は非常に便利です。
「Docker でサクッと環境構築したい」というとき、Docker をどう使うといいのでしょうか。

Docker を使い慣れていないと「とりあえず Dockerfile を書こう」と思うかもしれません。

しかし、実は **Dockerfile は不要な場合も少なくない**です。
この記事では、マウントを使うことで、不要な Dockerfile を書かず、サクッと環境構築する方法を説明します。

# 例えば - PHP スクリプトを手元で実行したい

「これを手元で実行してほしい」と言われ、PHP スクリプトを渡されたとします。

```hello.php
<?php echo 'Hello World'; ?> 
```

このスクリプトを Docker で実行したいときの環境構築について

* Dockerfile を書く場合
* Dockerfile を書かない場合

の各種手順を比較してみます。

## Dockerfile を書く場合

Docker に慣れていない場合、「Docker を使う」 = 「Dockerfile を書く」と思われるかもしれません。

上記の PHP スクリプトを Dockerfile を書いて実行する手順は、以下のようになります。

### 1. Dockerfile を書く

```dockerfile
FROM php:7.4.0-cli-alpine

COPY hello.php .

ENTRYPOINT ["php", "hello.php"]
```

### 2. コンテナイメージをビルドする

```console
$ docker build -t hello-php .
```

### 3. コンテナを起動する

```console
$ docker run --rm hello-php
Hello World
```

この手順でも確かに実行できるのですが、この方法には以下のような欠点があります。

* **スクリプト 1 つ実行したいだけなのに、手順がやたらと多い**
* PHP スクリプトを変更したら、docker build からやり直すことになる
* PHP のファイル名が変わったら、Dockerfile を書き直し、docker build し直すことになる

正直、これだけ手順が長いと、ローカルに PHP をインストールした方が早いと感じるでしょう。

Dockerfile を書かなければ、これらの欠点は解消します。

## Dockerfile を書かない場合

### 1. docker run する

```console
$ docker run --rm -v "${PWD}":/usr/src/myapp -w /usr/src/myapp php:7.4.0-cli-alpine php hello.php
Hello World
```

実は、PHP のスクリプトを実行するだけであれば、上記の **1 コマンドで OK** です。

このコマンドでは、**マウント機能を使うことで、ホストに置いてある PHP スクリプトをコンテナ内と共有し、それを実行**しています。
Dockerfile を書く場合と比べて、こちらの方がはるかに楽に実行できます。

**既存のイメージそのままで実現できることは、既存のイメージを使ってしまうのが一番楽**なのです。

この方法は、[DockerHub の PHP のページ](https://hub.docker.com/_/php) にも書かれています。

> **Run a single PHP script**
> For many simple, single file projects, you may find it inconvenient to write a complete Dockerfile. In such cases, you can run a PHP script by using the PHP Docker image directly:
> `$ docker run -it --rm --name my-running-script -v "$PWD":/usr/src/myapp -w /usr/src/myapp php:7.4-cli php your-script.php`

## こんな長いコマンドは辛いという場合

長いコマンドはタイピングが辛いと思うかもしれません。
それならば、シェルスクリプトを書いてしまえば OK です。

### 最小限のシェルスクリプト

最小限のシェルスクリプトは、以下のようになります。

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

docker run \
    --rm \
    -v "${PWD}":/usr/src/myapp \
    -w /usr/src/myapp \
    php:7.4.0-cli-alpine \
    php hello.php
```

実行は簡単です。

```console
$ ./docker_php.sh
Hello World
```

### 任意のコマンドを実行可能にしたシェルスクリプト

または以下のようにして、任意のコマンドを引数として与えるようにしてもいいかもしれません。

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

docker run \
    --rm \
    -v "${PWD}":/usr/src/myapp \
    -w /usr/src/myapp \
    php:7.4.0-cli-alpine \
    php "$@"
```

```console
$ ./docker_php.sh hello.php
Hello World
```

このようにシェルスクリプト化すれば、実行も簡単ですし、チーム内での共有も容易になります。

## Docker Compose を使う場合

シェルスクリプトでは、人によって書き方も変わってしまいます。
docker run コマンドのオプションをまとめるには、Docker Compose を使う手もあります。

### 最小限の docker-compose.yaml

```docker-compose.yaml
version: '3'
services:
  php:
    image: php:7.4.0-cli-alpine
    command: php hello.php
    volumes:
      - "${PWD}:/usr/src/myapp"
    working_dir: /usr/src/myapp
```

```console
$ docker-compose up
Creating network "php_default" with the default driver
Creating php_php_1 ... done
Attaching to php_php_1
php_1  | Hello World
php_php_1 exited with code 0
```

Docker Compose は「複数のコンテナを一気に起動するために使うもの」と思われているかもしれませんが、上記のように **docker-compose.yaml に docker run コマンドのオプションをまとめておく**という使い方もできます。

### 任意のコマンドを実行可能にした docker-compose.yaml

シェルスクリプトの場合のように実行時に command を切り替えたい場合、環境変数を使えば実現できます。

```docker-compose.yaml
version: '3'
services:
  php:
    image: php:7.4.0-cli-alpine
    command: php "${ARGS}"
    volumes:
      - "${PWD}:/usr/src/myapp"
    working_dir: /usr/src/myapp
```

```console
$ ARGS='hello.php' docker-compose up
Creating network "php_default" with the default driver
Creating php_php_1 ... done
Attaching to php_php_1
php_1  | Hello World
php_php_1 exited with code 0
```

# ここまでのまとめ

このように、マウント機能を使うことで、面倒な Dockerfile を書かずともコンテナを利用することができました。

**「Docker を使う」 = 「Dockerfile を書く」ではない**のです。

# マウント機能の他の使い方

ここから、マウント機能の使い方をもう少し紹介していきます。

## MySQL の公式イメージ + マウント機能

ローカル環境で MySQL などのコンテナを起動する場合も、マウントを上手に使うと便利です。

有名かもしれませんが、MySQL の公式イメージには、「起動時にコンテナ内の /docker-entrypoint-initdb.d というディレクトリ内の SQL などを実行する」という機能があります。

このディレクトリをマウントしてやれば、起動時にホストにある SQL ファイルなどを実行できるのです。

例えば、以下のように docker-compose.yaml を記述します。

```docker-compose.yaml
version: '3'
services:

  mysql:
    image: mysql:5.7.29
    ports:
      - 3306:3306
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: database
      MYSQL_USER: user
      MYSQL_PASSWORD: password
    volumes:
      - "${PWD}/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d"
```

逆に、Dockerfile を使って /docker-entrypoint-initdb.d ディレクトリに SQL ファイルを配置するようにすると、コンテナに入れたいデータが変わるたびに、イメージからビルドし直さないといけなくなってしまいます。

Dockerfile を書いた方がいい場面もあるかもしれませんが、マウントの方が楽なことも多いのではないでしょうか。

## 使用するプログラミング言語のライブラリがある場合

Dockerfile を書かず、マウントを駆使して環境構築する方法は、各種プログラミング言語のライブラリを扱う場合に特に便利だったりします。

例えば、JavaScript の開発環境を構築するとしましょう。

以下の docker-compose.yaml を作成すれば、`docker-compose up` のコマンド 1 つで開発環境が起動します。

```docker-compose.yaml
version: '3'
services:
  node:
    image: node:13.12.0-alpine3.10
    volumes:
      - "${PWD}:/usr/src/myapp"
    working_dir: /usr/src/myapp
    command: sh -c 'npm install && npm start'
```

この方法であれば、コンテナを破棄しても、ダウンロードしたライブラリがホストに残り、ライブラリ追加時も差分だけがダウンロードされます。

※ もちろん、Docker Compose ではなく docker run コマンドを使っても同じです。

## マルチステージビルドとマウント

ライブラリのキャッシュといえば、「マルチステージビルドを使った docker build のたびにライブラリが大量にダウンロードされて困る」ということもあるのではないでしょうか。

これもマウントで解決することができます。

### 普通にマルチステージビルドすると ...

様々な書籍や記事でマルチステージビルドがオススメされていますが、通常のマルチステージビルドではマウントが使えないため、アプリケーションのビルド時に毎回全てのライブラリがダウンロードされてしまいます。

アプリケーションをコンテナ上でビルドするのはいいのですが、ライブラリを毎回ダウンロードするのは面倒です。

アプリケーションのビルドをコンテナ上で実行しつつ、ライブラリのキャッシュも駆使する方法は、大きく 2 つあります。

### 解決策 1. アプリケーションのビルドは docker run で実行する

解決策の 1 つ目は、マルチステージビルドを使わないことです。

代わりに、アプリケーションのビルドは Dockerfile に記述せず、dokcer run で実行します。

例えば Node.js の場合以下のようになります。

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

docker run \
    --rm \
    -v "${PWD}":/usr/src/myapp \
    -w /usr/src/myapp \
    node:13.12.0-alpine3.10 \
    sh -c 'npm install && npm run build'
```

このビルド成果物がホストの例えば dist ディレクトリにできているので、それを Dockerfile で COPY すれば OK です。

### 解決策 2. BuildKit + マルチステージビルドを使う

別の解決策として、BuildKit を使うという方法があります。

BuildKit を使えば、マルチステージビルドの中でホストのファイルをマウントすることができます。

[こちらの記事](https://qiita.com/os1ma/items/f4ce2bb947680be8fdd9) にサンプルがあります。

# マウント活用の注意事項

ここまでマウントをうまく使うと便利という説明をしてきましたが、いくつか注意事項があります。

## 動作保証性

マウント機能を使う場合、コンテナからホストへの依存が強くなり、コンテナの「どこでも動く」という性質が弱まります。

例えば、以下のような現象に遭遇したことがあります。

* ホスト側のファイルの権限の影響で、コンテナ内でファイルがうまく操作できない
* OS に依存するライブラリをマウントしてしまい、そのライブラリがコンテナ内で動かない

「Mac では動くのに Windows では動かない」といったこともありました。

## セキュリティ

「既存のイメージそのままで実現できることは、既存のイメージを使ってしまうのが一番楽」と書きましたが、DockerHub にあるイメージが全て信用できる訳ではありません。

たとえ公式のイメージであっても脆弱性が含まれることはよく指摘されています。

参考

- [気軽に使えるContainerの脆弱性スキャンツール Trivy を試してみた](https://tech.recruit-mp.co.jp/infrastructure/continuous-integration-vulnerability-detection-tool-trivy/)

## 本番環境

本番環境でコンテナを使う場合、マウントで環境構築する手法はオススメしません。

本番環境でマウントを使ってコンテナにアプリケーションを注入しようとすると、アプリケーションが入った EBS などを作成し、それをマウントすることになります。
EBS を作るより、アプリケーション入りのコンテナイメージを作る方が楽です。

また、本番環境でコンテナを使うメリットの 1 つは、「ビルドした時点で諸々インストールされているため、動作保証しやすい」ということです。
アプリケーションをマウントする構成では、コンテナの外にあるデータによってコンテナの挙動が大きく変わるため、このメリットが損なわれます。

開発環境ではその逆で、コンテナの外からサクッとコンテナの挙動を変えたいからこそ、マウントを使うというわけです。

# まとめ

本番環境でコンテナを動かす場合、ほぼ確実に Dockerfile を書くことになります。
しかし、**ローカルでちょっと何かを動かすくらいなら、必ずしも Dockerfile を書く必要はありません**。

**マウントで工夫してみると、より楽に環境構築できることも多い**です。

個人的に、「**ローカルで Docker を使おうとして Dockerfile を書き始めそうになったら、もっと良い方法を考えてみる**」ことにしています。

Docker は便利ですが、使い方によっては面倒になることもあります。少しでも楽に使っていきましょう。