ローカルにインストールされていないプログラミング言語・ミドルウェアなどを使いたいとき、Docker は非常に便利です。
「Docker でサクッと環境構築したい」というとき、Docker をどう使うといいのでしょうか。
Docker を使い慣れていないと「とりあえず Dockerfile を書こう」と思うかもしれません。
しかし、実は Dockerfile は不要な場合も少なくないです。
この記事では、マウントを使うことで、不要な Dockerfile を書かず、サクッと環境構築する方法を説明します。
例えば - PHP スクリプトを手元で実行したい
「これを手元で実行してほしい」と言われ、PHP スクリプトを渡されたとします。
<?php echo 'Hello World'; ?>
このスクリプトを Docker で実行したいときの環境構築について
- Dockerfile を書く場合
- Dockerfile を書かない場合
の各種手順を比較してみます。
Dockerfile を書く場合
Docker に慣れていない場合、「Docker を使う」 = 「Dockerfile を書く」と思われるかもしれません。
上記の PHP スクリプトを Dockerfile を書いて実行する手順は、以下のようになります。
1. Dockerfile を書く
FROM php:7.4.0-cli-alpine
COPY hello.php .
ENTRYPOINT ["php", "hello.php"]
2. コンテナイメージをビルドする
$ docker build -t hello-php .
3. コンテナを起動する
$ docker run --rm hello-php
Hello World
この手順でも確かに実行できるのですが、この方法には以下のような欠点があります。
- スクリプト 1 つ実行したいだけなのに、手順がやたらと多い
- PHP スクリプトを変更したら、docker build からやり直すことになる
- PHP のファイル名が変わったら、Dockerfile を書き直し、docker build し直すことになる
正直、これだけ手順が長いと、ローカルに PHP をインストールした方が早いと感じるでしょう。
Dockerfile を書かなければ、これらの欠点は解消します。
Dockerfile を書かない場合
1. docker run する
$ 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 のページ にも書かれています。
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 です。
最小限のシェルスクリプト
最小限のシェルスクリプトは、以下のようになります。
#!/bin/bash
docker run \
--rm \
-v "${PWD}":/usr/src/myapp \
-w /usr/src/myapp \
php:7.4.0-cli-alpine \
php hello.php
実行は簡単です。
$ ./docker_php.sh
Hello World
任意のコマンドを実行可能にしたシェルスクリプト
または以下のようにして、任意のコマンドを引数として与えるようにしてもいいかもしれません。
#!/bin/bash
docker run \
--rm \
-v "${PWD}":/usr/src/myapp \
-w /usr/src/myapp \
php:7.4.0-cli-alpine \
php "$@"
$ ./docker_php.sh hello.php
Hello World
このようにシェルスクリプト化すれば、実行も簡単ですし、チーム内での共有も容易になります。
Docker Compose を使う場合
シェルスクリプトでは、人によって書き方も変わってしまいます。
docker run コマンドのオプションをまとめるには、Docker Compose を使う手もあります。
最小限の 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
$ 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 を切り替えたい場合、環境変数を使えば実現できます。
version: '3'
services:
php:
image: php:7.4.0-cli-alpine
command: php "${ARGS}"
volumes:
- "${PWD}:/usr/src/myapp"
working_dir: /usr/src/myapp
$ 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 を記述します。
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 つで開発環境が起動します。
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 の場合以下のようになります。
#!/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 を使えば、マルチステージビルドの中でホストのファイルをマウントすることができます。
こちらの記事 にサンプルがあります。
マウント活用の注意事項
ここまでマウントをうまく使うと便利という説明をしてきましたが、いくつか注意事項があります。
動作保証性
マウント機能を使う場合、コンテナからホストへの依存が強くなり、コンテナの「どこでも動く」という性質が弱まります。
例えば、以下のような現象に遭遇したことがあります。
- ホスト側のファイルの権限の影響で、コンテナ内でファイルがうまく操作できない
- OS に依存するライブラリをマウントしてしまい、そのライブラリがコンテナ内で動かない
「Mac では動くのに Windows では動かない」といったこともありました。
セキュリティ
「既存のイメージそのままで実現できることは、既存のイメージを使ってしまうのが一番楽」と書きましたが、DockerHub にあるイメージが全て信用できる訳ではありません。
たとえ公式のイメージであっても脆弱性が含まれることはよく指摘されています。
参考
本番環境
本番環境でコンテナを使う場合、マウントで環境構築する手法はオススメしません。
本番環境でマウントを使ってコンテナにアプリケーションを注入しようとすると、アプリケーションが入った EBS などを作成し、それをマウントすることになります。
EBS を作るより、アプリケーション入りのコンテナイメージを作る方が楽です。
また、本番環境でコンテナを使うメリットの 1 つは、「ビルドした時点で諸々インストールされているため、動作保証しやすい」ということです。
アプリケーションをマウントする構成では、コンテナの外にあるデータによってコンテナの挙動が大きく変わるため、このメリットが損なわれます。
開発環境ではその逆で、コンテナの外からサクッとコンテナの挙動を変えたいからこそ、マウントを使うというわけです。
まとめ
本番環境でコンテナを動かす場合、ほぼ確実に Dockerfile を書くことになります。
しかし、ローカルでちょっと何かを動かすくらいなら、必ずしも Dockerfile を書く必要はありません。
マウントで工夫してみると、より楽に環境構築できることも多いです。
個人的に、「ローカルで Docker を使おうとして Dockerfile を書き始めそうになったら、もっと良い方法を考えてみる」ことにしています。
Docker は便利ですが、使い方によっては面倒になることもあります。少しでも楽に使っていきましょう。