実習で学ぶDocker入門 | 7. バインドマウント Hello Java

今回はDockerのバインドマウント機能について説明する。また、バインドマウントの具体例として、Java Hello Worldプログラムをコンパイルして実行する方法を紹介する。

Dockerのマウント機能

「マウント」という用語は、LinuxやWindowsなどOSの用語として知っている人も多いだろう。たとえば「ハードディスクをマウントする」と言った場合、OSがハードディスクを認識し、利用できる状態にすることを意味する。

Dockerにおける「マウント」は、コンテナ外部にあるデータをコンテナから利用できる状態にすることを意味する。2017年末に公開されたDocker v17.12.0-ceでは、次の3種類のマウント機能が提供されている。

  1. バインドマウント(bind)
  2. ボリュームマウント(volume)
  3. 一時ファイルシステムマウント(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 10上のVirtualBoxに作成したDockerホストで検証している。Docker Machineの使い方については「Docker MachineでWindowsにDockerホストを作成する」を参照してほしい。

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
    • マウントタイプである。使用できる値は最初に紹介した3種類、bindvolumetmpfsである。ここではバインドマウントを行うためにbindを指定している。
  • src=/home/docker
    • マウントするDockerホストのディレクトリである。ここではHello.javaが入っている/home/dockerを指定している。親の/homeを指定してもよい。その場合、/home以下の/home/dockerHello.javaも一緒にマウントされる。srcの代わりにsourceと書いても同じ意味である。
  • dst=/home/test
    • Dockerホストのディレクトリを、コンテナ内のどのパスにマウントするかを示す。コンテナ内では、srcに指定した/home/docker/home/testというパスで利用できる。/home/testディレクトリは元のコンテナ内に存在していても、存在していなくてもよい。dstの代わりにdestinationtargetと書いても同じ意味である。

バインドマウント

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 Hellojavaはaliasなので、実際にはコンテナが動作する。このようにaliasを活用すると、まるでローカルにインストールされたツールのようにコンテナを利用できる。

Windowsでのファイルのバインドマウント

Docker MachineでWindowsにDockerホストを作成するページのように、Docker Machineを使用してWindows上のVirtualBoxにDockerホストを作成する場合、Windowsのファイルをバインドマウントできる。

どういう意味かは、次のようにDockerホストの/c/Usersフォルダを見ると直感的に理解できる。

$ ls /c/Users
All Users     Default User  Public/
Default/      kimkc/     desktop.ini         

使用しているWindowsシステムによって異なるが、おおむね上のような出力結果が表示されるはずである。どこかで見たことのあるフォルダが並んでいないだろうか。Windowsのユーザーフォルダである。

このc/Usersフォルダは、Docker Machineが自動的に設定してくれるVirtualBox共有フォルダである。共有フォルダのおかげで、WindowsユーザーフォルダをDockerホストのフォルダとして利用できる状態になっている。この共有フォルダを、たとえば--mount src=/c/Users/kimkcのようにバインドマウントすれば、Windowsファイルがバインドマウントされたものとして利用できる。

volumeオプション

最後に--volumeオプション(省略形は-v)について見てみよう。

今回紹介したバインドマウントとおおむね同じ処理は、--volumeオプションを使って実行できる。--volume--mountよりもずっと前から存在し、長く使われてきたオプションである。ここでは説明しないが、--volumeオプションのサンプルコードや解説記事は多数存在するため、検索してみるとよいだろう。

ただしDocker社は、これからマウント処理を学ぶ人には、より扱いやすい--mountオプションを利用することを推奨している。筆者は、--mountオプションの使い方を覚えると、Docker Composeを使用して複数コンテナをデプロイするページで紹介したDocker Composeやクラスタリング機能のSwarm、そして--volumeオプションを理解しやすくなると考えている。したがって最初は--mountオプションから学習することを推奨する。

結論

今回はDockerのバインドマウント機能について説明した。また、バインドマウントの具体例として、Java Hello Worldプログラムをコンパイルして実行する方法を紹介した。

バインドマウントは、コンテナの特徴である隔離性をあえて下げ、コンテナを便利に利用できるようにする機能と言える。コンテナの利点を大きく損なわないよう注意しながら、バインドマウントをうまく利用してコンテナを活用していこう。