Dockerを体系的に学べる公式チュートリアル和訳

この記事について

  • この記事は、Docker Desktopのチュートリアルを和訳したものです。

  • 公式のチュートリアルなので、安心して、かつ効率的に学習することができます。

  • Docker DesktopからDocker Hub、Docker Composeまで網羅されているので、初学者がDockerに初めて触れたり、中級者が基礎を振り返るのに最適です。

  • 翻訳元のチュートリアルは、2020/09/13時点で最新のものです。長い時間が経過している場合、情報が古くなっている場合がございますのでご注意ください。

  • 読者に誤解を与えない部分は、読みやすさを重視して適宜意訳しています。

  • DeepL等を使用して推敲は行っていますが、間違っているところやより良い表現があれば、編集リクエストをお願いいたします。

翻訳元
getting-started : https://github.com/docker/getting-started/tree/6190776cb618b1eb3cfb21e207eefde511d13449

ライセンス : Apache License 2.0

Dockerデスクトップ

Dockerデスクトップは、コンテナ化されたアプリケーションとマイクロサービスを構築・共有するためのツールです。MacOSとWindows上で動作します。

和訳者メモ

Dockerデスクトップをインストールするには、ここにアクセスしてダウンロードするか、下記コマンドを実行してHomebrew Caskよりインストールしてください。

$ brew cask install docker

Dockerデスクトップを開くと、チュートリアルが開始します。それぞれのコマンド等については後半で詳しく説明するので、ここではどのような流れでコンテナが作成されるのかを体感してください。

Clone

まず、レポジトリをクローンします。

Getting Started のプロジェクトは、シンプルなGithubレポジトリで、イメージを作成してコンテナとして実行するために必要なものが全て含まれています。

$ git clone https://github.com/docker/getting-started.git

Build

次に、イメージを作成します。

Dockerイメージは、コンテナのためのプライベートなファイルシステムです。コンテナが必要とする全てのファイルとコードを提供します。

$ cd getting-started
$ docker build -t docker101tutorial .

Run

コンテナを実行しましょう。

前のステップで作成したイメージを元にしたコンテナを起動します。コンテナを起動すると、PCの他の場所から安全に隔離されたリソースを使用してアプリケーションを起動できます。

$ docker run -d -p 80:80 --name docker-tutorial docker101tutorial

Share

イメージを保存して共有しましょう。

イメージをDocker Hubに保存・共有すると、他のユーザーがどんな目的のマシン上でも、簡単にイメージをダウンロードして起動できるようになります。

なお、Docker Hubを利用するには、Dockerアカウントを作成する必要があります。

$ docker tag docker101tutorial michinosuke/docker101tutorial
$ docker push michinosuke/docker101tutorial

docker_01.png

Docker Tutorial

Dockerデスクトップのチュートリアルで作成したコンテナにアクセスすると、より詳しいDocker Tutorialが始まります。

http://localhostにアクセスしましょう。

はじめよう

さっき実行したコマンドについて

このチュートリアルのコンテナを立てることができました。

まず、先ほど実行したコマンドについて説明します。忘れているかもしれないので、もう一度書きます。

docker run -d -p 80:80 docker/getting-started

いくつかフラグが使用されているのに気付いたと思います。それぞれのフラグは以下のような意味があります。

  • -d : コンテナをデタッチモード(バックグラウンド)で実行する。

  • -p : ホストの80番ポートをコンテナの80番ポートにマッピングする。

  • docker/getting-started : 使用するイメージ

応用

一文字のフラグは結合することでコマンド全体を短くできます。例えば、上のコマンドは以下のようにも書けます。

docker run -dp 80:80 docker/getting-started

Dockerダッシュボード

チュートリアルを進める前に、PC上で起動しているコンテナ一覧を表示できるDockerダッシュボードを紹介します。Dockerダッシュボードを使えば、コンテナのログに素早くアクセスできたり、コンテナ内のシェルを取得できたり、コンテナのライフサイクル(停止や削除)を簡単に管理できたりします。

ダッシュボードにアクセスするには、MacまたはWindowsの手順に従ってください。今開いてみると、このチュートリアルが起動しているはずです。コンテナの名前(下ではjolly_boumanになっている)は、ランダムに生成された名前です。なので、違う名前で表示されていると思います。

tutorial-in-dashboard.png

コンテナとは?

コンテナを起動したわけですが、コンテナとは一体なんなのでしょうか?簡単にいうと、コンテナはホストマシンにある他の全てのプロセスから隔離された、シンプルな別のプロセスです。隔離には、カーネルの名前空間とCgroups、長い間Linuxで使用されていた機能を使用します。Dockerはこれらの機能を親しみやすく使いやすいものにするために取り組んできました。

コンテナイメージとは?

コンテナを実行する際には、隔離されたファイルシステムを使用します。このファイルシステムはコンテナイメージから供給されます。イメージにはコンテナのファイルシステムが含まれるので、アプリケーションを実行するのに必要な全ての依存関係・設定・スクリプト・バイナリなどはすべてイメージに含まれている必要があります。また、イメージには、環境変数や起動時のデフォルトコマンド、その他のメタデータなどコンテナの設定も含まれています。

イメージのレイヤ、ベストプラクティスなどの詳細な内容については後で紹介します。

情報

chrootをご存知なら、コンテナはchrootの拡張版だと考えてください。ファイルシステムは、ただイメージから持ってきたものです。しかし、コンテナにはchrootより強力な分離機能が追加されています。

使うアプリの紹介

ここからは、Node.jsで動くシンプルなリスト管理アプリを使って進めていきます。Node.jsが分からなくても問題ありません。JavaScriptについての知識も必要ないです。

ここでは、開発チームはとても小さく、MVP(実用最小限の製品)を示すためにシンプルなアプリを作っている状況を想定します。大規模なチームや複数の開発者などに対してどう動作するかを考える必要はなく、どうやって動くか、何ができるのかを示すためのアプリを作成します。

todo-list-sample.png

アプリを手に入れる

アプリケーションを動かす前に、アプリケーションのソースコードをPCに入れる必要があります。実際のプロジェクトでは、リポジトリからクローンするのが一般的だと思います。しかし、このチュートリアルでは、アプリケーションが入ったZIPファイルを作成しておいたので、そちらを使用します。

  1. ZIPファイルをダウンロードしたら、ZIPファイルを開いて、解凍してください。

  2. 解凍したら、任意のエディターでプロジェクトを開いてください。エディターをインストールしていない場合、Visual Studio Codeを使用してください。package.jsonと2つのサブディレクトリ(srcspec)が表示されるはずです。

ide-screenshot.png

アプリのコンテナイメージを作成する

アプリケーションを構築するには、Dockerfileを使用します。Dockerfileは、コンテナイメージを作成するために使われるテキストベースの命令スクリプトです。今までにDockerfileを作成したことのある方なら、下のDockerfileには欠陥があると分かるかもしれません。それについては後で説明します。

1.以下の内容を書き込んだDockerfilepackage.jsonがあるディレクトリに作成してください。

   FROM node:12-alpine
   WORKDIR /app
   COPY . .
   RUN yarn install --production
   CMD ["node", "src/index.js"]

Dockerfile.txtなどの拡張子が付いていないか確認してください。エディターによっては自動的に拡張子をつけてしまい、次のステップでエラーになる場合があります。

2.移動していない場合は、ターミナルを開いてDockerfileのあるappディレクトリ に移動します。docker buildコマンドを使用してコンテナイメージを構築しましょう。

   docker build -t getting-started .

このコマンドは、Dockerfileを使用して、新しいコンテナイメージを構築します。たくさんの"レイヤ"がインストールされたのに気がついたと思います。なぜかというと、node:12-alpineイメージを起点にすることをビルダーに指示したからです。しかし、そのイメージがPC上になかったので、イメージをダウンロードする必要がありました。

