はじめに
dockerの公式チュートリアルがかなりまとまっていてわかりやすかったので、一通りやってみました。ただ、チュートリアルが英語で躊躇しがちなのかなと思ったので、どなたかの参考になればと和訳してみました。
当方、windows環境なのでwindowsのUIで説明しています。
前提
・dockerインストール済
・gitインストール済
チュートリアルを開始する
インストール済の状態からチュートリアルを開始する場合は、dockerのアイコンをクリックして、Learnをクリックするとチュートリアルが起動します。インストール前ならインストール完了時に自動でチュートリアルが開始されるはずです。
clone
チュートリアル用のコードをcloneします
git clone https://github.com/docker/getting-started.git
Build
docker build -t docker101tutorial .
Dokerfile
からDockerイメージを作成する際はこのbuild
コマンドを使用します。-t docker101tutorial
はDockerイメージのタグ名を指定しています。t
はtagの頭文字です。.
はDockerfile
がカレントディレクトリにあるということを表しています。
まとめると上記コマンドは、カレントディレクトリにあるDokerfile
からdocker101tutorial
というタグ名でDockerイメージを作成する、という意味になります。Dockerイメージは、アプリケーションの実行に必要なファイル群が格納されたディレクトリで、コンテナのもとになるものです。
Run
docker run -d -p 80:80 --name docker-tutorial docker101tutorial
run
コマンドは、Dockerイメージからコンテナを生成し起動するために使用するコマンドで、生成と起動を同時に実行することができます。生成するコマンドdocker create
と起動するコマンドdocker start
もあり、createしてからstartすることはrunするのと同じ動作になります。
-d
はdetachモードで起動することを指定しており、これを指定しておくとバックグラウンド起動できます。これを指定せずに起動するとターミナルに情報が出力されて占有されしまうので、これを避けたい場合は指定します。
上記コマンドは、docker101tutorial
というDockerイメージからdocker-tutorial
という名前のコンテナを起動します。
このときdockerの起動に失敗した場合は、ホスト側のポートを8080に変更してみてください。
1.さぁ、はじめよう
localhost:80にアクセスして下記画面が表示されれば、チュートリアル用のコンテナにアクセスできています。ここからはこのチュートリアルに沿って和訳していきます。ところどころ私のコメントも入れています。
(補足)
このように記載しているところは私のコメント(補足)です。
実行したコマンドについて
コンテナを生成、起動した下記コマンドの説明をします。
docker run -d -p 80:80 docker/getting-started
-
-d
: バックグラウンドモードでコンテナを生成/起動します -
-p 80:80
: ホスト側のポート80をコンテナ側のポート80にマッピングする -
docker/getting-started
: 使用するイメージ
このコマンドは下記のように省略して記載することができます。
docker run -dp 80:80 docker/getting-started
The Docker Dashboard
ここで一旦、Docker Dashboardについて説明しておきます。Dashboardを起動することで、コンテナログを確認したり、コンテナ内のshellを起動したりできます。このほかにも、ブラウザで開いたり、再起動、削除することができ、いろいろな操作がUIで直感的に操作できます。
Dashboardにアクセスすると、下記のような画面が開き、コンテナがRUNNINGしていることが確認できます。「jolly_bouman」のところは、ランダムな名前になるので別の名前になっているはずです。
コンテナとは?
コンテナとは何か・・?簡単に言うと、ホストマシン上での、その他プロセスとは隔離されたマシン上のプロセスです。プロセスというのは、マシンで実行されているプログラムのことです。プロセスの隔離は、Linuxのnamespacesとcgroupsという機能を利用しています。
コンテナイメージとは?
コンテナイメージとは・・?イメージは実行環境で動くコンテナのもと(ひな形)です。コンテナイメージの正体は、アプリケーションの実行に必要なファイル群が格納されたディレクトリです。環境変数やデフォルトで実行するコマンド、その他メタデータが含まれています。
2.アプリケーション
このチュートリアルで実行するアプリケーションはNode.jsで動作するtodoアプリです。Node.jsになじみがなくても大丈夫です。
ここでは、todoアプリとして最低限動くものを用意します。下記の手順に沿ってアプリを起動し、動作を確認してください。
アプリケーションの取得
-
http://localhost:80/assets/app.zip
からソースコードをダウンロードします。(補足)
上記アドレスをブラウザに入力するとソースコードがダウンロードされます。ポートは適宜変更してください。 zipを解凍すると、package.jsonと2つのサブディレクトリ(srcとspec)があります。
コンテナイメージのビルド
アプリケーションをビルドするために、ここではDockerfile
を使います。Dockerfile
はテキストベースのスクリプトで、コンテナイメージを生成するための指示書のようなものです。
下記の手順にそってDockerイメージを作成しましょう。
-
package.jsonと同じディレクトリ階層に
Dockerfile
を作成し、下記を記載してください。dockerfile
には拡張子は必要ありません。FROM node:12-alpine WORKDIR /app COPY . . RUN yarn install --production CMD ["node", "src/index.js"]
-
ターミナル開き、appディレクトリまで移動して下記コマンドを実行してください。
docker build -t getting-started .
Dockerfileから新規のコンテナイメージを生成する場合はこのコマンドを使用します。実行後にターミナルを確認すると多数の"レイヤー"をダウンロードしていることがわかります。これは
node:12-alpie
イメージをベースイメージとして使用したためで、マシン上にこのイメージがない場合はダウンロードする必要があるからです。
ダウンロードが完了すると、アプリケーション内にコピー(COPY . .
)してyarnを使ってアプリケーション依存関係をインストール(RUN yarn install --production
)します。依存関係のインストールはRUN yarn install --production
のコマンドで実行され、package.jsonでdevDependencies
に記載されているもの以外をアプリケーション内にインストールします。devDependencies
に記載されているものは開発時に必要となるパッケージであり、製品版(--production)では必要ないので--production
を指定しています。
CMD
はコンテナイメージからコンテナを起動したときにデフォルトで実行されるコマンドです。つまり、docker run XXXX
を実行したときにnode src/index.js
のコマンドが実行されるということです。
docker build
コマンドの最後の.
はDockerfile
がカレントディレクトリにあるということを意味しています。(補足)
dockerはDockerfile
という名前をデフォルトで探しますが、-fを指定すれば別名も指定できます。Dockerfile.base
という名前にしたい場合は下記のようなコマンドになります
docker build -t sample -f Dockerfile.base .
コンテナを開始する
イメージの準備ができたのでdocker run
コマンドを使用してアプリケーションを実行します。
-
イメージを指定して
docker run
コマンドでコンテナを起動しますdocker run -dp 3000:3000 getting-started
-d
はコンテナをバックグラウンドで起動することを意味していて、-p
によってホスト側のポート3000とコンテナ側のポート3000をマッピングしています。このポートマッピングがないとアプリケーションにアクセスすることができません。 http://localhost:3000にアクセスするとアプリケーションにアクセスできます。
アイテムが想定通り追加されることが確認できるはずです。完了マークをつけることができ、追加したアイテムを削除することもできます。
ここに少し変更を加えてコンテナの管理について学んでいきます。
3.アプリケーションの更新
下記の手順に沿ってアプリケーションを更新してください。
アップデートしたバージョンのイメージをbuildしましょう。下記コマンドを実行します。
docker build -t getting-started .
更新したコードを使った新しいコンテナを起動します。
docke run -dp 3000:3000 getting-started
このコードを実行したとき、次のようなエラーが表示されたはずです。
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.
このエラーの原因は、古いコンテナがまだポート3000で起動したままになっているからです。ホスト側の一つのポートで占有できるプロセスは一つだけです。
これを解決するには、古いコンテナを削除すればよいです。
古いコンテナを差し替える
コンテナを削除するために、まず停止します。停止をしないと削除できません。
古いコンテナを削除する方法は2通りあるので好きなほうで削除してください。
コンテナの削除(CLIを使う)
-
docker ps
コマンドでコンテナIDを取得しますdocker ps
-
コンテナを停止するために
docker stop
を使います# <the-container-id> のところはdocker ps で取得したコンテナIDと差し替えてください docker stop <the-container-id>
-
停止したコンテナを
docker rm
コマンドで削除しますdocker rm <the-container-id>
下記コマンドを使用するとコンテナの停止と削除を1行のコマンドで実行することができます。
docker rm -f <the-container-id>
コンテナの削除(Docker Dashboardを使う)
Docker dashboardを使うと、2クリックでコンテナを削除することができます。CLIを使う場合と比べて簡単で直感的にコンテナを削除することができます。
更新したアプリケーションを開始する
-
更新したアプリケーションを起動します
docker run -dp 3000:3000 getting-started
http://localhost:3000にアクセスして、テキストが更新されていることを確認してください
(3章の要約)
アプリケーションの更新をしましたが、下記2点気づいたと思います。
- 初めに登録したアイテムがすべて消えています。これはtodoリストアプリとして良くないので、これについては後の章で触れます
- 小さな変更にしては、アプリを更新するのにたくさんのステップが必要でした。rebuildして新規のコンテナ起動をいちいちしなくてもよい方法を後の章で見ていきます。
アイテムが保持される方法について触れる前に、イメージを共有する方法について簡単に見ていきましょう。
4.アプリケーションの共有
ここまででイメージが完成したので、それをシェアしていきましょう。Dockerイメージをシェアするには、Dockerレポジトリを利用する必要があります。Docker Hub
を使用していきましょう、ここには、私たちが使用するイメージのすべてが入っています。
レポジトリの作成
イメージをプッシュするためにDocker Hubにレポジトリを作成していきましょう。
- Docker Hubにログインします
- Create a Repositoryをクリックします
- レポジトリ名は
getting-started
とします。またVisibilityはPublic
になっていることを確認します。 - Createボタンをクリックします
ページ右側を見ると、Docker commandsセクションがあり、イメージをプッシュするために実行するコマンドサンプルが表示されています。
イメージのプッシュ
-
コマンドラインを開き、さきほど確認したプッシュ用のコマンドを入力します。このとき、
docker
のところは自分のDocker IDに差し替えてください。$ 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
コマンドを実行してREPOSITORYを確認してみてください。確かにそのような名前のイメージはないですね。
これを解決するためには、"tag"でイメージに別名を付与します。 Docker Hubにログインします。
docker login -u YOUR-USER-NAME
(実行後パスワードを入力してください)-
YOUR-USER-NAME
は自分のDocker IDに差し替えてください。docker tag getting-started YOUR-USER-NAME/getting-started
-
再度プッシュコマンドを実行します。コマンドをDocker Hubからコピーしている場合は、
tagname
はなにも記入しないでください。Dockerではtagを指定しなかった場合、latest
タグが使われます。docker push YOUR-USER-NAME/getting-started
新しいインスタンスでイメージを起動する
ここまでで、イメージのビルドとレジストリへのプッシュが完了しました。プッシュしたイメージを完全に新規の環境で起動させてみたいと思います。ここではPlay with Docker
を使いましょう。
- Play with Dockerを開きます
- Docker Hubアカウントでログインしましょう
ログインができたら左側のサイドバーにある
+ ADD NEW INSTANCE
をクリックしましょう。(もし表示されていない場合はブラウザのサイズを少し小さくしてみてください)少し待つと、下記のような画面がブラウザ上で表示されます。
ターミナル上で先ほどプッシュしたアプリケーションを起動してみましょう
docker run -dp 3000:3000 YOUR-USER-NAME/getting-started
起動すると
3000
と書かれたボタンが表示されるのでクリックしてみましょう。Docker Hubにプッシュしたアプリケーションが起動できたのが確認できます。ボタンがない場合は、OPEN PORTをクリックして、3000と入力してください。
(4章の要約)
この章では作成したイメージをレジストリにプッシュして共有する方法について解説しました。Play with Dockerを使ってプッシュしたイメージを使って新規にアプリケーションを起動してみました。これはCIパイプラインと呼ばれるものと同じで、イメージを作成、プッシュすることでプロダクション環境で最新のイメージを利用することができるのです。
5.DBの永続化
気づいていないかもしれませんが、TODOアプリはコンテナを起動させるたびにデータが消えてしまいます。なぜこのようなことが起きるのか、コンテナがどのように動いているのかを確認しながら理解していきましょう。
コンテナのファイルシステム
コンテナはイメージの様々なレイヤーを使用して起動します。
(補足)
dockerイメージはdockerfile一つ一つの命令がイメージとして重なった状態で、ここではそれら重なった状態を"レイヤー(層)"と表現しているようです。
dockerはdockerfileの各命令一つ一つをイメージとして保持し、それらイメージを利用して、コンテナを起動します。
それぞれのコンテナは独自の"スクラッチスペース"を取得し、その中でファイルの生成、更新、削除を実施します。同じイメージであってもあらゆる変更は他のコンテナからはみることができません。
(補足)
スクラッチスペース:
ここでは他のプロセスから隔離されたメモリ上の空間を想像するとよさそうです
実際に手を動かして確認してみよう
上記を確認するために、2つのコンテナを起動して、それぞれでファイルを編集してみましょう。一方のコンテナで編集したファイルは、もう一方のコンテナからは利用できないことがわかると思います。
-
ubuntu
コンテナを起動します。このコンテナでは、1~10000のランダムな数値を/data.txtに出力しますdocker run -d ubuntu bash -c "shuf -i 1-10000 -n 1 -o /data.txt && tail -f /dev/null"
このコマンドでは、bash shellを起動して2つのコマンドを呼び出しています(&&で2つのコマンドを繋げている)。最初のコマンドはランダムな数値を/data.txtに書き出しています。2つ目のコマンドは、コンテナを実行し続けるために単にファイルを監視しています.
(補足)
試しに2つ目のコマンドと&&を削除してrunしてみてください、起動したコンテナは直ちに停止してしまい、コンテナ内のファイルを確認することができません。 -
出力された値を確認しましょう。Dashboardを開いて、
ubuntu
イメージを起動しているコンテナにマウスオーバーし、一番左のボタン(CLI)をクリックしましょう。
コンテナ内に入ったら、下記コマンドを実行して/data.txtの中身を確認してみましょう。cat /data.txt
もしコマンドラインを使いたい場合は、
docker exec
コマンドで同じことができます。docker ps
でコンテナIDを取得後、下記コマンドを実行すればよいです。docker exec <container-id> cat /data.txt
ランダムな数値が表示されているのがわかると思います。
-
次に、同じ
ubuntu
イメージを使って、もう一つコンテナを起動してみましょう。そうすると/data.txtが存在しないことが確認できます。docker run -it ubuntu ls /
/data.txtが存在しないのは、このファイルがはじめのコンテナのスクラッチスペースに書き出されたからです。
docker rm -f
コマンドで不要なコンテナを削除してください。
コンテナVolumes
これまで見てきたようにコンテナはそれが起動されたときにイメージの内容に従って起動されるものです。コンテナを生成し、ファイルを更新、削除しても、それらの変更はコンテナが削除されてしまうと消失してしまいます。すべての変更は各コンテナで独立しているということです。
Volumesを使えば、消失しないようにすることができます。
Volumesを使えばコンテナの特定のファイルシステムパスをホストマシンに接続することができます。コンテナ側のディレクトリがマウントされていれば、変更はホストマシン側でも確認することができます。もしコンテナを起動したときに、同じディレクトリをマウントしておけば、同じファイルをコンテナ側で確認することができます。つまりデータは消失しないということです。
volumesは2つのタイプがあります。まずは、named volumesから確認してみましょう。
TODOデータを消えないようにする
デフォルトでは、TODOアプリケーションのデータは/etc/todos/todo.dbのSQLite Databaseに保存されます。SQLiteに詳しくなくても大丈夫です。単に関係データベースで、すべてのデータがひとつのファイル内に保存されています。大規模なアプリケーションではこの方法は適さないですが、今回のTODOアプリのような小規模アプリではうまく機能します。あとで、別のデータベースエンジンに切り替える方法についてもみていきます。
データベースが一つのファイルであるならば、そのファイルをホスト側に保持しておき、次回の新規コンテナで使用できるようにすれば、中断したところからコンテナを再開できるはずです。volumeを作成して、データを格納するディレクトリにvolumeをアタッチ(これを"マウント"と呼びます)することにより、データを持続的に利用することができます。コンテナがデータをtodo.dbファイルに書き込むと、それらのデータはホスト側のvolume内で保存されます。
さきほども言った通り、ここではnamed volumeを使います。named volumeは単なるデータのバケツと考えればよいです。これを使う場合、volumeの名前だけを覚えておけば十分で、物理的な記憶領域がどこであるかを意識する必要はなく、volumeの名前と記憶領域の紐づけはDockerが管理します。volumeを利用するたびに、実際のデータの所在をDockerが特定します。
-
docker volume create
コマンドを使ってvolumeを作成します。docker volume create todo-db
今回のnamed volumesを利用していないTODOアプリをDashboardで削除します。(あるいは、
docker rm -f <container-id>
で削除する)-
続いてTODOアプリコンテナを起動するのですが、今回は、
-v
フラグによりvolume mountを指定してください。これによりnamed volumeが利用され、/etc/todos
にマウントされます。これにより/etc/todos
のパスに生成されたすべてのファイルをホスト側で保存することができます。docker run -dp 3000:3000 -v todo-db:/etc/todos getting-started
コンテナを起動したらいくつかアイテムを追加してみてください。
TODOアプリのコンテナを削除します。Dashboardを使うか、
docker ps
でコンテナIDを取得したあと、docker rm -f <id>
で削除します。上記で示したのと同じコマンドを再度入力し、実行します
アプリを開き、先ほど追加したアイテムがあることが確認できるはずです
確認ができたらコンテナをさきほどと同様に削除します。
これでデータを保持する方法がわかりましたね。
(補足)
volumesにはnamed volumesとbind mountの2タイプが標準で使用できます。それぞれの主な違いは、ホスト側でのデータ管理場所です。named volumesは上記で見たようにユーザーはvolume名だけ意識すればOKでした。つまりデータの実体をどこに保存するかはdockerにお任せということです。次にbind mountは、ホスト側のどのディレクトリをコンテナ側のどこのディレクトリにマウントするかを指定することができます。つまり、ホスト側でのデータ管理場所を任意に選択可能ということです。
(情報) Pro-tip
named volumesやbind mounts(次章で説明)はDockerエンジンをインストールすると標準でサポートされている2つの主なvolumesなのですが、このほかにもNFS,SFTP,NetApp,そのほかにもたくさん..をサポートしたvolumeドライバープラグインがあります。これは、Swarm, Kubernetesなどのクラスタ環境内のマルチホスト上でコンテナを起動するときに特に重要となる技術です。
Volumeの詳細を確認する
"named volumeを使ったとき、保存したデータはいったいどこにあるんだろう"と疑問に思うかもしれません。もしそれが知りたい場合は、下記コマンドを実行すれば解決できます。
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
が実際にデータが保存されている場所を示しています。多くのマシンでは、ホストからこのディレクトリにアクセスするためには管理者権限が必要になりますが、確かにデータはそこにあります。
(情報) Docker Desktop上で直接Volumeデータにアクセスするには
上記todo-dbのMountpointを見たとき、こんなディレクトリどこにも存在しない、と思ったかもしれません。Docker Desktopを起動している間、Dockerコマンドはマシン上の小さなVM内で実際は動いています。なのでもしMountpointのディレクトリ内のデータを確認したい場合は、VM内部に接続する必要があります。この方法についてはググると方法がたくさんでてきます。
(5章の要約)
コンテナを削除して再起動した場合にもデータが保持されているアプリケーションを作成できました。
しかし、前の章で見たとおり、イメージに変更を加えて再度ビルドするためにはいくらかの時間がかかってしまいます。bind mountsを利用すれば、より良い方法でアプリを構築できます。次章ではbind mountsについてみていきます。
6.Bind Mountsを使う
前章ではnamed volumeを使ったデータ保持の方法についてみてきました。named volumesはデータの実体がどこに保存されているかを意識しなくてよいので単にデータを保存したい場合にはとても役に立ちます。
bind mountsを使えば、ホスト上のどこにマウントするのかを確実に制御できます。おおよそこれはデータを保持するために使用されますが、新規データをコンテナに追加したい場合にも使用されます。アプリケーションを起動する際にソースコードをコンテナ側にマウントし、ソースコード変更や動作確認をリアルタイムに行うのにbind mountsを利用することができます。
Nodeベースアプリケーションでは、ファイル変更を監視し、変更時にアプリを再起動するといったことを実施するnodemonというツールがあり、他の言語やフレームワークにはこれに相当するツールがあります。
Volumeタイプ比較表
Bind mountsやnamed volumesはDockerエンジンで標準で使用できる主なvolumeなのですが、これに加えて他のユースケース(SFTP,Ceph,NetApp,S3,etc...)をサポートするために様々なvolumeドライバーを利用することができます。
開発モードコンテナを起動する
開発ワークフローをサポートしたコンテナを起動するために、下記を実施します。
- ソースコードをコンテナにマウントする
- すべてのdependenciesをインストールする("dev"dependenciesを含む)
- ファイル変更を監視するためにnodemonを起動する
-
getting-started
コンテナが起動していないことを確認してください - 下記コマンドを実行してください。コマンドの意味は後ほど説明します
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
:backgroundで起動し、ホスト側のポート3000とコンテナ側のポート3000をマッピングします。 -
-w /app
:作業ディレクトリを設定しています。コマンドを実行したとき、ここで指定したディレクトリがカレントディレクトリになります。 -
-v "$(pwd):/app"
:ホスト側のカレントディレクトリとコンテナ側の/app
ディレクトリをマウントします -
node:12-plpine
:使用するイメージです。これはDockerfileからのアプリケーションベースイメージであることに注意してください。 -
sh -c "yarn install && yarn run dev
:sh
を使ってshellを起動します(alpineではbash
はありません)。そしてyarn install
をしてすべてのdependenciesをインストールし、yarn run dev
を走らせます。package.json
をみればわかりますが、dev
スクリプトを走らせることでnodemon
を起動しています。
3.docker logs -f <container-id>
を使ってログを確認します。下記のような表示になっていれば準備OKです。
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
の109行目を下記のように変更してください。
5.ページを更新(あるいは開く)して変更が即座に反映されていることを確認してください。Nodeサーバーが再起動するまでに数秒かかります。もしエラーとなった場合はリフレッシュしてみてください。
6.自由に変更を加えてみてください。満足したらコンテナを停止し、docker build -t getting-started .
で新しいイメージをビルドしてください。
ローカル開発環境構築においてbind mountsはよく利用されます。利点は開発マシンにビルドツールが必要ないことです。単にdocker run
するだけで、開発環境はプルされ、準備完了です。今後Docker Composeについて話す予定ですが、これによりコマンドを簡略化することができます。
(6章要約)
ここではデータベースを永続化して、さらにニーズや要求に対して迅速に対応する方法についてみてきました。
本番環境に備えるため、データベースをSQLiteから、より簡単にスケールできるものへと移行する必要があります。話を簡単にするためにここではリレーショナルデータベースを使い、アプリケーションでMySQLを利用するように更新します。コンテナが互いに通信を許可する方法などを次章から見ていきます。
7.マルチコンテナアプリケーション
ここまではシングルコンテナアプリケーションを扱ってきましたが、次はTODOアプリにMySQLを追加したいと思います。「MySQLはどこで起動させるのか?」「同じコンテナ内にインストールする?それともそれぞれ独立して起動する?」などの疑問があると思います。一般的にはそれぞれのコンテナ内では一つのことを実施すべきです。理由は次の通りです。
- データベースとは関係なく、APIやフロントエンドをスケールさせたいという状況が十分に考えられます
- 各コンテナを独立させることでアップデートやバージョン管理をそれぞれ独立して実施できます
- アプリケーションにデータベースを内蔵する必要がなく、本番環境のデータベースにマネージドサービスを使用したい場合に相性がよいです
- マルチプロセスを起動するにはプロセスマネージャーが別途必要です(コンテナは1つのプロセスしか開始できません)。それにより起動、シャットダウンが複雑になります。
このほかにも理由はたくさんあります。
なのでここではアプリケーションを下記の構成としましょう。
コンテナネットワーキング
コンテナはデフォルトでは他のプロセスとは独立して実行されていて、同じマシン上であっても他のコンテナ/プロセスと繋がることはできません。コンテナを他のコンテナとつなげるためにはどうすればよいでしょうか?答えはネットワーキングです。ネットワークエンジニア並みの知識は必要なく、下記だけ覚えておけば十分です。
2つのコンテナが同じネットワークにあれば互いに繋がることができる
MySqlを起動する
コンテナをネットワーク上に配置する方法は2つあります。1)起動時にネットワークを配置する 2)起動済のコンテナに接続する。ここでは始めにネットワークを作成して、MySQLコンテナ起動時にそれをアタッチします。
-
ネットワークを作成します
docker network create todo-app ```
-
MySQLを起動してネットワークをアタッチします。データベースの初期設定をするためにいくつか環境変数を設定しています。(詳細を知りたい場合はMuSQL Docker Hub listingを確認してください。)
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
フラグがあることがわかります。これについては後ほど説明します。(Pro-tip)
上記コマンドでは、todo-mysql-data
というnamed volumeを使いMYSQLのデータ保存先である/var/lib/mysqlにマウントしています。しかし、docker volume create
コマンドを実行してvolumeを作成していないです。Dockerは、新規のnamed volume名が指定された場合は自動で作成してくれるのです。 -
データベースが起動していることを確認するために接続してみましょう
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)
todoデータベースの準備が完了していることがわかりましたね。
MySQLに接続する
MySQLが起動していることが確認できたのでそれを使ってみましょう。ただどうやって・・?同じネットワーク上でもう一つのコンテナを起動しているとしても、どうやってそのコンテナを見つければいいんでしょう(それぞれのコンテナにはIPが割りあっているのは覚えているが)?
これを理解するためにnicolaka/netshootコンテナを使いましょう。このコンテナにはネットワークのトラブルシューティングやデバックをするのに便利なツール群がインストールされています。
-
nicolaka/netshootイメージを使って新しいコンテナを起動します。同じネットワークに接続することを忘れないでください。
docker run -it --network todo-app nicolaka/netshoot
-
上記コマンドを打つとコンテナ内にはるので
dig
コマンドを実行しましょう(このコマンドは便利なDNSツールです)。ホスト名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"をみるとIPアドレス172.23.0.2に解決されたホスト名mysql
のA
レコードが確認できます(IPアドレスは環境によって異なります)。通常mysql
は有効なホスト名ではありませんが、Dockerはnetwork aliasを持ったコンテナのIPアドレスを解決することができるのです(--network-alias
フラグを覚えていますよね?)。
これが意味するところは、TODOアプリからホスト名mysql
に接続するだけで、データベースと接続することができるということです。これ以上簡単なことはないですね!
TODOアプリをMySQLに接続して起動する
TODOアプリはMySQL接続に関する環境変数の設定をいくつかサポートしています。下記の4つです。
-
MYSQL_HOST
:MySQLサーバーを実行しているホスト名 -
MYSQL_USER
:接続に使用するユーザー名 -
MYSQL_PASSWORD
:接続に使用するパスワード -
MYSQL_DB
:使用するデータベース名
注意!
接続の設定を実施するのに環境変数を使うのは開発環境では問題ないのですが、本番環境でアプリを実行する場合には全く推奨されない方法です。Dockerの前セキュリティリーダーのDiogo Monicaはこの理由についてすばらしいブログ記事に記載してくれています。
それでは、開発環境コンテナをを起動しましょう。
-
上記の環境変数を指定して、MySQLコンテナをTODOアプリネットワークに接続します。
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"
-
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 ```
ブラウザでTODOアプリを開いて、いくつかアイテムを追加してみてください。
-
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 | +--------------------------------------+--------------------+-----------+
追加したアイテムによってnameは変わりますが、確かに保存されていることが確認できました。
Docker Dashboardを確認してみてください。2つのコンテナが起動していることがわかりますが、それらが1つのアプリにグループ化されていないことがわかります。それぞれのコンテナは独立して起動されていますが、TODOアプリはmysqlコンテナに接続しているのです。
(7章要約)
TODOアプリのデータを、独立した外部のmysqlコンテナ内に保存しました。そして、コンテナネットワーキングについても少し学び、サービスディスカバリがどのように実施されるのかをDNSを利用して確認しました。
しかし、アプリケーションを起動するだけなのに、ネットワークを作成したり、コンテナを起動したり、環境変数を指定したり、ポートを開放したり、そのほかにもたくさんのことをやらなければならず、すこし圧倒されたかもしれません。こんなに多くのことは覚えられないですし、誰かに伝えるのも大変です。
次の章では、Docker Composeについて説明します。Docker Composeを使うとはるかに簡単な方法でアプリケーションを他者と共有することができ、また単純なコマンドでこの章で扱ったアプリを起動することができるようになります。
8.Docker Composeを使おう
Docker Composeはマルチコンテナアプリケーションを定義したり、共有したりするときの手助けになるようにと開発されたツールです。Docker Composeを使うとYAMLファイルでサービスを定義でき、一つのコマンドでサービスを起動したり破棄したりできます。
Docker Composeを利用する利点は、アプリケーションを一つのファイルに定義することができ(ファイルはプロジェクトのルートディレクトに配置する)、他の誰かが簡単にプロジェクトに参加できるようになる点です。プロジェクトに参加したい場合は、レポジトリをクローンし、アプリケーションをDocker Composeのコマンドで起動するだけでよいです。GithubやGitLabにはこれを使用したプロジェクトがたくさんあります。
Docker Composeをインストールする
WindowsあるいはMacでDocker Desktop/Toolboxをインストールしている場合は、すでにDocker Composeがインストールされています。Play-with-Dockerについても同様にDocker Composeがすでにインストールされています。Linuxを利用している場合はこちらの手順に沿ってインストールします。
インストールすると、下記コマンドでバージョン情報が確認できます。
docker-compose version
Docker Composeファイルを作成する
- プロジェクトのルートディレクトリに
docker-compose.yml
ファイル(コンポーズファイル)を作成する -
コンポーズファイルにはまずスキーマバージョンを記載します。ほとのどのケースでは最新のバージョンを利用するとよいです。Compose file referenceに現在のスキーマバージョンと互換性表が記載されています。
version: 3.7
-
次は、アプリケーションとして起動させたいサービス(あるいは、コンテナ)のリストを定義します
version: "3.7" services:
次から、コンポーズファイルのサービスについて説明していきます。
アプリのサービスを定義する
アプリのコンテナを定義するために使用した下記コマンドを思い出してください。
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"
-
はじめにサービスを登録し、コンテナイメージを定義しましょう。サービスの名前を任意に設定でき、その名前が自動的にネットワークのエイリアスになります。このエイリアスはMySQLサービスを定義するのに便利です。
version: "3.7" services: app: image: node:12-alpine
-
通常は
image
の定義の近くにcommand
を配置しますが、順序に制約はありません。version: "3.7" services: app: image: node:12-alpine command: sh -c "yarn install && yarn run dev"
-
次に、
ports
で定義される-p 3000 3000
コマンドについてみていきましょう。ここで記載しているのはshort syntaxで、より詳細なlong syntax変数もあります。version: "3.7" services: app: image: node:12-alpine command: sh -c "yarn install && yarn run dev" ports: - 3000:3000
-
次は、
working_dir
で定義する作業ディレクトリ(-w /app
)と、volumes
で定義するボリュームマッピング(-v "$(pwd):/app"
)です。ボリュームにもショートとロングシンタックスがあります。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
です。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
-
最初は、
mysql
という名前で新規にサービスを定義しましょう。このときmysql
が自動的にネットワークエイリアスになります。appを定義したときと同様にイメージを指定しましょう。version: "3.7" services: app: # The app service definition mysql: image: mysql:5.7
-
次はボリュームマッピングの定義です。
docker run
コマンドでコンテナを起動したときは、named volumeは自動的に作成されます。しかし、docker compose
の場合は自動的に作成されません。volumes:
を定義する必要があり、マウントポイントを指定します。ボリューム名を指定するだけで、デフォルトのオプションが使用されます。ほかにもたくさんのオプションがあります。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:
-
最後に環境変数を指定します。
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
が作成できたので、アプリを起動してみましょう。
- ほかのアプリやデータベースのコピーが起動していないか確認してください。(
docker ps
でidを調べて、rm if <ids>
で削除してください) -
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ではネットワークがデフォルトで自動的に作成されます。なのでdocker-compose.yml内にネットワーク作成を定義しませんでした。
-
docker-compose logs -f
コマンドでログを確認しましょう。各サービスごとに1行でログが出力されていることがわかります。このログは、時間に関する不具合を監視したいときに役にたちます。-f
フラグはログに"フォロー(follow)"するというもので、ログが生成されたときに即座に出力されるようになります。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-comopse log -f app
のコマンドのようにサービス名を最後に追加します。(お役立ち情報)アプリが起動するまでDBを待つ
アプリケーションが起動しているとき、アプリ側はMySQLが起動して準備完了になるのを待って接続を開始します。Dockerには、別のコンテナが完全に起動し、準備完了になるのを待ってから別のコンテナを開始するための組み込みサポートがありません。Nodeベースのプロジェクトの場合、待機ポート(wait-port)の依存関係(dependency)を利用することができます。他の言語、フレームワークにも同様のプロジェクトが存在します。 ここまでくると、アプリケーションが起動していることが確認できます。たった一つのコマンドで実行できることが確認できましたね!
Docker Dashboardでアプリを確認する
Docker Dashboardを開くと、appという名前でグルーピングされたものが確認できます。これは、Docker Composeが割り当てたプロジェクト名で、複数のコンテナが一つにまとめられています。プロジェクト名は、デフォルトではdocker-compose.yml
が配置されたディレクトリ名になります。
appを展開すると、コンポーズファイル内で定義した2つのコンテナが確認できます。それぞれの名前はわかりやすく表記されていて、<プロジェクト名>_<サービス名>_<レプリカ番号>
の形式になっているので、どれがappのコンテナで、どれがmysqlのデータベースなのかが一目でわかります。
コンテナを停止し、削除する
コンテナを停止して削除したい場合は、docker-compose down
コマンドを使用するか、Docker Dashboardでゴミ箱アイコンをクリックします。コンテナは停止し、ネットワークは削除されます。
ボリュームの削除
デフォルトの設定ではdocker-compose down
コマンドで停止してもnamed volumeは削除されません。もし削除したい場合は--volumes
フラグを追加する必要があります。
Docker Dashboardで削除した場合にもnamed volumeは削除されません。
停止して削除してしまえば、docker-compose up
により別のプロジェクトを開始することができます。
(8章の要約)
このセクションではDocker Composeについて学び、それによりマルチサービスアプリケーションを定義したり、共有することが劇的に簡単になることがわかりました。また、適切なコンポーズの形式に則ってコマンドを記載してコンポーズファイルを作成しました。
これでチュートリアルは終わりにしたいと思います。しかし、これまで使用してきたDockerfileは大きな問題を抱えているため、それらについて学ぶためにイメージビルドのベストプラクティスについて次章以降記載していきます。
とりあえず、ここまでです。
次の9、10章は随時加筆していこうと思います。
9章:イメージビルドのベストプラクティス
10章:次は何をすればよいか