本稿は、Christffer Noring さん (@chris_noring) の Learn Docker, from the beginning, part II を翻訳し、分かりやすいように少しだけ追記、サンプルコードの実行上の補足等を行ったものです。シリーズ翻訳の意図については 超基礎からの 速習 Docker (1) の冒頭に掲載しています。併せてご参照くださいませ。
#超基礎からの 速習 Docker シリーズ一覧
-
超基礎からの 速習 Docker (1)
- なぜ Docker なのか、コンテナーやイメージ、Dockerfile の基本コンセプトの解説、もちろん、それらを管理するのに必要なコマンド群もカバーしています。
-
超基礎からの 速習 Docker (2)
- いまここ。
-
超基礎からの 速習 Docker (3)
- データベースをコンテナ化し、レガシーなやり方及び新しいやり方である Network を用いて他のコンテナから連絡可能にします。
-
超基礎からの 速習 Docker (4)
- Docker Compose を用いた複数サービスの管理方法を解説します(その1)
-
超基礎からの 速習 Docker (5)
- Docker Compose を用いた複数サービスの管理方法を解説します(その2)
速習 Docker パート2へようこそ。できればパート1を読んで Docker コア コンセプトと基本コマンドについての基本的な理解が出来ているか、他で知っているかしてくれていると良いですね。
本稿では、以下のトピックをカバーしています。
- 復習、そして課題の紹介 パート1で学んだことを復習しましょう。そして、Volume を使わないことがいかに骨が折れるかお伝えしましょう。
- 永続的データ Volume は、僕らが作るファイル群や僕らが中身をいじるデータベース(e.g Sqlite)を永続化するのに利用できます。
- WORKDIR を Volume に変える Volume は、更新あるごとにコンテナーをセットアップしたり、削除したりすることなしにアプリ作業を続ける良い方法も提供してくれます。
訳注
多くの Docker 文章で "Persist data" を "永続的データ" と翻訳していますが、字面では少し分かりにくいですね。この後の Volume の実地で明らかになりますが、要はコンテナーが削除されてもそのまま残っているデータのことで、それを "Persist"、"永続的" と言ってます。
#リソース
Docker を使う事、コンテナー化は、一枚岩をマイクロサービスに分解していくことです。このシリーズのいたるところで僕らは Docker やそのコマンド体系をマスターするために学ぶことになります。そうすれば、きっと君は自作のコンテナーをプロダクション環境で使いたくなるでしょう。その環境は大抵クラウド上にあります。十分な Docker 経験を積んだと思ったなら、次のリンクで Docker をクラウドでどのように活用できるか、ご確認してみると良いと思います。
- Azure 無料アカウントのサインアップ プライベート レジストリのようなクラウドのコンテナーを使うには、無料 Azure アカウントが必要でしょう。
- クラウドのコンテナー クラウドのコンテナーについて他に知っておくべきことについて網羅する概要ページです。
- 自作コンテナーをクラウドにデプロイ 今の Docker スキルをレバレッジしてクラウド上でサービスを動かすことがいかに簡単かを示すチュートリアル。
- コンテナー レジストリの作成 自作 Docker イメージを Docker Hub に入れられますが、クラウドのコンテナー レジストリも可能です。自作イメージをどこかにストアして、一瞬でレジストリから実際のサービスをできるようにすることは凄くないですか?
#復習、そして Volume を使わないことの問題
OK、このシリーズの最初のパートで作った express ライブラリーを使った Node.js アプリケーションをそのまま使っていきましょう。
このセクションでは次のことを行います:
- コンテナーの実行 コンテナーをスタートすることで、最初のパートで学んだ Docker 基本コマンドをいくつか繰り返します。
- アプリの更新 僕らのソースコードを更新し、コンテナーをスタート、ストップ。このやり方がいかに骨が折れるか実感してもらいます。
##コンテナーの実行
アプリケーションが大きくなるにつれて、ルートを追加したくなったり、特定のルートでレンダリングするものを変えたりしたくなります。ここまでのソースコードを見てみましょう:
const express = require('express')
const app = express()
const port = process.env.PORT
app.get('/', (req, res) => res.send('Hello World!'))
app.listen(port, () => console.log(`Example app listening on port ${port}!`))
基本コマンドを覚えているか見てみましょう。Let's Type:
docker ps
% docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
OK、何もないようですね。前回使ったかどうかに関係なく、最後に僕らは docker stop または docker kill を使ってクリーンアップしましたね。ですので、コンテナーをビルドしなければなりません。僕らはどんなイメージを持っているか見てみましょう。
docker images
% docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
chrisnoring/node latest 177e657fae6e 6 days ago 943MB
OK、僕らのイメージがありますね。コンテナーを作って実行しましょう。
docker run -d -p 8000:3000 chrisnoring/node
これは、コンテナーを作り、ポート 8000 、detached モードで実行します。-d フラグを指定してくれてありがとう!
訳注
あれっ、パート1では daemon モードって言ってなかったっけ・・? -d は --detached の略式なので、detached モードの方であってそうだけど。
% docker run -d -p 8000:3000 chrisnoring/node
a093abebf85dd157f35413a5398b0bbc23a5013e90acbca0a109791db0840ec3
コンテナー ID を取得しました。良いですね。http://localhost:8000 にアクセスしてアプリを見つけられるか見てみましょう。
OK、良いですね。いよいよ次のステップ、ソースコードを更新する準備が整いました。
##アプリの更新
デフォルトのルートを、Hello Chris とレンダリングするように変えましょう。次のように加えます。
app.get('/', (req, res) => res.send('Hello Chris!'))
OK、変更を修正したら、ブラウザに戻ってみよう。すると、「Hello World」のままだって気が付きますよ。まだ僕らの変更はコンテナーに反映されてないようです。こうなったら、コンテナーを落として、削除、イメージをリビルド、そしてコンテナーを再び実行する必要があります。すべてのコマンドを実行しなければならない都合上、ビルドしてコンテナーを実行するとき名前で行えるようにすると良いですね。次のようにコンテナーを実行する代わりに:
docker run -d -p 8000:3000 chrisnoring/node
こうタイプしましょう:
docker run -d -p 8000:3000 --name my-container chrisnoring/node
この意味するところは、コンテナーが my-container と言う名前を得ることで、コンテナー ID の代わりにこの名前でコンテナーを参照できるようにしたのです。セットアップや削除にコンテナーIDと同じように使えるので、ちょっと良い感じですね。(訳注:以下いずれも Node.js プロジェクトのフォルダで実行ください。)
docker stop my-container // this will stop the container, it can still be started if we want to
docker rm my-container // this will remove the container completely
docker build -t chrisnoring/node . // creates an image
docker run -d -p 8000:3000 --name my-container chrisnoring/node
こんな感じでコマンドを繋ぐとことも出来ます:
docker stop my-container && docker rm my-container && docker build -t chrisnoring/node . && docker run -d -p 8000:3000 --name my-container chrisnoring/node
訳注
&& によるコマンドのチェーンは PowerShell v7 以降のサポートになります。Windows で PowerShell またはコマンド プロンプトをご利用の皆様はご注意ください。なお、&& はここしか出てこないけどね。
これを最初に見た時の印象は「わお」です。コマンドだらけです。特に開発フェーズにある時にはもっといい方法があるはずです。
そうです、もっと良い方法があります。それでは Volume について見て行きましょう。
#Volume を使う
Volume や データ Volume は、ファイルを書いて永続化できる場所を作る方法です。どうしてそうしたいのでしょう?開発時、アプリケーションの状態はそのままにしたいですよね。最初から始める必要なんてない。一般に、log ファイルや JSON ファイル、データベース (SQLite) までも Volume に保存したいでしょう。
Volume を作ることはとても簡単です。たくさんの方法がありますが、主に二つの方法があります:
- コンテナーを作成する前に、
- だらだらする e.g コンテナーを作っている間
##Volumeの作成と管理
Volumeを作るには以下のようにタイプします:
docker volume create [name of volume]
作成された Volume は以下のようにタイプすることで確認できます:
docker volume ls
% docker volume ls
DRIVER VOLUME NAME
これで、僕らが持っている他の Volume をリストできます。Volume は使っている間にたくさんになってしまいがちですので、数を減らすやり方を知っていると良いでしょう。次のようにタイプします:
docker volume prune
これで今使っていない Volume を削除できます。続けて良いかどうか確認がはいります。
% docker volume prune
WARNING! This will remove all local volumes not used by at least one container.
Are you sure you want to continue? [y/N]
もし、単体の Volume を削除したい場合は、次のようにタイプします:
docker volume rm [name of volume]
君が知りたいだろうもう一つのコマンドは、作った Volume をより詳細に見ることのできる inspect コマンドです。恐らく、永続化ファイルに関する話題としては、もっとも重要です。
docker inspect [name of volume]
これについてのコメントは、Docker が配置するこれらファイルについてほとんど気にしないかも知れませんが、デバッグ目的で時々知りたくなるはずです。このセクションの後で示すように、アプリケーションの開発時に永続化ファイルをコントロールすることは、僕らのアドバンテージとして機能します。
% docker inspect logs
[
{
"CreatedAt": "2020-05-16T10:18:25Z",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/logs/_data",
"Name": "logs",
"Options": null,
"Scope": "local"
}
]
Moutpoint フィールドは、Docker が永続化ファイルにしようとしている場所を教えてくれます。
##アプリケーションに Volume をマウントする
OK、それでは、アプリケーションで使いたい Volume の要点に触れました。僕らのコンテナーにあるファイルを変更したり作成したりしたいので、コンテナーを終了して再起動しても、変更はそのまま残ります。
これを行うために、ほとんど同じことをするための二つの異なるコマンド・構文を使えます。それは:
- -v、--volume、構文は -v [name of volume]:[directory in the container] となります。例えば、-v my-volume:/app
- --mount、構文は --mount source=[name of volume],target=[diretory in container]、例えば、--mount source=my-volume,target=/app
コンテナの実行と組み合わせると、次のようになります:
docker run -d -p 8000:3000 --name my-container --volume myvolume:/logs chrisnoring/node
試してみましょう。まず初めにコンテナを実行します:
% docker run -d -p 8000:3000 --name my-container --volume logs:/logs chrisnoring/node
db2e45b88f88f67633c20e55907770b84d342c5e2377df531bf990522bb0bcc8
訳注
もし、すでにコンテナーが存在している状態の場合はエラーになりますので、docker ps -a
などとして、実行しているコンテナーを確認し、docker stop my-container
docker rm my-container
して、コンテナーを削除してください。
それでは、コンテナーの内部に Volume がマウントされているかどうか、inspect コマンドを実行してみましょう。
・・・
},
"Mounts": [
{
"Type": "volume",
"Name": "logs",
"Source": "/var/lib/docker/volumes/logs/_data",
"Destination": "/logs",
"Driver": "local",
"Mode": "z",
"RW": true,
"Propagation": ""
}
],
"Config": {
・・・
訳注
inspect コマンドの実行ですが、ここでは、docker inspect my-container
のことを言っているようです。このようにすることで、my-container コンテナーの状態全体が JSON で返ってきますので、そのうちの "Mounts" の内容を確認します。
OK、僕らの Volume がありますね。良いでしょう。次のステップは、コンテナーの中の Volume を配置することです。コンテナーに入っていきましょう:
docker exec -it my-container bash
そして、/logs ディレクトリを確認します:
% docker exec -it my-container bash
root@db2e45b88f88:/app# cd ..
root@db2e45b88f88:/# ls
app bin boot dev etc home lib lib64 logs media mnt opt proc root run sbin srv sys tmp usr var
root@db2e45b88f88:/# cd logs/
root@db2e45b88f88:/logs# echo logging... > logs.txt
root@db2e45b88f88:/logs# ls
logs.txt
root@db2e45b88f88:/logs#
OK、今、もしコンテナーを終了したなら、Volume に作ったすべては永続化されて、Volume にないものはすべて失われるはず。うん、いいアイディア。良いでしょう、Volume の原理を理解しましょう。
訳注
一旦、docker stop my-container
docker rm my-container
して、コンテナーを削除しても、docker volume ls
すると Volume が残っていることが分かります。その上で、またdocker run (略) --volume logs:/logs (略)
、docker exec -it my-container bash
して中に入れば、/logs フォルダに logs.txt が残っていることが確認できるでしょう。
##Volume をサブディレクトリとしてマウントする
ここまで、Volume を作って、Docker に永続化するファイルの場所を設定しました。それらのファイルを永続化すると設定したとき、何が起きているのでしょう?
もし、ハード ドライブのディレクトリをポイントすると、そのディレクトリが見えたり、ファイルを配置することができるだけでなく、すでにあるファイルをコンテナーのマウント ポイントに持ってくることもできます。それではデモしていきましょう。私が言っているのは:
- ディレクトリの作成 /logs ディレクトリを作りましょう
- ファイルを作成する logs.txt ファイルを作り、そこにテキストを書きましょう
- コンテナーを実行しましょう ローカル ディレクトリ + /logs をマウント ポイントとして作成しましょう
最初の二つのコマンドで、こんな風なファイル構造になるでしょう。(訳注:Node.js プロジェクトのフォルダ内です)
app.js
Dockerfile
/logs
logs.txt // contains 'logging host...'
package.json
package-lock.json
さて、次は run コマンドを実行して、コンテナーを立ち上げましょう:
% docker run -d -p 8000:3000 --name my-container --volume $(pwd)/logs:/logs chrisnoring/node
186c05ced5e02c4a84996f4b55717e08633c0f8f0b228674ef9f0cb3ecb15f35
訳注
例によって、すでにコンテナーが存在している状態の場合はエラーになりますので、docker ps -a
などとして、実行しているコンテナーを確認し、docker stop my-container
docker rm my-container
して、コンテナーを削除してください。
なお、Windows では $(pwd)/logs がエラーとなります。代わりに、PowerShell から以下のようにしてください。
docker run -d -p 8000:3000 --name my-container -v "${pwd}/logs:/logs" chrisnoring/node
--volume コマンドが少し異なりますね。最初のパラメータ $(pwd)/logs は、カレント ディレクトリとそのサブ ディレクトリ logs を意味しています。二つ目のパラメータ /logs は、僕らのホスト コンピューターの logs ディレクトリーをコンテナーの同じ名前のディレクトリにマウントすることを意味しています。
コンテナーにダイブして、コンテナーが僕らのホスト コンピュータにある logs ディレクトリからのファイルを実際に持ってきていることを確認しましょう。
% docker exec -it my-container bash
root@186c05ced5e0:/app# ls
Dockerfile app.js node_modules package-lock.json package.json
root@186c05ced5e0:/app# cd ..
root@186c05ced5e0:/# ls
app bin boot dev etc home lib lib64 logs media mnt opt proc root run sbin srv sys tmp usr var
root@186c05ced5e0:/# cd logs/
root@186c05ced5e0:/logs# ls
logs.txt
root@186c05ced5e0:/logs# cat logs.txt
logging host...
root@186c05ced5e0:/logs#
上記通りのコマンド セットで docker exec -it my-container bash
コンテナーに入り、logs ディレクトリーに進み、ついに、cat logs.txt
で logs.txt を読み出しました。結果は logging host... これは一つの例で、僕らがホスト コンピューターで持っているファイルと内容は正確に同じです。
これは ホスト コンピューターの Volume とコンテナーの間を接続している Volume です。ホスト コンピューターの log.txt を編集して、コンテナーに何が起きるか確認してみましょう。
root@186c05ced5e0:/logs# cat logs.txt
logging host 2...
わぉ、コンテナーを終了したり、再起動したりせずに、コンテナーの中を変更しました。
##アプリケーションを Volume として扱う
アプリを Volume として扱うように作るために、コンテナーをこんな風に終了させます。
docker kill my-container && docker rm my-container
なぜこんなことをする必要があるのでしょう?それは、ソースコードだけでなく Dockerfile も変更しようとしていて、以下に示そうとしているように Volume を使わない限り、コンテナーは変更を反映できないからです。
今回は、Volume の名前を --volume $(pwd):/app
とパラメータで変えてコンテナーを再実行する必要があります。(訳注:Windows + PowerShell の場合は --volume "${pwd}:/app"
となります)
注:
もし、PWDがスペースを含むディレクトリで構成されている場合は、"$(PWD)":/app
を代わりにパラメータとして指定してください。つまり、$(PWD) をダブルクォーテーション「"」で囲む必要があります。指摘してくれた Vitaly に感謝します
完全なコマンドは以下のようになります(訳注:Node.js プロジェクトのフォルダで行います):
% docker run -d -p 8000:3000 --name my-container --volume $(pwd):/app chrisnoring/node
1b4019ec7c4379f7943a5a1e5a9f3e397cbf81a85060d64e9684e8760867802b
これによって、僕らのアプリ ディレクトリを Volume にして、コンテナー内の何か変更を反映するようにします。
では、Node.js Express アプリケーションにルートを追加しましょう:
app.get("/docker", (req, res) => {
res.send("hello from docker");
});
OK、Express ライブラリを扱うことで知っていることから、ブラウザーから http://localhost:800/docker は到達できるかな?
悲しい顔だね 動いていません。何が良くなかったのでしょう?まぁ、こういうことです。Node.js Express アプリケーションのソースコードを変更した場合、アプリをリスタートする必要があります。戻って、ファイル変更に対して即座に Node.js Express ウェブ サーバーを再起動する方法を考えなければなりません。それにはいくつかのやり方があります。例えば:
- インストール ウェブ サーバーをリスタートする nodemon や forever のようなライブラリをインストール
- 実行 PKILL コマンドを実行して、node.js プロセスを kill し、node app.js を実行する
nodemon のようなライブラリをインストールするだけなら、面倒ということもないので、さっそくそうしましょう(訳注:プロジェクトのフォルダで実行してください):
npm install --save-dev nodemon
これは、他のライブラリの依存関係を pckage.json に持ったということですが、アプリを動かす方法を変える必要があるということでもあります。アプリは nodemon app.js
と言うコマンドを使ってスタートさせなければなりません。これは nodemon が、変更あり次第、全体をリスタートするようケアすることを意味します。それでは、start script を package.json に追加しましょう。結局、それは Node.js 的な方法です:
Node.js が初めての人のためにも、上でおこなったことを書こう。start script を package.json に追加するっていうのは、"scripts" セクションに行き、次のように entry start を追加します(package.json の抜粋):
"scripts": {
"start": "nodemon app.js"
}
デフォルトでは、"scripts" にあるコマンドは、npm run [name of command]
と入力して実行します。しかし、 start や test といった知られたコマンドは、キーワード run で省略でき、npm run start
は npm start
と書けます。他のコマンド "log" を追加してみましょう(package.json の抜粋):
"scripts": {
"start": "nodemon app.js",
"log": "echo \"Logging something to screen\""
}
新しいコマンド "log" を実行するのに、npm run log
と書けます。
OK、一つ残っていますが、それは、Dockerfile を僕らのアプリをどう start するか変更することです。最後のラインを:
ENTRYPOINT ["node", "app.js"]
こうします:
ENTRYPOINT ["npm", "start"]
Dockerfile を変更したので、イメージをリビルドします。このようにします:
docker build -t chrisnoring/node .
訳注
しつこいようですが、docker stop my-container
docker rm my-container
をお忘れなく。コンテナーが残っている時に、新たにイメージを作ると、古いイメージが実行中のコンテナー用に無名のイメージとして残ります。必要に応じて、docker images
で確認し、docker rmi <image id>
などとして削除した方がよいでしょう。
OK、次のステップはコンテナーの起動です:
docker run -d -p 8000:3000 --name my-container --volume $(PWD):/app chrisnoring/node
注目に値することは、カレントとなるプロジェクト ディレクトリ全体を公開し、それをコンテナ内の /app にマッピングする方法です。
/docker に対するルートはすでに追加したので、次のように新しいルートを追加してみましょう。
app.get('/nodemon', (req, res) => res.send('hello from nodemon'))
僕らが app.js の変更を保存したとき、nodemon がその変更を反映してくれますように!
アァァァァンド、僕らは勝った!/nodemon がルートとして動いている。僕は君を知らないけど、これを最初に機能させたのは僕ってことだ。(訳注:ちょっと意味不明・・)
#サマリー
これで記事の最後まで到達しました。とてもクールで使いやすい機能である Volume について学びました。更に重要なことは、全ての開発環境を Volume にして、ソースコードをコンテナーのリスタートなしに働かせ続けられるようにしたことです。
僕らのシリーズのパート3では、リンクされたコンテナーやデータベースがどのように動くかについてカバーします。乞うご期待。
Twitter をフォローして、トピックへのあなたの問い合わせやご質問、提案を頂けるとハッピーです。