イメージがダウンロードされた後、アプリケーションをコピーし、yarnを使用してアプリケーションの依存関係をインストールしました。CMD命令は、このイメージからコンテナが起動されたときに実行されるデフォルトのコマンドを指定します。

最後に、-tフラグは、イメージにタグを付けます。これは、イメージに人間の理解しやすい名前をつけるものだと考えてください。ここでは、イメージにgetting-startedと名付けたので、コンテナを起動するときはこの名前を参照することができます。

docker buildコマンドの最後につけた.は、DockerがカレントディレクトリにあるDockerfileを探すことを示しています。

アプリのコンテナを起動する

イメージは用意したので、アプリケーションを実行させてみましょう。それには、docker runコマンドを使用します。(すでに一度使ったのを覚えていますか?)

1.docker runコマンドを使用してコンテナを起動し、先ほど作成したイメージの名前を指定してください。

   docker run -dp 3000:3000 getting-started

-dフラグと-pフラグを覚えていますか?新しいコンテナをデタッチ(バックグランド実行)モードで起動し、ホストの3000番ポートをコンテナの3000番ポートにマッピングしました。ポートマッピングをしなかった場合、アプリケーションにアクセスすることはできません。

2.数秒後、http://localhost:3000をWebブラウザで開いてみてください。アプリが表示されるはずです。

todo-list-empty.png

3.1つか2つアイテムを追加してみて、期待通りの動作になるか確認してください。アイテムに完了のチェックを入れたり、アイテムを削除することができます。フロントエンドがアイテムをバックエンドに保存できています。とても簡単でしょう?

この時点で、いくつかのアイテムを持つTodoリスト管理アプリができました。それでは、少し変更を加えながら、コンテナの管理について学んでいきましょう。

Dockerダッシュボードを見てみると、コンテナが2つ起動しているのが分かります。(このチュートリアル自身と、起動したばかりのアプリコンテナです。)

dashboard-two-containers.png

要約

この章では、コンテナイメージの構築についての基本的なことを学び、そのためのDockerfileを作成しました。イメージを構築してから、コンテナを起動し、実行中のアプリを触ってみました。

次は、アプリに修正を加えて、実行中のアプリを新しいイメージで更新する方法を学びましょう。途中で、便利なコマンドもいくつか学びます。

アプリをアップデートする

ちょっとした機能のリクエストとして、プロダクトチームからToDoリストのアイテムが存在しないときに表示される「空のテキスト」を変更してほしいという依頼がありました。以下のように変更したいとのことです。

You have no todo items yet! Add one above!

簡単ですよね?この変更を加えていきます。

ソースコードを更新する

1.src/static/js/app.jsの56行目を書き換えて、新しいテキストが使用されるようにします。

   - <p className="text-center">No items yet! Add one above!</p>
   + <p className="text-center">You have no todo items yet! Add one above!</p>

2.先ほど使ったものと同じコマンドを使って、更新したイメージをビルドしましょう。

   docker build -t getting-started .

3.更新したコードを使って新しいコンテナを起動しましょう。

   docker run -dp 3000:3000 getting-started

あ゛!多分こんなエラーが表示されたと思います。(IDは違います)

docker: Error response from daemon: driver failed programming external connectivity on endpoint laughing_burnell 
(bb242b2ca4d67eba76e79474fb36bb5125708ebdabd7f45c8eaf16caaabde9dd): Bind for 0.0.0.0:3000 failed: port is already allocated.

何が起こったんでしょうか。古いコンテナが動いていたので、新しいコンテナを立ち上げることができなかったのです。この問題が起こった理由として、特定のポートをリッスンできるのはコンテナがあるPC上で一つのプロセス(コンテナを含む)だけですが、コンテナは3000番ポートを既に使っていたからです。このエラーを解消するためには、古いコンテナを削除する必要があります。

古いコンテナを置き換える

コンテナを削除するためには、まず停止させる必要があります。停止してしまえば、削除できます。古いコンテナを削除するには、2通りの方法があります。お好きな方法をお選びください。

CLIでコンテナを削除する

1.docker psコマンドを使用してコンテナのIDを取得します。

   docker ps

2.docker stopコマンドを使用してコンテナを停止させます。

   # <the-container-id>はdocker psコマンドで取得したIDで置き換えてください。
   docker stop <the-container-id>

3.コンテナを停止したら、docker rmコマンドで削除します。

   docker rm <the-container-id>

応用

docker rmコマンドに"force"フラグを追加することで、一つのコマンドでコンテナの停止と削除を行うことができます。

例)docker rm -f <the-container-id>

Dockerダッシュボードを使ってコンテナを削除する

Dockerダッシュボードを開いて、二回クリックするだけでコンテナを削除することができます。コンテナIDを探して削除するよりはるかに簡単です。

  1. ダッシュボードを開いて、アプリのコンテナにカーソルを合わせると、アクション一覧が右側に表示されます。

  2. ゴミ箱ボタンをクリックすると、コンテナが削除されます。

  3. 削除を確認したら完了です。

dashboard-removing-container.png

更新したアプリを起動する

1.更新したアプリを起動します。

   docker run -dp 3000:3000 getting-started

2.http://localhost:3000でブラウザを再読み込みすると、アップデートされたテキストが表示されます。

todo-list-empty.png

要約

アプリの更新ができた一方で、特筆すべきことが2点ありました。

  • 存在していた全てのToDoリストが全て消滅してしまったことです。これではいいアプリとは言えません。これについては近いうちに話そうと思います。

  • 小さな変更だったのにもかかわらず、多くのステップを踏む必要がありました。次の章では、変更を加えるたびに新しいコンテナの再構築と起動を行わなくてもコードを更新する方法について紹介します。

永続性についてお話しする前に、イメージを他人と共有する方法について学びましょう。

アプリを共有する

イメージを作成できたので、早速共有してみましょう。Dockerイメージを共有するには、Dockerレジストリを使用する必要があります。デフォルトのレジストリはDocker Hubであり、今まで使ってきたイメージもそこから持ってきたものでした。

レポジトリの作成

イメージをプッシュするには、まずDocker Hub上にレポジトリを作成する必要があります。

  1. Docker Hubにアクセスして、必要ならログインしてください。

  2. Create Repositoryボタンをクリックしてください。

  3. レポジトリ名はgetting-startedを指定してください。公開レベルがPublicになっていることを確認してください。

  4. Createボタンをクリックしてください。

ページの右側を見ると、Dockerコマンドというのがあると思います。ここには、このレポジトリにプッシュするために実行する必要のあるコマンドの例が記載されています。

push-command.png

イメージをプッシュする

1.Docker Hubにあったpushコマンドをコマンドライン上で実行してみてください。注意点として、コマンドのネームスペースは"docker"ではなく、自分の名前空間を使用してください。

   $ docker push docker/getting-started
   The push refers to repository [docker.io/docker/getting-started]
   An image does not exist locally with the tag: docker/getting-started

なぜ失敗してしまったのでしょうか。このpushコマンドはdocker/getting-startedという名前のイメージを探しましたが、見つからなかったのです。docker image lsを実行してみてもイメージは見つかりません。
この問題を解決するためには、これまでに作成したイメージに別の名前をつけるため、タグ付けする必要があります。

和訳者メモ

docker image lsと同じ動作をするコマンドにdocker imagesがあります。これはDockerコマンドの再編成によるもので、docker image lsの方が新しく、推奨されています。このチュートリアルでは、今後も同じ動作をするコマンドを他に持つコマンドが登場します。

参考:https://qiita.com/zembutsu/items/6e1ad18f0d548ce6c266

2.docker login -u YOUR-USER-NAMEコマンドを使用して、Docker Hubにログインします。

