本連載では、Docker に興味はありつつもまだ触ったことのない方向けに、実際に Docker を触って理解していただくための記事を提供します。今回は Docker のバインドマウント機能について説明します。また、バインドマウントの具体的な例として、Java の Hello World プログラムをコンパイルし実行する方法を紹介します。
Docker のマウント機能
「マウント」という用語については、Linux や Windows など OS の用語としてご存知の方が多いかと思います。例えば「ハードディスクをマウントする」と言った場合、OS がハードディスクを認識し、利用できる状態にすることを意味します。
Docker における「マウント」とは、コンテナの外にあるデータを、コンテナの中で利用できる状態にすることを意味します。昨年末に公開された Docker v17.12.0-ce では、以下の三種類のマウント機能が提供されています。
- バインドマウント (bind)
- ボリュームマウント (volume)
- 一時ファイルシステムマウント (tmpfs)
今回説明する1番のバインドマウントは、Dockerホストのファイルやディレクトリを利用できるようにする機能です。バインドマウントを行うと、コンテナの外にある Dockerホストのファイルを、コンテナの中から読み書きできるようになります。バインドマウントしたファイルは、「/home」や「/var」のようなパスを用いて、もとからコンテナの中にあるファイルと同じように利用できるのです。
2番のボリュームマウントは、Dockerホストのファイルやディレクトリのうち、Dockerが管理しているものを利用できるようにする機能です。3番の一時ファイルシステムマウントは、Dockerホストにファイルとして保存したくないデータを一時的に利用できるようにする機能です。どちらも今回詳細は説明しませんが、データの利用目的に応じてそれぞれを使い分けるとよいでしょう。
コンテナの隔離性 (isolation)
バインドマウントは Docker の初期の頃から提供されている機能であり、現在でも非常によく利用されています。バインドマウントが必要とされる理由は、コンテナの特徴の一つである隔離性 (isolation) と強く関係しています。
通常、コンテナはそのコンテナの中にあるデータだけを利用できます。あるコンテナから、別のコンテナの中にあるデータや、Dockerホストのデータを利用することはできません。例えば、コンテナの中で ls /home
というコマンドを実行しても、出力されるのはコンテナの中の /home
の情報だけです。別のコンテナの /home
や、Dockerホストの /home
の情報はまったく出力されません。そのような性質を、コンテナの隔離性といいます。
コンテナはファイルやディレクトリを隔離するだけではなく、プロセスやネットワークなども隔離してくれます。Docker が仮想化技術の一種と言われるのは、そのように様々なものを隔離し、仮想的に独立した環境を作り出してくれるためです。
隔離性は、Dockerにとって非常に重要な性質です。隔離性を高めることには、Dockerホストなど周辺環境への依存度を低下させるなど、様々な利点があります。しかしその逆に、隔離性が高いことで不便を感じる場合もあります。
例えば、今回取り上げる Java の Hello World プログラムのように、ちょっとしたファイルを作成してコンテナで利用する場合を考えてみましょう。Dockerホストにファイルを作成した場合、コンテナの中からはそのファイルを読み書きできません。コンテナの中でファイルを作成すれば読み書きは当然できますが、普段使いなれたテキストエディタなどがコンテナの中にあるとは限りません。また、コンテナの中で作成したファイルは、コンテナを削除すると一緒に削除されてしまいます。削除される前に docker container cp
コマンドを実行し、ファイルをコンテナの外にコピーすることもできますが、なんだか面倒な感じがします。
そのような状況を、バインドマウントは隔離性を下げることで解消してくれます。通常はコンテナの中から読み書きできない Dockerホストのファイルを、バインドマウントしたものに限り、特別に読み書きできるようにしてくれるのです。
事前準備
ではここからは Java の Hello World プログラムを例に、バインドマウントの具体的な使い方について見ていきましょう。概要は以下のとおりです。
- 事前準備
- 隔離性の実験:バインドマウントしないとどうなるか
- バインドマウントの基本:mountオプション
- Java のコンパイル: mountオプション
- Java の実行: workdirオプション と mountオプション
- alias の活用
- Windows 上のファイルのバインドマウント
- volume オプション
まずは事前準備として、Dockerホストを用意してください。本記事の内容は、Docker Machine v0.12.2 を用いて Windows 7 上の VirtualBox に作成した Dockerホストで検証しています。Docker Machine の使い方については、第5回を参考にしてください。
Docker のバージョンは v17.06 以上としてください。このあと --mount
というオプションを使用するためです。本記事の Docker のバージョンは v17.12.0-ce です。
Dockerホストが用意できたら、Java のコンテナを動かしてみましょう。今回は Docker の公式イメージである openjdkイメージ を利用します。
以下のコマンドを実行し、Java のバージョンを確認してください。
$ docker run --rm openjdk:9 java --version openjdk 9.0.1 OpenJDK Runtime Environment (build 9.0.1+11-Debian-1) OpenJDK 64-Bit Server VM (build 9.0.1+11-Debian-1, mixed mode)
openjdk:9
イメージから作成されたコンテナの中で、java --version
というコマンドが実行され、バージョン情報が出力されました。このように今回は Docker の公式イメージである openjdkイメージ のうち、JDK のバージョン9系が含まれているイメージを利用します。
上記のコマンドで --rm
オプションを指定していることに注目してください。--rm
は、コンテナが停止したら自動的に削除するオプションです。このあと何度も Java コンテナを作成・起動するのですが、何もしなければコンテナは次々と溜まっていく一方ですので、このように --rm
オプションで自動的に削除します。
最後に、Dockerホストに以下のような Javaファイルを作成してください。
public class Hello { public static void main(String[] args) { System.out.println("Hello, World"); } }
これを /home/docker/Hello.java
として保存します。繰り返しになりますが、この「Hello.java」は Dockerホスト上に作成してください。
事前準備はこれで完了です。
隔離性の実験:バインドマウントしないとどうなるか
それでは、まずはじめにバインドマウントしないとどうなるのか、隔離性とはどういったものなのか確かめてみましょう。
以下のコマンドを実行してみてください。
$ ls /home/docker/Hello.java /home/docker/Hello.java $ docker run --rm openjdk:9 ls /home/docker/Hello.java ls: cannot access '/home/docker/Hello.java': No such file or directory
1つ目の ls
は Dockerホストで実行していますので、Dockerホストの /home/docker
ディレクトリにある Hello.java
が見えています。
2つ目の ls
はコンテナの中で実行していますので、コンテナの中の /home/docker
ディレクトリを探しにいきます。コンテナの中のディレクトリは隔離されているので、Dockerホストの Hello.java
は見つかりません。(そもそも openjdkイメージから作成したコンテナには、/home
ディレクトリはありますが、/home/docker
ディレクトリはありません。)
さらに実験してみましょう。今度は cat
コマンドを実行して、Hello.java
の中身を出力してみてください。
$ cat /home/docker/Hello.java public class Hello { public static void main(String[] args) { System.out.println("Hello, World"); } } $ docker run --rm openjdk:9 cat /home/docker/Hello.java cat: /home/docker/Hello.java: No such file or directory
やはり2つ目の cat
は、コンテナの中で実行しているため、Dockerホストの Hello.java
を見つけられませんでした。
最後に、コンテナで Java のコンパイルができるか試してみましょう。
$ docker run --rm openjdk:9 javac Hello.java javac: file not found: Hello.java Usage: javac <options> <source files> use --help for a list of possible options
やはりエラーが出力されました。コンテナの javac
コマンドを実行し、Hello.java
をコンパイルしようとしたのですが、Dockerホストの Hello.java
が見つけられませんでした。
せっかく公式イメージから JDK のコンテナを簡単に作成できるのに、このままでは Hello.java
をコンパイルできません。
バインドマウントの基本:mountオプション
それでは、バインドマウントを用いて Hello.java
をコンテナの中で利用できるようにしましょう。
バインドマウントの設定は、--mount
オプションで行います。以下のように、バインドマウントを設定して cat
コマンドを実行してください。
$ docker run --rm --mount type=bind,src=/home/docker,dst=/home/test openjdk:9 cat /home/test/Hello.java public class Hello { public static void main(String[] args) { System.out.println("Hello, World"); } }
Hello.java
の中身が出力されました!コンテナの中で実行した cat
から、Dockerホストの Hello.java
が見えているということですね。
--mount
オプションを見てみましょう。--mount
オプションには、「キー=値」の形式でマウントの設定を記述します。「キー=値」はカンマ区切りで複数指定できます。カンマの左右に空白を付けないよう注意してください。各「キー=値」の意味は以下のとおりです。
type=bind
マウントの種類です。 指定できる値は、冒頭で紹介した三種類
bind
,volume
,tmpfs
です。 ここではバインドマウントを行うため、bind
と指定しています。src=/home/docker
マウントする Dockerホストのディレクトリです。 ここでは
Hello.java
が入っている/home/docker
を指定しています。 一つ上の/home
を指定してもよいです。 その場合、/home
の下の/home/docker
やHello.java
も一緒にマウントされます。src
のかわりに、source
と書いても同じ意味になります。dst=/home/test
マウントする Dockerホストのディレクトリの、コンテナの中におけるパスです。 コンテナの中では
/home/test
というパスで、上記src
に指定した/home/docker
を利用できるようになります。/home/test
ディレクトリは、もとのコンテナの中に存在していても、存在していなくてもかまいません。dst
のかわりに、destination
やtarget
と書いても同じ意味になります。
Java のコンパイル: mountオプション
バインドマウントができるようになりましたので、Hello.java
をコンパイルしてみましょう。
以下のコマンドを実行してください。
$ docker run --rm --mount type=bind,src=/home/docker,dst=/home/test openjdk:9 javac /home/test/Hello.java
これでコンパイルが行われたはずです。確認してみましょう。
$ ls /home/docker/Hello* /home/docker/Hello.class /home/docker/Hello.java
Hello.class
が生成されています。コンパイル成功です。
バインドマウントを行うことで、Dockerホストの Javaファイルをコンテナでコンパイルし、クラスファイルを Dockerホストに出力できることが分かりました。
Java の実行: workdirオプション と mountオプション
次に、この Hello.class
を実行してみましょう。
Hello.class
を実行するコマンドは java Hello
とします。ひとまず、先ほどコンパイルしたときの javac ...
コマンドを java Hello
に置き換えて実行してみましょう。
$ docker run --rm --mount type=bind,src=/home/docker,dst=/home/test openjdk:9 java Hello Error: Could not find or load main class Hello Caused by: java.lang.ClassNotFoundException: Hello
ClassNotFoundException
が発生しました。Hello.class
が見つからないようです。
クラスファイルが見つからないときの解決方法はいくつか考えられますが、ここでは --workdir
オプション(省略形 -w
)を使って、java
コマンドを実行するディレクトリを変えてみましょう。
--workdir
オプションは、コンテナの中のどのディレクトリで処理を実行するかを指定できるオプションです。そのように処理を実行するディレクトリを、以下では作業ディレクトリと呼びます。
--workdir
オプションを指定しない場合は、利用するイメージの設定に従い作業ディレクトリが決定されます。先ほど ClassNotFoundException
が発生したコマンドの作業ディレクトリが何だったのか、pwd
コマンドで調べてみましょう。
$ docker run --rm --mount type=bind,src=/home/docker,dst=/home/test openjdk:9 pwd /
作業ディレクトリは、ルートディレクトリ(/
)ですね。Hello.class
が置いてある場所はルートディレクトリではなく、dst
に指定した /home/test
ディレクトリであるため、ClassNotFoundException
が発生したのでしょう。java Hello
を実行する作業ディレクトリを、ルートディレクトリではなく /home/test
ディレクトリに変更すれば、Hello.class
を見つけることできそうですね。
そこで以下のように --workdir
オプションに /home/test
を指定してみましょう。
$ docker run --rm --mount type=bind,src=/home/docker,dst=/home/test --workdir /home/test openjdk:9 java Hello Hello, World
実行できました。Hello, World 成功です!
--workdir
オプションは、バインドマウントや Java のコンテナに限らず、よく利用する便利なオプションです。今回のように、あるはずのファイルが見つからなくて困ったときなどに、思い出して使ってみるとよいでしょう。
alias の活用
バインドマウントは便利ですが、ちょっとコマンドが長いですね。以下のように、Linux の alias コマンドで短くしてみましょう。
$ alias java='docker run --rm --mount type=bind,src=/home/docker,dst=/home/test --workdir /home/test openjdk:9 java' $ java Hello Hello, World
2つ目のコマンド java Hello
の java
は alias ですので、実際にはコンテナが動いています。このように alias を活用すると、まるでローカルにインストールされているツールのようにコンテナを利用することができます。
Windows 上のファイルのバインドマウント
第5回のように Docker Machine を用いて Windows 上の VirtualBox に Dockerホストを作成している場合、Windows 上のファイルもバインドマウントすることができます。
どういうことなのか、以下のように Dockerホストの /c/Users
フォルダを眺めてみると直感的に理解できます。
$ ls /c/Users All Users Default User Public/ Default/ Taro/ desktop.ini
お使いの Windows マシンによって異なりますが、おおよそ上記のような出力結果になると思います。なんだか見たことのあるフォルダが並んでいませんか? Windows の ユーザーフォルダですね。
この /c/Users
フォルダは、Docker Machine が自動的に設定してくれる VirtualBox の共有フォルダです。共有フォルダのおかげで、Windows のユーザーフォルダを Dockerホストのフォルダとして利用できる状態になっているのです。この共有フォルダを、例えば --mount src=/c/Users/Taro
のようにバインドマウントすれば、Windows のファイルがバインドマウントされたのと同じように利用できるというわけです。
volume オプション
最後に --volume
オプション(省略形 -v
)についても紹介しておきます。
今回紹介したバインドマウントとおおよそ同じような処理を、--volume
オプションを用いて実行することができます。--volume
は --mount
よりもずっと以前から存在し、長らく利用されているオプションです。本記事では説明しませんが、--volume
オプションのサンプルコードや解説記事が多数存在しますので、検索してみるとよいでしょう。
しかし Docker社としては、これからマウント処理を学ぶ人は、より扱いやすい --mount
オプションを利用することを推奨しています。筆者は、--mount
オプションの使い方をおぼえると、第4回で紹介した Docker Compose やクラスタリング機能の Swarm、そして --volume
オプションについて理解しやすくなると考えています。そのため、最初は --mount
オプションから学習することをおすすめします。
おわりに
今回は、Docker のバインドマウント機能について説明しました。また、バインドマウントの具体的な例として、Java の Hello World プログラムをコンパイルし、実行する方法を紹介しました。
バインドマウントは、コンテナの特徴である隔離性を敢えて下げて、コンテナを便利に利用できるようにする機能と言えます。コンテナの利点を大きく損なわないよう注意しながら、バインドマウントを上手に利用し、コンテナを活用できる場面を広げていきましょう。