3.docker tagコマンドを使用して、getting-startedイメージに新しい名前をつけます。YOUR-USER-NAMEはあなたのDocker IDに置き換えてください。

   docker tag getting-started YOUR-USER-NAME/getting-started

4.もう一度pushコマンドを実行してみましょう。イメージ名にタグは追加していないので、Docker Hubからコピー&ペーストしてきた場合は、tagnameの部分は削除してください。タグを指定しない場合、Dockerはlatestというタグを使用します。

   docker push YOUR-USER-NAME/getting-started

イメージを新しいインスタンス上で動かす

イメージを構築してレジストリにプッシュできたので、新しいインスタンス上でこのコンテナイメージを動かしてみましょう。そのために、Play with Dockerを使用します。

1.ブラウザでPlay with Dockerを開きます。

2.Docker Hubアカウントでログインします。

3.ログインしたら、左のバーにある「+ ADD NEW INSTANCE」リンクをクリックします(見当たらない場合、ブラウザを少し横に広げてください)。数秒後、ブラウザ上にターミナルウィンドウが表示されます。

pwd-add-new-instance.png

4.ターミナル上で、プッシュしたアプリを起動させましょう。

   docker run -dp 3000:3000 YOUR-USER-NAME/getting-started

イメージが取得されたのち、起動します。

5.3000と書かれたバッジが表示されるので、それをクリックすると、変更を加えたアプリが表示されます。やりましたね。3000と書かれたバッジが表示されない場合、「Open Port」ボタンをクリックして、3000と入力してください。

要約

この章では、イメージをレジストリにプッシュして共有する方法を学びました。それから、新しいインスタンスに入って、プッシュしたイメージを起動しました。これは、CIパイプラインでは一般的なことで、パイプラインがイメージを作成してレジストリにプッシュすると、本番環境では最新版のイメージを使用できるようになります。

ここまでは理解できたので、さっきの章の最後の話題に戻りましょう。アプリを再起動すると、ToDoリストのアイテムが全て消去されてしまうという問題がありました。当然それでは良いUX(ユーザー体験)とは言えないので、どうやってリスタートした後もデータを保持するかを学びましょう。

データベースを永続化する

気がついたと思いますが、ToDoリストはコンテナを起動するたびに初期化されています。なぜでしょうか。コンテナがどのように動作しているのかをもう少し掘り下げてみましょう。

コンテナのファイルシステム

コンテナが起動したとき、イメージの様々なレイヤがファイルシステムのために使用されます。また、それぞれのコンテナは作成/更新/削除を行うための「スクラッチスペース」を確保します。同じイメージが使われている場合でも、変更は別のコンテナに影響しません。

実際に見てみる

実際にみてみるために、2つのコンテナを起動させ、それぞれにファイルを作成しましょう。片方のコンテナでファイルを作成しても、もう一方のコンテナでそのファイルが有効で無いことがわかります。

1.1から10000までのランダムな数字を書き込んだ/data.txtを作成するubuntuコンテナを起動します。

docker run -d ubuntu bash -c "shuf -i 1-10000 -n 1 -o /data.txt && tail -f /dev/null"

コマンドに詳しい方なら、Bashシェルを起動して、二つのコマンドを呼び出していることが分かると思います(そのために&&を使用しています)。最初の部分で、ランダムな1つの数字を/data.txtに書き込んでいます。2つ目のコマンドは、コンテナの実行を維持するためにファイルを監視し続けているだけです。

2.出力されたものを確認するために、execでコンテナの中に入ってみましょう。ダッシュボードを開いて、起動しているubuntuイメージの最初のアクションをクリックすることで、これを行うことができます。

dashboard-open-cli-ubuntu.png

ubuntuコンテナの中でシェルが起動しているのがわかったと思います。以下のコマンドを実行して、/data.txtの内容を表示してみましょう。それができたら、またターミナルを閉じてください。

   cat /data.txt

同じことをコマンドラインを使って行いたい場合は、docker execを使用してください。docker psでコンテナのIDを取得したのち、以下のコマンドでファイルの内容を取得できます。

   docker exec <container-id> cat /data.txt

ランダムな数字が表示されるはずです。

3.別のubuntuコンテナを立ち上げて、同じファイルが存在しないか確認してみましょう。

docker run -it ubuntu ls /

data.txtがありません。書き込まれた先は、最初のコンテナのためのスクラッチスペースだったからです。

4.docker rm -fコマンドを使用して最初のコンテナを削除します。

コンテナのボリューム

ここまでで、コンテナは起動時にイメージの定義から始めることがわかりました。コンテナはファイルの作成、更新、削除を行うことができますが、コンテナが削除されると失われ、全ての変更はそのコンテナに限定されます。しかし、ボリュームを使えば、その全てを変更することができます。

ボリュームは、コンテナの特定のファイルシステムパスがホストマシンに接続できるようにする機能を提供します。コンテナ内のディレクトリがマウントされている場合、そのディレクトリの変更はホストマシンにも影響します。コンテナを再起動しても同じディレクトリをマウントした場合、同じファイルを参照できるというわけです。

ボリュームには2通りのタイプがあります。どちらも使うのですが、とりあえずネームドボリュームを使ってみましょう。

Todoデータを永続化する

ToDoアプリはデータを/etc/todos/todo.dbにあるSQLite Databaseに保存しています。SQLiteが分からなくても気にしないでください。SQLiteはシンプルなリレーショナルデータベースで、一つのファイルに全てのデータを保存しています。これは大きなデータを扱う上ではベストな方法ではないのですが、小さなデモアプリでは有効です。違うデータベースエンジンに切り替える方法については後述します。

データベースが単一のファイルであるため、このファイルをホストで永続化して次のコンテナから参照できるようにすれば、中断した最後のところから再開できるようになるはずです。ボリュームを作成してデータが保存されているディレクトリに接続(マウントともいいます)すれば、データが永続化できます。コンテナがtodo.dbファイルに書き込むと、ボリューム内のホストに保持されます。

軽く触れておくと、これから使おうとしているのは、ネームドボリュームです。ネームドボリュームはデータを入れるバケツと考えてください。Dockerはディスク上に物理的な領域を確保するので、ボリュームの名前だけ覚えておけば良いです。ボリュームを利用するときに、Dockerが正しいデータが取得されているかを検証してくれます。

1.docker volume createコマンドでボリュームを作成します。

   docker volume create todo-db

2.すでに立ち上げたToDoアプリは持続したボリュームを使用せずに実行されているので、ダッシュボードを使うかdoker rm -f <id>コマンドを使用して停止させてください。

3.ToDoアプリのコンテナを起動するのですが、ボリューム接続を指定するのに-vフラグを付け加えてください。ネームドボリュームを使用して/etc/todosに接続し、全てのファイルをキャプチャします。

   docker run -dp 3000:3000 -v todo-db:/etc/todos getting-started

4.コンテナを立ち上げたら、アプリを開いて、ToDoリストにいくつかアイテムを追加してみてください。

items-added.png

5.ToDoアプリのコンテナを削除します。ダッシュボードを使用するか、docker psでIDを取得してからdocker rm -f <id>で削除を行ってください。

6.上記と同じコマンドを使用して、新しいコンテナを起動してください。

7.リストが表示されることを確認したら、コンテナを削除して次に進みましょう。

データの永続化の方法を理解できましたね。

応用

ネームドボリュームとバインドマウント(これについては後で話します)は、Dockerをインストールした時からサポートされている2通りのボリュームですが、ドライバプラグインも数多く存在し、NFS、SFTP、NetAppなどをサポートしています。これは、SwarmやKubernetesなどのクラスタ環境内の複数のホスト上でコンテナを起動させたときにとても重要になります。

ボリュームについて深く知る

多くの人に「ネームドボリュームを使ったときにDockerがデータを保存する実際の場所はどこなんですか?」とよく聞かれます。知りたいのであれば、docker volume inspectコマンドを使用すれば可能です。

docker volume inspect todo-db
[
    {
        "CreatedAt": "2019-09-26T02:18:36Z",
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/todo-db/_data",
        "Name": "todo-db",
        "Options": {},
        "Scope": "local"
    }
]

Mountpointというのが、ディスク上にデータが保存されている実際の場所です。ほとんどのマシンでは、ホストからこのディレクトリにアクセスするのにRoot権限が必要になることに注意してください。

Dockerデスクトップでボリュームデータに直接アクセスする

Dockerデスクトップで実行している間、Dockerコマンドは実際にはマシン上の小さな仮想マシン内で実行されています。Mountpointディレクトリの実際の内容を見たければ、まず仮想マシン内に入る必要があります。

要約

この時点で、存続したまま再起動できる機能的なアプリケーションを作ることができました。投資家に見せびらかして、私たちのビジョンを理解してもらえることを願っています。

しかしながら、変更を加えるたびにイメージを再構築するのは少し時間がかかり過ぎです。変更を加えるのにはもっと良い方法があるんです。バインドマウント(さっきほのめかしたやつ)が、その方法です。さっそく見てみましょう。

バインドマウントを使用する

前の章で、ネームドボリュームを使用して、データベースの永続化を行いました。ネームドボリュームは、データの保存場所について気にする必要がないので、単にデータを保存したい場合には有効です。

バインドマウントを使えば、ホスト上の正確なMountpointをコントロールできます。データの永続化にも使用できますが、追加のデータをコンテナに提供するのによく使われます。アプリを開発する場合、バインドマウントでソースコードをコンテナに接続して、コードを変更したり、応答したり、変更をすぐに確認したりできます。

Nodeで作られたアプリの場合、ファイルの変更を監視してアプリケーションを再起動するのにはnodemonが最適です。同じようなツールは、ほとんどの言語とフレームワークに存在します。

ボリュームのタイプ比較表

バインドマウントとネームドボリュームは、Dockerエンジンが備えた2つの主なボリュームタイプです。一方で、追加のボリュームドライバは、他のユースケース(SFTP, Ceph, NetApp, S3 など)で役立ちます。

Named Volumes バインドマウント
ホストの場所 Dockerが選ぶ 自分が選ぶ
マウントの例 ( -vを使用) my-volume:/usr/local/data /path/to/data:/usr/local/data
コンテナのコンテンツで新しいボリュームを作成する Yes No
ボリュームドライバのサポート Yes No

デベロッパモードのコンテナを起動する

開発段階で使えるコンテナを起動してみましょう。以下のことを行います。

  • ソースコードをコンテナにマウントする。
  • "dev" dependenciesを含む全ての依存関係をインストールする。
  • nodemonを起動してファイルの変更を監視する

では、始めましょう。

1.今までに使用したgetting-startedコンテナが起動していないことを確認してください。

2.以下のコマンドを実行してください。何をしているかの説明もしていきます。

   docker run -dp 3000:3000 \
       -w /app -v "$(pwd):/app" \
       node:12-alpine \
       sh -c "yarn install && yarn run dev"

PowerShellを使っている場合は、以下のコマンドを使用してください。

   docker run -dp 3000:3000 `
       -w /app -v "$(pwd):/app" `
       node:12-alpine `
       sh -c "yarn install && yarn run dev"
  • -dp 3000:3000 : 今までと同じです。デタッチ(バックグラウンド)モードで起動し、ポートマッピングを作成します。

  • -w /app : "ワーキングディレクトリ"か、コマンドが実行されるカレントディレクトリを指定します。

  • -v "$(pwd):/app" : コンテナのホストから、/appディレクトリにカレントディレクトリをバインドマウントします。

  • node:12-alpine : 使用するイメージです。このイメージはDockerfileで指定した通り、アプリのベースイメージとなっていることに気をつけてください。

  • sh -c "yarn install && yarn run dev" : コマンドです。sh(alpineにはbashがありません)を使用してシェルを起動し、yarn installで全ての依存関係をインストールしてから、yarn run devを実行しています。package.jsonを見てみると、devスクリプトはnodemonを起動することがわかります。

3.docker logs -f <container-id>コマンドでログを見ることができます。これを見れば、準備ができていることが分かります。

   docker logs -f <container-id>
   $ nodemon src/index.js
   [nodemon] 1.19.2
   [nodemon] to restart at any time, enter `rs`
   [nodemon] watching dir(s): *.*
   [nodemon] starting `node src/index.js`
   Using sqlite database at /etc/todos/todo.db
   Listening on port 3000

ログを見終わったら、Ctrl+Cで終了することができます。

4.では、アプリに変更を加えてみましょう。src/static/js/app.jsファイル内の「Add Item」ボタンを「Add」に変更してみましょう。109行目にあります。

   -                         {submitting ? 'Adding...' : 'Add Item'}
   +                         {submitting ? 'Adding...' : 'Add'}

5.ページをリフレッシュ(もしくは開く)だけで、ブラウザにほとんど即座に変更が反映されているのがわかるはずです。Nodeサーバーを再起動するのには数秒かかるので、エラーになった場合は、数秒後にリフレッシュしてみてください。

updated-add-button.png

6.他にも変更を加えてみてください。それが終わったら、コンテナを停止してから、docker build -t getting-started .を使用して新しいイメージをビルドしてください。

バインドマウントを使用することは、ローカル開発においてとても一般的なことです。その利点として、開発マシンにビルドツールや環境がインストールされている必要がないことがあります。docker runコマンドだけで、開発環境はプルされ、準備が完了します。のちの章でDocker Composeについて話す予定ですが、これはたくさんのフラグがついたコマンドをシンプルにすることができます。

要約

データベースを永続化し、投資家と創設者の要求と要望に迅速に対応できるようになりました。でも、ちょっと待ってください。素晴らしいニュースが飛び込んできました!

あなたのプロジェクトは、将来的に開発されることになりました。

製品化に備えて、データベースをSQLiteより拡張性の高いものに移行する必要があります。単純に考えて、リレーショナルデータベースはそのままに、MySQLを使用するべきでしょう。しかし、どうやってMySQLを動かせば良いのでしょうか?どうやってコンテナ間での通信を許可すれば良いのでしょうか?それについて、次の章で話していこうと思います。

複数のコンテナを持つアプリ

ここまで、一つのコンテナのアプリで作業してきました。しかし、アプリケーションにMySQLを追加したいです。「MySQLはどこで動かせば良いんですか?同じコンテナに動かして別々に起動すれば良いですか?」という質問がよくあります。一般的に、各コンテナは一つのことのみを行うべきです。それには、いくつか理由があります。

  • APIやフロントエンドをデータベースと異なる方法で拡張させる可能性が高い。

  • コンテナを分離することで、バージョンの更新を分離して行うことができます。

  • ローカルのデータベース用コンテナを使用することもできますが、本番環境ではデータベースを管理するサービスを使用したいと思うかもしれません。その場合、アプリと一緒にデータベースエンジンを製品に含める必要はありません。

  • 複数のプロセスを実行するには、プロセスマネージャ(コンテナは1つのプロセスしか起動しません)が必要になり、コンテナの起動/停止が複雑になります。

もっと他にも理由はあります。なので、こんなふうに動作するようにアプリをアップデートしていきます。

multi-app-architecture.png

コンテナのネットワーク

思い出して欲しいのですが、コンテナはデフォルトで独立して動作し、同じマシン上の他のプロセスやコンテナについて何も知りません。では、どうやってコンテナが他のコンテナと通信できるようにすれば良いのでしょうか?その答えがネットワークです。あなたがネットワークエンジニアである必要はありません。このルールだけ覚えておいてください。

2つのコンテナが同じネットワーク内にあるとき、お互いに通信することができます。同じネットワーク内にないときは、通信できません。

MySQLを起動する

コンテナをネットワーク上に配置するには2通りの方法があります。1つ目は、スタート時に割り当てる方法。2つ目は、すでにあるコンテナを接続する方法です。今回は、最初にネットワークを作成してから、起動したMySQLコンテナを接続しましょう。

1.ネットワークを作成します。

   docker network create todo-app

2.MySQLコンテナを起動して、ネットワークに接続します。また、データベースの初期化に使用する環境変数をいくつか定義します。(MySQL Docker Hub listingの"Environment Variables"という章を参照してください)

   docker run -d \
       --network todo-app --network-alias mysql \
       -v todo-mysql-data:/var/lib/mysql \
       -e MYSQL_ROOT_PASSWORD=secret \
       -e MYSQL_DATABASE=todos \
       mysql:5.7

PowerShellを使用している場合は、以下のコマンドを使用してください。

   docker run -d `
       --network todo-app --network-alias mysql `
       -v todo-mysql-data:/var/lib/mysql `
       -e MYSQL_ROOT_PASSWORD=secret `
       -e MYSQL_DATABASE=todos `
       mysql:5.7

--network-aliasフラグを指定しました。これについては後述します。

プロのための情報

todo-mysql-dataという名前のボリュームを使用し、それをMySQLのデータが保存される/var/lib/mysqlにマウントしました。 しかし、docker volume createコマンドは使用していません。Dockerはネームドボリュームを使おうとしていることを認識して、自動でボリュームを作成してくれたのです。

3.データベースが起動しているか確認するために、データベースに接続して接続されているか確認します。

   docker exec -it <mysql-container-id> mysql -p

もしパスワードを聞かれたら、secretと入力してください。MySQLシェル内で、データベース一覧を表示し、todosデータベースがあることを確認してください。

   mysql> SHOW DATABASES;

このように表示されるはずです。

   +--------------------+
   | Database           |
   +--------------------+
   | information_schema |
   | mysql              |
   | performance_schema |
   | sys                |
   | todos              |
   +--------------------+
   5 rows in set (0.00 sec)

todosデータベースを用意することができました。

MySQLに接続する

MySQLが起動したことは確認できたので、実際に使ってみましょう。でも、同じネットワークで別のコンテナを起動したとして、どうやってコンテナを探せば良いのでしょうか。(各コンテナはそれぞれ別のIPアドレスを持っていることを覚えておいてください。)

これを理解するために、ネットワークに関する問題のトラブルシューティングやデバッグに便利なツールが入ったnicolaka/netshootコンテナを利用しましょう。

1.nicolaka/netshootイメージを使用して、新しいコンテナを立ち上げましょう。同じネットワークに接続されていることを確認してください。

   docker run -it --network todo-app nicolaka/netshoot

2.コンテナ内で便利なDNSツールであるdigコマンドを使用します。ホスト名がmysqlのIPアドレスを探しましょう。

   dig mysql

すると、以下のように表示されるはずです。

   ; <<>> DiG 9.14.1 <<>> mysql
   ;; global options: +cmd
   ;; Got answer:
   ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 32162
   ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

   ;; QUESTION SECTION:
   ;mysql.             IN  A

   ;; ANSWER SECTION:
   mysql.          600 IN  A   172.23.0.2

   ;; Query time: 0 msec
   ;; SERVER: 127.0.0.11#53(127.0.0.11)
   ;; WHEN: Tue Oct 01 23:47:24 UTC 2019
   ;; MSG SIZE  rcvd: 44

「ANSWER SECTION」というところを見ると、mysqlAレコードが172.23.0.2とわかります(あなたのIPアドレスは違う値になっている可能性が高いです)。mysqlは通常有効なホスト名ではありませんが、Dockerはmysqlというネットワークエイリアスを持つコンテナのIPアドレスを解決することができました(--network-aliasフラグを使用したのを覚えていますか?)。

これが意味するのは、ToDoアプリについてもmysqlという名前のホストに接続するだけで、データベースと通信できるということです。これほど簡単なことはありません。

MySQLを使ったアプリを動かす

ToDoアプリでは、MySQLコネクション設定を指定するいくつかの環境変数を設定することができます。詳細は以下の通りです。

  • MYSQL_HOST : 稼働中のMySQLサーバのホスト名です。

  • MYSQL_USER : 接続に使用するユーザ名です。

  • MYSQL_PASSWORD : 接続に使用するパスワードです。

  • MYSQL_DB : 接続後に使用するデータベースです。

注意

開発環境でコネクションの設定に環境変数を使うことは問題ありませんが、本番環境で動いているアプリケーションで使用することはとても推奨されない行為です。Dockerで以前セキュリティを担当していたDiogo Monicaさんがその理由を説明する素晴らしい記事を書いてくれています。

もっと安全な手法としては、コンテナのオーケストレーションフレームワークによって提供されるシークレットサポートを利用することがあります。ほとんどの場合、これらのシークレットファイルは稼働しているコンテナにマウントされます。多くのアプリ(MySQLイメージとToDoアプリを含めて)はファイルを含むファイルを指す_FILE接尾辞がついた環境変数もサポートしています。

例として、変数MYSQL_PASSWORD_FILEを設定すると、アプリは参照されたファイルの内容をコネクションパスワードとして使用します。なお、Dockerはこれらの環境変数を一切サポートしません。アプリは、環境変数を探して、ファイルの内容を取得する方法を知っておく必要があります。

説明は終わったので、コンテナを起動させましょう。

1.前述した環境変数をそれぞれ指定して、コンテナがアプリのネットワークに接続できるようにしましょう。

   docker run -dp 3000:3000 \
     -w /app -v "$(pwd):/app" \
     --network todo-app \
     -e MYSQL_HOST=mysql \
     -e MYSQL_USER=root \
     -e MYSQL_PASSWORD=secret \
     -e MYSQL_DB=todos \
     node:12-alpine \
     sh -c "yarn install && yarn run dev"

PowerShellを使用している場合は、以下のコマンドを使用してください。

   docker run -dp 3000:3000 `
     -w /app -v "$(pwd):/app" `
     --network todo-app `
     -e MYSQL_HOST=mysql `
     -e MYSQL_USER=root `
     -e MYSQL_PASSWORD=secret `
     -e MYSQL_DB=todos `
     node:12-alpine `
     sh -c "yarn install && yarn run dev"

2.コンテナのログを見ると(docker logs <container-id>)、MySQLデータベースを使用していることを示すメッセージがあります。

   # Previous log messages omitted
   $ nodemon src/index.js
   [nodemon] 1.19.2
   [nodemon] to restart at any time, enter `rs`
   [nodemon] watching dir(s): *.*
   [nodemon] starting `node src/index.js`
   Connected to mysql db at host mysql
   Listening on port 3000

3.ブラウザでアプリを開いて、ToDoリストにいくつかアプリを追加してみてください。

4.MySQLデータベースに接続して、アイテムが書き込まれているか確認しましょう。パスワードはsecretです。

   docker exec -ti <mysql-container-id> mysql -p todos

そしてMySQLシェル内で、以下を実行してください。

   mysql> select * from todo_items;
   +--------------------------------------+--------------------+-----------+
   | id                                   | name               | completed |
   +--------------------------------------+--------------------+-----------+
   | c906ff08-60e6-44e6-8f49-ed56a0853e85 | Do amazing things! |         0 |
   | 2912a79e-8486-4bc3-a4c5-460793a575ab | Be awesome!        |         0 |
   +--------------------------------------+--------------------+-----------+

もちろん、あなたのアイテムが含まれているので、テーブルの内容は異なります。でも、ここにアイテムが保存されているのが分かりましたね。

Dockerダッシュボードを見ると、2つのコンテナが起動しています。しかし、一つのアプリにグループ化されているという表示はありません。これを改善する方法について見ていきましょう。

dashboard-multi-container-app.png

要約

独立したコンテナで動く外部のデータベースにデータを保存するアプリケーションを作ることができました。コンテナのネットワークについて少し学び、DNSを使用してサービスの発見を行う方法を理解しました。

しかし、このアプリを立ち上げるために必要な全てのことに、圧倒されているかもしれません。ネットワークを作成し、コンテナを起動し、全ての環境変数を指定し、ポートを解放したりする必要があります。覚えることが多すぎて誰かに伝えるのが難しくなっているのは確かです。

次の章では、Docker Composeについてお話しします。Docker Composeを使えば、アプリケーションスタックをより簡単に共有して、一つのシンプルなコマンドだけで起動するようにできます。

Docker Composeを使用する

Docker Composeは、マルチコンテナアプリの定義と共有をしやすくするために開発されたものです。Composeを使えば、YAMLファイルを作成することでサービスを定義して、1つのコマンドで起動したり、停止したりすることができます。

Composeを使用する大きな利点は、ファイルにアプリケーションスタックを定義し、(バージョン管理されている)プロジェクトリポジトリのルートに保存して、誰でも簡単にプロジェクトにコントリビュートできるようにできることです。実際、GitHubやGitLabにはそのようなプロジェクトがたくさんあります。

では、早速初めていきましょう。

Docker Composeをインストールする

Dockerデスクトップ/ツールボックスをWindowsかMacにインストールしているなら、すでにDocker Composeはインストールされています。また、Play-with-Dockerのインスタンスにも、Docker Composeはインストールされています。Linuxマシンをお使いなら、こちらのページに従ってDocker Composeをインストールしていただく必要があります。

インストールが完了したら、下記のコマンドを実行して、バージョン情報を確認できるはずです。

docker-compose version

Composeファイルを作成する

1.アプリのプロジェクトのルートに、docker-compose.ymlというファイルを作成します。

2.Composeファイルでは、スキーマバージョンを定義するところから始めます。ほとんどの場合、最新版を使うのが良いです。最新のスキーマバージョンと互換性については、Composeファイルのレファレンスを参照してください。

   version: 3.7

3.次に、アプリの一部として動かしたいサービス(もしくはコンテナ)のリストを定義します。

   version: "3.7"

   services:

次は、サービスをComposeファイルに移行していきましょう。

アプリのサービスを定義する

思い出していただきたいのですが、以下はアプリのコンテナを定義するのに使用したコマンドです。

docker run -dp 3000:3000 \
  -w /app -v "$(pwd):/app" \
  --network todo-app \
  -e MYSQL_HOST=mysql \
  -e MYSQL_USER=root \
  -e MYSQL_PASSWORD=secret \
  -e MYSQL_DB=todos \
  node:12-alpine \
  sh -c "yarn install && yarn run dev"

PowerShellを使用している場合は、以下のようなコマンドを使用しました。

docker run -dp 3000:3000 `
  -w /app -v "$(pwd):/app" `
  --network todo-app `
  -e MYSQL_HOST=mysql `
  -e MYSQL_USER=root `
  -e MYSQL_PASSWORD=secret `
  -e MYSQL_DB=todos `
  node:12-alpine `
  sh -c "yarn install && yarn run dev"

1.最初に、コンテナのためのサービスエントリとイメージを定義します。サービス名は任意のものを選ぶことができます。名前は自動的にネットワークエイリアスとして使用されるので、MySQLサービスを定義するのが楽になります。

   version: "3.7"

   services:
     app:
       image: node:12-alpine

2.一般的に、commandはimageの定義の近くに書きますが、順序は自由です。では、ファイルに書き込みましょう。

   version: "3.7"

   services:
     app:
       image: node:12-alpine
       command: sh -c "yarn install && yarn run dev"

3.コマンドの-p 3000:3000という部分をportsに移行しましょう。ここでは簡略した書き方を使用しますが、冗長で長い書き方も同じように使用できます。

   version: "3.7"

   services:
     app:
       image: node:12-alpine
       command: sh -c "yarn install && yarn run dev"
       ports:
         - 3000:3000

4.次に、ワーキングディレクトリ(-w /app)とボリュームマッピング(-v "$(pwd):/app")をworking_dirvolumesに移行します。Volumesの書き方にも短いのと長いのがあります。

Docker Composeのボリューム定義の長所として、カレントディレクトリからの相対パスが使用できることがあります。

   version: "3.7"

   services:
     app:
       image: node:12-alpine
       command: sh -c "yarn install && yarn run dev"
       ports:
         - 3000:3000
       working_dir: /app
       volumes:
         - ./:/app

5.最後に、環境変数をenvironmentキーを使って移行します。

   version: "3.7"

   services:
     app:
       image: node:12-alpine
       command: sh -c "yarn install && yarn run dev"
       ports:
         - 3000:3000
       working_dir: /app
       volumes:
         - ./:/app
       environment:
         MYSQL_HOST: mysql
         MYSQL_USER: root
         MYSQL_PASSWORD: secret
         MYSQL_DB: todos

MySQLサービスを定義する

では、MySQLサービスを定義します。コンテナのために使用したコマンドは以下の通りです。

docker run -d \
  --network todo-app --network-alias mysql \
  -v todo-mysql-data:/var/lib/mysql \
  -e MYSQL_ROOT_PASSWORD=secret \
  -e MYSQL_DATABASE=todos \
  mysql:5.7

PowerShellを使用している場合は、以下のコマンドを使用してください。

docker run -d `
  --network todo-app --network-alias mysql `
  -v todo-mysql-data:/var/lib/mysql `
  -e MYSQL_ROOT_PASSWORD=secret `
  -e MYSQL_DATABASE=todos `
  mysql:5.7

1.まず新しいサービスを定義して、mysqlと名付けると、自動でネットワークエイリアスを取得します。使用するイメージを指定しましょう。

   version: "3.7"

   services:
     app:
       # The app service definition
     mysql:
       image: mysql:5.7

2.次に、ボリュームマッピングを指定します。docker runでコンテナを起動したとき、ネームドボリュームが自動で作成されました。しかし、Composeを使用して起動したときには作成されません。トップレベルにあるvolume:でボリュームを定義してから、サービスコンフィグのマウントポイントを指定します。ボリューム名だけを指定すると、デフォルトのオプションが使用されます。ですが、他にも多くのオプションが利用可能です。

   version: "3.7"

   services:
     app:
       # The app service definition
     mysql:
       image: mysql:5.7
       volumes:
         - todo-mysql-data:/var/lib/mysql

   volumes:
     todo-mysql-data:

3.最後に、環境変数を指定します。

   version: "3.7"

   services:
     app:
       # The app service definition
     mysql:
       image: mysql:5.7
       volumes:
         - todo-mysql-data:/var/lib/mysql
       environment: 
         MYSQL_ROOT_PASSWORD: secret
         MYSQL_DATABASE: todos

   volumes:
     todo-mysql-data:

完成したdocker-compose.ymlは以下のようになります。

version: "3.7"

services:
  app:
    image: node:12-alpine
    command: sh -c "yarn install && yarn run dev"
    ports:
      - 3000:3000
    working_dir: /app
    volumes:
      - ./:/app
    environment:
      MYSQL_HOST: mysql
      MYSQL_USER: root
      MYSQL_PASSWORD: secret
      MYSQL_DB: todos

  mysql:
    image: mysql:5.7
    volumes:
      - todo-mysql-data:/var/lib/mysql
    environment: 
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: todos

volumes:
  todo-mysql-data:

アプリケーションスタックを起動する

docker-compose.ymlが用意できたので、あとは起動するだけです。

1.まず、他にapp/dbのコピーが実行されていないことを確認してください。(docker psdocker rm -f <ids>を使用してください。)

2.docker-compose upコマンドを使用して、アプリケーションスタックを起動してください。-dフラグを追加して、バックグラウンドで実行されるようにします。

   docker-compose up -d

実行すると、以下のように出力されるはずです。

   Creating network "app_default" with the default driver
   Creating volume "app_todo-mysql-data" with default driver
   Creating app_app_1   ... done
   Creating app_mysql_1 ... done

ネットワークと同じくボリュームも作成することができていますね。デフォルトでは、Docker Composeはアプリケーションスタックのネットワークを自動で作成します(Composeファイルで定義しなかったのはこのためです)。

和訳者メモ - docker-compose upで以下のエラーが表示された場合

[ERROR] [FATAL] InnoDB: Table flags are 0 in the data dictionary but the flags in file ./ibdata1 are 0x4800!

以下のコマンドでボリューム削除すると直りました。最初に間違えてmysql:latestdocker-compose.ymlを書いたのが原因だったかもしれないです。

docker volume rm app_todo-mysql-data

3.docker-compose logs -fコマンドを使用してログを見て見ましょう。各サービスのログが一つに集約されているのが分かると思います。これは、タイミングに関する問題を確認したいときにとても便利です。-fフラグはログを追従するので、生成されたログをライブ出力します。

実際の出力は以下のようになります。

   mysql_1  | 2019-10-03T03:07:16.083639Z 0 [Note] mysqld: ready for connections.
   mysql_1  | Version: '5.7.27'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server (GPL)
   app_1    | Connected to mysql db at host mysql
   app_1    | Listening on port 3000

最初のラインの(色付けされている)サービス名は、メッセージを識別するのに役立ちます。特定のサービスについてのログを見たいときは、ログコマンドの末尾にサービス名を追加してください(例:docker-compose logs -f app)。

応用 - アプリを起動する前にデータベースを待機させる

実際には、アプリケーションを起動しても、MySQLが起動して準備が整ってから接続を試みます。Dockerには、コンテナが完全に起動、実行、準備されてから別のコンテナを起動するための組み込みのサポートは用意されていません。Nodeベースのプロジェクトでは、wait-portを使用できます。同じようなプロジェクトは、他の言語・フレームワークにおいても存在します。

4.アプリを開くと、起動しているのを確認できるはずです。そして、停止も一つのコマンドで行えます。

Dockerダッシュボードでアプリケーションスタックを確認する

Dockerダッシュボードを見ると、appという名前のグループがあります。これはDocker Composeのプロジェクト名で、コンテナをグループ化するのに使用されています。デフォルトでは、プロジェクト名はdocker-compose.ymlが配置されているディレクトリの名前です。

dashboard-app-project-collapsed.png

appのドロップダウンを開くと、composeファイルで定義した2つのコンテナが確認できます。そちらの名前は<プロジェクト名>_<サービス名>_<複製番号>というふうになっています。そのおかげで、どのコンテナがどのアプリか、どのコンテナがmysqlデータベースかということが分かりやすくなっています。

dashboard-app-project-expanded.png

全て停止する

docker-compose downを実行するか、Dockerダッシュボードでapp全体をゴミ箱に入れるだけで、全て停止できます。コンテナは停止され、ネットワークは削除されます。

ボリュームの削除

デフォルトでは、docker-compose downを実行しても、composeファイル内のネームドボリュームは削除されません。ボリュームを削除したい場合は、--volumesフラグを付け加える必要があります。

Dockerダッシュボードでは、アプリスタックを削除してもボリュームは削除されません。

停止したら、他のプロジェクトに切り替えてdocker-compose upを実行するだけで、そのプロジェクトを開発することができます。とても簡単ですよね。

要約

この章では、Docker Composeについてと、それによって複数サービスのアプリケーションの定義と共有がどれだけ簡単になるのかということを学びました。使っていたコマンドを適切なCompose形式に移行してComposeファイルを作成しました。

チュートリアルも終盤に入りました。しかし、これまで使ってきたDockerfileには大きな問題があるので、イメージ構築に関するベストプラクティスをいくつか取り上げていきたいと思います。では、みてみましょう。

イメージ構築のベストプラクティス

イメージのレイヤ

イメージの構成要素を確認できることをご存知ですか?docker image historyコマンドを使用すれば、イメージに含まれる各レイヤの作成に使用されたコマンドを確認することができます。

1.docker image historyコマンドを使用して、このチュートリアルで以前作成したgetting-startedイメージのレイヤを見てみましょう。

   docker image history getting-started

すると、以下のように表示されるはずです(おそらくIDは異なります。)。

   IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
   a78a40cbf866        18 seconds ago      /bin/sh -c #(nop)  CMD ["node" "src/index.j…    0B                  
   f1d1808565d6        19 seconds ago      /bin/sh -c yarn install --production            85.4MB              
   a2c054d14948        36 seconds ago      /bin/sh -c #(nop) COPY dir:5dc710ad87c789593…   198kB               
   9577ae713121        37 seconds ago      /bin/sh -c #(nop) WORKDIR /app                  0B                  
   b95baba1cfdb        13 days ago         /bin/sh -c #(nop)  CMD ["node"]                 0B                  
   <missing>           13 days ago         /bin/sh -c #(nop)  ENTRYPOINT ["docker-entry…   0B                  
   <missing>           13 days ago         /bin/sh -c #(nop) COPY file:238737301d473041…   116B                
   <missing>           13 days ago         /bin/sh -c apk add --no-cache --virtual .bui…   5.35MB              
   <missing>           13 days ago         /bin/sh -c #(nop)  ENV YARN_VERSION=1.21.1      0B                  
   <missing>           13 days ago         /bin/sh -c addgroup -g 1000 node     && addu…   74.3MB              
   <missing>           13 days ago         /bin/sh -c #(nop)  ENV NODE_VERSION=12.14.1     0B                  
   <missing>           13 days ago         /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B                  
   <missing>           13 days ago         /bin/sh -c #(nop) ADD file:e69d441d729412d24…   5.59MB   

それぞれにレイヤがイメージのレイヤを表しています。この画面では、イメージのベースが下部、最新のレイヤーが上部に表示されています。これを使えば、素早くそれぞれのレイヤを確認し、サイズの大きいイメージを突き止めるのに役立ちます。

2.いくつかの行が省略されていることに気がついたでしょうか。--no-truncフラグを付け加えると、フル出力を得ることができます。(省略されたフラグを使って省略されていない出力を得るって、面白いですよね。)

   docker image history --no-trunc getting-started

レイヤーのキャッシュ

実際にレイヤーを見たわけですが、これはコンテナイメージのビルド時間を減らすことに関してとても重要な話です。

レイヤーを変更したら、下流の全てのレイヤーを再生成する必要があります。

使用していたDockerfileをもう一度見てみましょう。

FROM node:12-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]

image historyの出力の話に戻ると、Dockerfileのそれぞれのコマンドがイメージの新しいレイヤーになっていることが分かります。イメージに変更を加えたとき、yarnの依存関係が再インストールされたことを覚えているかもしれません。これを修正する方法はあるのでしょうか。ビルドするたびに同じ依存関係をインストールするなんて馬鹿げていますよね。

これを修正するには、Dockerfileを再構築して依存関係をキャッシュする必要があります。Nodeベースのアプリケーションでは、package.jsonファイルで依存関係が定義されています。ということは、このファイルだけを最初にコピーしておけば、依存関係がインストールされてから他のファイルがコピーされます。なので、package.jsonを変更したときのみyarnの依存関係が再作成されるようになります。素晴らしいでしょう?

1.Dockerfileを更新して、最初にpackage.jsonのコピーが行われ、依存関係がインストールされ、それから他のファイルがコピーされるようにします。

   FROM node:12-alpine
   WORKDIR /app
   COPY package.json yarn.lock ./
   RUN yarn install --production
   COPY . .
   CMD ["node", "src/index.js"]

2.Dockerfileと同じフォルダに.dockerignoreというファイルを作成し、以下の内容を書き込みます。

   node_modules

.dockerignoreファイルを使えば、イメージに必要なファイルだけを選択してコピーすることができます。詳しくはここを参照してください。この場合、node_modulesフォルダは2回目のCOPYで除外されます。さもなければ、RUNのコマンドで生成されたファイルで上書きされるでしょう。なぜこの方法がNode.jsアプリケーションで推奨されているかということと、他のベストプラクティスについての詳細は、DockerでのNode製Webアプリをご覧ください。

3.docker buildで新しいイメージをビルドします。

   docker build -t getting-started .

以下のように表示されるはずです。

   Sending build context to Docker daemon  219.1kB
   Step 1/6 : FROM node:12-alpine
   ---> b0dc3a5e5e9e
   Step 2/6 : WORKDIR /app
   ---> Using cache
   ---> 9577ae713121
   Step 3/6 : COPY package.json yarn.lock ./
   ---> bd5306f49fc8
   Step 4/6 : RUN yarn install --production
   ---> Running in d53a06c9e4c2
   yarn install v1.17.3
   [1/4] Resolving packages...
   [2/4] Fetching packages...
   info fsevents@1.2.9: The platform "linux" is incompatible with this module.
   info "fsevents@1.2.9" is an optional dependency and failed compatibility check. Excluding it from installation.
   [3/4] Linking dependencies...
   [4/4] Building fresh packages...
   Done in 10.89s.
   Removing intermediate container d53a06c9e4c2
   ---> 4e68fbc2d704
   Step 5/6 : COPY . .
   ---> a239a11f68d8
   Step 6/6 : CMD ["node", "src/index.js"]
   ---> Running in 49999f68df8f
   Removing intermediate container 49999f68df8f
   ---> e709c03bc597
   Successfully built e709c03bc597
   Successfully tagged getting-started:latest

全てのレイヤーが再構築されているのがわかると思います。Dockerfileを大きく変更したので、もう問題ありません。

4.src/static/index.htmlファイルを編集しましょう(<title>のところの内容を"The Awesome Todo App"に変更します)。

5.もう一度docker build -t getting-started .でDockerイメージをビルドします。今回は、表示される内容が少し変わって、以下のようになるはずです。

   Sending build context to Docker daemon  219.1kB
   Step 1/6 : FROM node:12-alpine
   ---> b0dc3a5e5e9e
   Step 2/6 : WORKDIR /app
   ---> Using cache
   ---> 9577ae713121
   Step 3/6 : COPY package.json yarn.lock ./
   ---> Using cache
   ---> bd5306f49fc8
   Step 4/6 : RUN yarn install --production
   ---> Using cache
   ---> 4e68fbc2d704
   Step 5/6 : COPY . .
   ---> cccde25a3d9a
   Step 6/6 : CMD ["node", "src/index.js"]
   ---> Running in 2be75662c150
   Removing intermediate container 2be75662c150
   ---> 458e5c6f080c
   Successfully built 458e5c6f080c
   Successfully tagged getting-started:latest

ビルドがとても早くなったのに気がついたでしょう。また、ステップ1-4には全てUsing casheが含まれているのが分かると思います。これで、ビルドキャッシュを使うことができました。イメージをプッシュし、プルし、更新するのがとても早くなります。

マルチステージビルド

このチュートリアルではあまり深く触れませんが、マルチステージビルドは、複数のステージを使用してイメージを作成するのに役立つとんでもなくパワフルなツールです。以下のような利点があります。

  • ランタイムの依存関係とビルド時間の依存関係を分離できます。

  • アプリが起動するのに必要な分だけを配置することで、イメージ全体のサイズを減らします。

MavenとTomcatの例

Javaベースのアプリケーションを構築する際には、ソースコードをJavaバイトコードにコンパイルするのにJDKが必要になります。しかし、JDKは本番環境ではそのJDKは必要ありません。また、アプリをビルドするのにMavenやGradleのようなツールを使うかもしれません。これらも最終的なイメージには必要ありません。ここで、マルチステージビルドが役立ちます。

FROM maven AS build
WORKDIR /app
COPY . .
RUN mvn package

FROM tomcat
COPY --from=build /app/target/file.war /usr/local/tomcat/webapps 

この例では、buildと名付けた1つ目のステージで、Mavenを用いたJavaのビルドを行います。FROM tomcatから始まる2つ目のステージで、buildステージからファイルをコピーします。最終的なイメージは、作成される最終ステージ(--targetフラグを使用するとオーバーライドされます)だけです。

Reactの例

Reactアプリケーションをビルドするときは、JSコード(通常はJSX)、SASSスタイルシートなどを静的なHTMLとJS、CSSにコンパイルするNode環境が必要がです。サーバーサイドレンダリングが必要ない場合は、本番環境のビルドにNode環境は必要ありません。それなら、静的なNginxコンテナに静的なリソースを配置すればいいだけの話ですよね。

FROM node:12 AS build
WORKDIR /app
COPY package* yarn.lock ./
RUN yarn install
COPY public ./public
COPY src ./src
RUN yarn run build

FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html

ここでは、node:12イメージを使用してビルド(レイヤキャッシュの最大化)を行ってから出力をNginxコンテナにコピーしています。この方がいいでしょ?

要約

イメージが構造化される方法を少し理解したことで、イメージのビルドは早くなり、より少ない変更を配置できるようになりました。また、マルチステージビルドは、イメージ全体のサイズを減らし、ビルドタイムの依存関係とランタイムの依存関係を切り離すことで最終的なコンテナのセキュリティを向上させるのに役立ちます。

次にすること

チュートリアルは終わったわけですが、コンテナについて学ぶことはもっとあります。ここで深く触れることはないですが、少しみてみましょう。

コンテナのオーケストレーション

本番環境でコンテナを動かすのは難しいことです。マシンにログインしてdocker rundocker-compose upを実行したいとは思いません。なぜでしょうか?では、コンテナが死んだら何が起こるでしょうか?複数のマシンにまたがってスケールする方法は?コンテナのオーケストレーションはこの問題を解決します。Kubernetes、Swarm、Nomad、ECSのようなツールは全てこの問題をわずかに異なる方法で解決します。

予期される状態を受け取る「マネージャ」を持つというのが一般的な考えです。この状態というのは、「Webアプリのインスタンスを2つ起動して、80番ポートを開放してほしい」というようなものです。マネージャはクラスタ内の全てのマシンを監視し、作業を「ワーカ」に委任します。マネージャは(コンテナが停止された、のような)変更を監視し、それから実際の状態が予期された状態を反映するように動作します。

クラウドネイティブコンピューティング基盤プロジェクト

CNCF(クラウドネイティブ基盤のプロジェクト)は、ベンダーに依存しない様々なオープンソースプロジェクトで、Kubernetes、Prometheus、Envoy、Linkerd、NATSなどを含みます。作られたプロジェクトはこちら、全体的なCNCFの図はこちらで見ることができます。これらのたくさんのプロジェクトは、モニタリング、ロギング、セキュリティ、イメージレジストリ、メッセージングなどの問題を解決するのに役立ちます。

なので、コンテナの全容とクラウドネイティブアプリケーションの開発がよくわからない場合は、ぜひご利用ください。コミュニティに参加し、質問をして、勉強してみてください。あなたが参加してくれることを心待ちにしております。

Michinosuke
20代のフロントエンドエンジニア。犬とコーヒーとReactが好きです。
ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
コメント
この記事にコメントはありません。
あなたもコメントしてみませんか :)
すでにアカウントを持っている方は
ユーザーは見つかりませんでした