教会エンジニアの開発日記

Webアプリの開発を生業としたクリスチャンです。

Dockerハンドブック

f:id:sol_satoru:20220115164422p:plain

Dockerの概念や仕組みまではなんとなく理解できるもののDockerfileを書こうとするとスムーズに書けなかったり、そもそものDockerの基礎、あるいはコンテナ技術というものの基礎が抜け落ちていてDocker環境に移行できていないところも多いのではと思い、この記事を翻訳しました。

Source:The Docker Handbook by Farhan Hasin Chowdhury@Twitter

本記事は、原著者の許諾のもとに翻訳・掲載しております。


コンテナ化の概念自体はかなり古いですが、2013年にDocker Engineが登場したことで、アプリケーションのコンテナ化がはるかに簡単になりました。

Stack Overflow Developer Survey-2020によると、 Docker#1 最も望まれるプラットフォーム#2 最も愛されるプラットフォーム、および#3 最も人気のあるプラットフォームとなりました。

必要に応じて、最初に始めるときは少し不安があるかもしれません。したがって、この記事では、コンテナ化の基本レベルから中級レベルまでのすべてを学習します。記事全体を読み終えると、次のことができるようになります。

  • (ほぼ)すべてのアプリケーションのコンテナ化
  • Docker HubにカスタムDockerイメージをアップロードする
  • Docker Composeを使用して複数のコンテナを操作する

前提条件

プロジェクトコード

サンプルプロジェクトのコードは、次のリポジトリにあります。

https://github.com/fhsinchy/docker-handbook-projects

コードの完成形はcontainerizedブランチにあります。

目次

コンテナ化とDockerの概要

コンテナ化とは、ソフトウェアコードとそのすべての依存関係を単一のパッケージ内にカプセル化して、どこでも一貫して実行できるようにするプロセスです。

Dockerはオープンソースのコンテナ化プラットフォームです。コンテナと呼ばれる隔離された環境でアプリケーションを実行する機能を提供します。

コンテナは、ハイパーバイザーを必要とせずにホストオペレーティングシステムカーネルで直接実行できる非常に軽量な仮想マシンのようなものです。その結果、複数のコンテナを同時に実行できます。

同時に実行されるコンテナ

各コンテナには、アプリケーションとその依存関係のすべてが含まれており、他の依存関係から分離されています。開発者は、これらのコンテナをレジストリを通じてイメージとして交換でき、サーバーに直接デプロイすることもできます。

仮想マシンとコンテナ

仮想マシンは、仮想CPU、メモリ、ストレージ、およびオペレーティングシステムを備えた物理コンピューターシステムと同じようなエミュレートされたものです。

ハイパーバイザーと呼ばれるプログラムは、仮想マシンを作成して実行します。ハイパーバイザーが実行されている物理コンピューターはホストシステムと呼ばれ、仮想マシンはゲストシステムと呼ばれます。

仮想マシン

ハイパーバイザーはCPU、メモリ、およびストレージのようなリソースを簡単に既存のゲスト仮想マシン間で再割り当てすることができるプールとして扱います。

ハイパーバイザーには次の2つのタイプがあります。

コンテナは、コードと依存関係を一緒にパッケージ化するアプリケーション層の抽象概念です。物理マシン全体を仮想化する代わりに、コンテナはホストオペレーティングシステムのみを仮想化します。

コンテナ

コンテナは、物理マシンとそのオペレーティングシステムの上に配置されます。各コンテナは、ホストオペレーティングシステムカーネルと、通常はバイナリとライブラリも共有します。

Dockerのインストール

Docker Desktopのダウンロードページに移動し、ドロップダウンからオペレーティングシステムを選択します。

オペレーティングシステムを選択

Macバージョンのインストールプロセスを示しますが、他のオペレーティングシステムへのインストールも同じように簡単なはずです。

Macのインストールプロセスには2つのステップがあります。

  1. ダウンロードしたDocker.dmgファイルをマウントします。
  2. DockerApplicationディレクトリにドラッグアンドドロップします。

*Docker*を*Application*ディレクトリにドラッグアンドドロップします

次に、アプリケーションディレクトリに移動し、ダブルクリックしてDockerを開きます。daemonが実行され、メニューバー(ウィンドウのタスクバー)にアイコンが表示されます。

Dockerアイコン

このアイコンからDockerダッシュボードにアクセスできます。

Dockerダッシュボード

現時点では少し退屈に見えるかもしれませんが、いくつかのコンテナを実行すると、これはより興味深いものになります。

DockerでのHello World

Dockerをマシンで実行する準備ができたので、次は最初のコンテナを実行します。ターミナル(Windowsコマンドプロンプト)を開き、次のコマンドを実行します。

docker run hello-world

すべてがうまくいくと、次のような出力が表示されます。

docker run hello-worldコマンドからの出力

hello-worldイメージは、Dockerでの最小限のコンテナ化の例です。ターミナルに表示されているメッセージを出力するためのhello.cファイルが1つあります。

ほとんどすべてのイメージにはデフォルトのコマンドが含まれています。hello-worldイメージの場合、デフォルトのコマンドは、前述のCコードからコンパイルされたhelloバイナリを実行します。

ダッシュボードをもう一度開くと、hello-worldコンテナが表示されています。

コンテナログ

ステータスはEXITED(0)で、コンテナが実行され、正常に終了したことを示しています。Logs Stats(CPU/memory/disk/network usage)またはInspect(environment/port mappings)が確認できます。

何が起こったのかを理解するには、Dockerのアーキテクチャ、イメージとコンテナ、レジストリについて理解する必要があります。

Dockerアーキテクチャ

Dockerはクライアントサーバーアーキテクチャを使用します。エンジンは3つの主要コンポーネントで構成されています。

  1. Dockerデーモン: デーモンは、クライアントが発行したコマンドをリッスンし、バックグラウンドで動作し続ける長時間実行アプリケーションです。イメージ、コンテナ、ネットワーク、ボリュームなどのDockerオブジェクトを管理できます。

  2. Dockerクライアント: クライアントは、dockerコマンドでアクセスできるCUIプログラムです。このクライアントはサーバーに何をすべきかを伝えます。docker run hello-worldのようなコマンドを実行すると、クライアントはデーモンにタスクを実行するように指示します。

  3. REST API デーモンとクライアント間の通信は、UNIXソケットまたはネットワークインターフェイスを介してREST APIを使用して行われます。

Dockerの公式ドキュメントには、アーキテクチャのわかりやすい図があります。

https://docs.docker.com/get-started/overview/#docker-architecture

いま複雑に見えても心配しないでください。次のサブセクションでは、すべてがより明確になります。

イメージとコンテナ

イメージは、コンテナを作成するために必要な指示を含む、多層の自己完結型ファイルです。レジストリを介してイメージを交換できます。他の人が作成したイメージを使用することも、新しい指示を追加して変更することもできます。

scratchからもイメージを作成できます。イメージのベースレイヤーは読み取り専用です。Dockerfileを編集して再構築すると、変更された部分のみが最上位レイヤーに再構築されます。

コンテナは、イメージの実行可能なインスタンスです。hello-worldのようなイメージをプルして実行すると、イメージに含まれているプログラムの実行に適した隔離された環境が作成されます。この隔離された環境がコンテナです。イメージをOOPのクラスと比較すると、コンテナがオブジェクトになります。

レジストリ

レジストリは、Dockerイメージのストレージです。Docker Hubは、イメージを保存するためのデフォルトのパブリックレジストリです。docker rundocker pullなどのコマンドを実行すると、デーモンは通常ハブからイメージをフェッチします。誰でもdocker pushコマンドを使用してハブに画像をアップロードできます。ハブに移動して、他のWebサイトと同様にイメージを検索できます。

Docker Hub

アカウントを作成すると、カスタムイメージもアップロードできるようになります。アップロードした画像は、https://hub.docker.com/u/fhsinchyページで誰でも利用できます。

全体像

アーキテクチャ、イメージ、コンテナ、レジストリに精通したので、docker run hello-worldコマンドを実行したときに何が起こったかを理解する準備が整いました。プロセスの図は次のとおりです。

docker run hello-world

プロセス全体は5つのステップで行われます。

1.docker run hello-worldコマンドを実行します。 2. Dockerクライアントは、hello-worldイメージを使用してコンテナを実行することをデーモンに通知します。 3. Dockerデーモンは、最新バージョンのイメージをレジストリから取得します。 4. イメージからコンテナを作成します。 5. 新しく作成されたコンテナを実行します。

これは、ローカルに存在しないハブ内のイメージを検索するDockerデーモンのデフォルトの動作です。ただし、イメージがフェッチされると、ローカルキャッシュに残ります。したがって、コマンドを再度実行しても、下記のような出力はされません。

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
0e03bdcc26d7: Pull complete
Digest: sha256:d58e752213a51785838f9eed2b7a498ffa1cb3aa7f946dda11af39286c3db9a9
Status: Downloaded newer image for hello-world:latest

利用可能なイメージの新しいバージョンがある場合、デーモンはイメージを再度フェッチします。その:latestはタグです。イメージには通常、バージョンまたはビルドを示す意味のタグがあります。これについては、後のセクションで詳しく説明します。

コンテナの操作

前のセクションでは、Dockerクライアントについて簡単に触れました。Dockerデーモンにコマンドを渡すのはコマンドラインインターフェースプログラム(CUI)です。このセクションでは、Dockerでのより高度なコンテナ操作について学習します。

コンテナを実行する

前のセクションでは、docker runを使用して、hello-worldイメージを使用するコンテナを作成および実行しました。このコマンドの一般的な構文は次のとおりです。

docker run <イメージ名>

ここでイメージ名は、Docker Hubまたはローカルマシンからの任意のイメージを指定することができます。実行だけでなく、作成し実行と言っていることにお気づきかと思います。その背後にある理由は、docker runコマンドが実際には2つの別個のdockerコマンドの役割を果たしているためです。それが次のとおりです。

1.docker create <イメージ名>- 指定されたイメージからコンテナを作成し、コンテナIDを返します。

2.docker start <コンテナID>- 既に作成されたコンテナの指定されたIDよりコンテナを起動します。

hello-worldイメージからコンテナを作成するには、次のコマンドを実行します。

docker create hello-world

コマンドはこのような長い文字列出力します。cb2d384726da40545d5a203bdb25db1a8c6e6722e5ae03a573d717cd93342f61これがコンテナIDです。このIDは、ビルドされたコンテナを開始するために使用されます。

コンテナを識別するには、コンテナIDの最初の12文字で十分です。文字列全体を使用する代わりに、cb2d384726daを使用するのが適切です。

このコンテナを起動するには、次のコマンドを実行します。

docker start cb2d384726da

コンテナIDを出力として返す必要があります。コンテナが適切に実行されていないと思われるかもしれません。しかし、ダッシュボードを確認すると、コンテナが実行され、正常に終了したことがわかります。

docker start cb2d384726da`および`docker ps -a`コマンドからの出力

ここで起こったことは、ターミナルをコンテナの出力ストリームに接続しなかったということです。UNIXおよびLINUXコマンドは、通常、実行時に3つのI/Oストリーム、つまりSTDINSTDOUT、およびSTDERRを開きます。

詳細については、このトピックに関するすばらしい記事があります。

ターミナルをコンテナの出力ストリームに接続するには、-aまたは--attachオプションを使用する必要があります。

docker start -a cb2d384726da

すべてが正常に完了すると、次の出力が表示されます。

docker start -a cb2d384726da`コマンドからの出力

startコマンドを使用して、まだ実行されていないコンテナを実行できます。runコマンドを使用すると、毎回新しいコンテナが作成されます。

コンテナの一覧表示

前のセクションで、ダッシュボードを使用してコンテナを簡単に確認できることを覚えているかもしれません。

コンテナリスト

これは、個々のコンテナを確認するためにはかなり便利なツールですが、コンテナの単純なリストを表示するには多すぎます。これが、より簡単な方法がある理由です。ターミナルで次のコマンドを実行します。

docker ps -a

そして、ターミナル上のすべてのコンテナの一覧が表示されます。

docker ps -aコマンドからの出力

-aまたは--allオプションは、実行中のコンテナだけでなく、停止中のコンテナも表示します。-aオプションなしでpsを実行すると、実行中のコンテナのみが一覧表示されます。

コンテナの再起動

コンテナを実行するためにはstartコマンドを使用しました。restartというコンテナを開始するための別のコマンドがあります。コマンドは表面的には同じ目的を果たしているように見えますが、若干の違いがあります。

startコマンドは実行されていないコンテナを起動します。ただし、restartコマンドは実行中のコンテナを強制終了し、それを再度起動します。停止したコンテナでrestartを使用すると、startコマンドと同じように機能します。

ダングリングコンテナのクリーンアップ

すでに終了したコンテナはシステムに残ります。これらのダングリングまたは不要なコンテナはスペースを占有し、後で問題が発生する可能性さえあります。

コンテナをクリーンアップする方法はいくつかあります。特定のコンテナを削除したい場合は、rmコマンドを使用できます。このコマンドの一般的な構文は次のとおりです。

docker rm <コンテナID>

IDがe210d4695c51のコンテナを削除するには、次のコマンドを実行します。

docker rm e210d4695c51

そして、削除されたコンテナのIDを出力として取得する必要があります。すべてのDockerオブジェクト(イメージ、コンテナ、ネットワーク、ビルドキャッシュ)をクリーンアップする場合は、次のコマンドを使用できます。

docker system prune

Dockerは確認してきます。-fまたは--forceオプションを使用して、この確認手順をスキップできます。このコマンドは、実行が正常に終了したときに削減された容量を表示します。

インタラクティブモードでのコンテナの実行

これまでのところ、hello-worldイメージから構築されたコンテナのみを実行しています。hello-worldイメージのデフォルトのコマンドは、イメージに付属する単一のhello.cプログラムを実行することです。

すべてのイメージはそれほど単純ではありません。イメージは、オペレーティングシステム全体をカプセル化できます。UbuntuFedoraDebianなどのLinuxディストリビューションには、すべてDocker Hubで利用可能な公式のDockerイメージがあります。

公式のubuntuイメージを使用して、コンテナ内でUbuntuを実行できます。docker run ubuntuコマンドを実行してUbuntuコンテナを実行しようとしても、何も起こりません。ただし、次のように-itオプションを指定してコマンドを実行すると、

docker run -it ubuntu

Ubuntuコンテナ内のbashに直接アクセスする必要があります。このbashウィンドウでは、普段のUbuntuターミナルで実行するようなタスクを実行できます。標準のcat / etc / os-releaseコマンドを実行して、OSの詳細を出力しました。

docker run -it ubuntu コマンドからの出力

この-itオプションが必要な理由は、Ubuntuイメージが起動時にbashを開始するように構成されているためです。bashインタラクティブプログラムです。つまり、何のコマンドも入力しければ、bashは何もしないということです。

コンテナ内のプログラムとやりとりするには、インタラクティブセッションが必要であることをコンテナに明示的に知らせる必要があります。

-itオプションは、コンテナ内のインタラクティブなプログラムとやりとりするための場を設定します。このオプションは、実際には2つの個別のオプションを組み合わせたものです。

*-iオプションはコンテナの入力ストリームに接続するため、入力をbashに送信できます。

*-tオプションは、いくつかの優れたフォーマットとネイティブターミナルのような体験を確実にします。

インタラクティブモードでコンテナを実行するときはいつでも、-itオプションを使用する必要があります。docker run -it nodeまたはdocker run -it pythonを実行すると、nodeまたはpythonREPLプログラムに直接到達するはずです。

node REPL内でのJavaScriptコードの実行

インタラクティブモードでランダムにコンテナを実行することはできません。インタラクティブモードで実行できるようにするには、起動時にインタラクティブプログラムを起動するようにコンテナを設定する必要があります。Shell、REPL、CLIなどは、いくつかのインタラクティブプログラムの例です。

実行可能イメージを使用してコンテナを作成する

これまで、Dockerイメージには、自動的に実行されるデフォルトのコマンドがあると言ってきました。すべてのイメージがそうではありません。一部のイメージは、コマンド(CMD)ではなくエントリポイント(ENTRYPOINT)で構成されています。

エントリポイントを使用すると、実行可能ファイルとして実行されるコンテナを構成できます。他の通常の実行可能ファイルと同様に、これらのコンテナに引数を渡すことができます。実行可能コンテナに引数を渡すための一般的な構文は次のとおりです。

docker run <イメージ名> <引数>

Ubuntuイメージは実行可能ファイルであり、イメージのエントリポイントはbashです。実行可能コンテナに渡される引数は、エントリポイントプログラムに直接渡されます。つまり、Ubuntuイメージに渡す引数はすべてbashに直接渡されます。

Ubuntuコンテナ内のすべてのディレクトリのリストを表示するには、lsコマンドを引数として渡します。

docker run ubuntu ls

次のようなディレクトリのリストが表示されます。

docker run ubuntu ls コマンドからの出力

-itオプションを使用していないことに注意してください。bashとやりとりしようとするのではなく、出力が必要なだけだからです。有効なbashコマンドを引数として渡すことができます。pwdコマンドを引数として渡すと、現在の作業ディレクトリが返されます。

有効な引数のリストは通常​​、エントリポイントプログラム自体によって異なります。コンテナがシェルをエントリーポイントとして使用する場合、任意の有効なシェルコマンドを引数として渡すことができます。コンテナが他のプログラムをエントリーポイントとして使用する場合、その特定のプログラムに有効な引数をコンテナに渡すことができます。

デタッチモードでコンテナを実行する

コンピューターでRedisサーバーを実行するとします。Redisは非常に高速なインメモリデータベースシステムであり、さまざまなアプリケーションでキャッシュとしてよく使用されます。公式のredisイメージを使用してRedisサーバーを実行できます。これを行うには、次のコマンドを実行します。

docker run redis

ハブからイメージを取得するのにしばらく時間がかかる場合があります。その後、ターミナルにテキストの壁が表示されるはずです。

docker run redis コマンドからの出力

ご覧のとおり、Redisサーバーは実行中であり、接続を受け入れる準備ができています。サーバーを実行し続けるには、このターミナルウィンドウを開いたままにする必要があります(これは私の考えでは面倒です)。

これらの種類のコンテナは、デタッチモードで実行できます。デタッチモードで実行されているコンテナは、サービスのようにバックグラウンドで実行されます。コンテナをデタッチするには、-dまたは--detachオプションを使用できます。コンテナをデタッチモードで実行するには、次のコマンドを実行します。

docker run -d redis

コンテナIDを出力として取得する必要があります。

docker run -d redis コマンドからの出力

Redisサーバーがバックグラウンドで実行されています。ダッシュボードを使用するか、psコマンドを使用して確認できます。

実行中のコンテナ内でコマンドを実行する

Redisサーバーがバックグラウンドで実行されたので、redis-cliツールを使用していくつかの操作を実行するとします。先に進んでdocker run redis redis-cliを実行することはできません。コンテナは既に実行中です。

このような状況では、実行中のコンテナ内にexecと呼ばれる他のコマンドを実行するコマンドがあり、このコマンドの一般的な構文は次のとおりです。

docker exec <コンテナID> <コマンド>

RedisコンテナのIDが5531133af6a1の場合、コマンドは次のようになります。

docker exec -it 5531133af6a1 redis-cli

そして、redis-cliプログラムに入り込みます。

docker exec -it 5531133af6a1 redis-cli コマンドからの出力

これはインタラクティブセッションになるため、-itオプションを使用していることに注意してください。このウィンドウではどの有効なRedisコマンドでも実行でき、データはサーバーに保持されます。

ctrl + cを押すか、ターミナルウィンドウを閉じるだけで終了できます。ただし、CLIプログラムを終了しても、サーバーはバックグラウンドで実行され続けることに注意してください。

実行中のコンテナ内でシェルを起動する

なんらかの理由で、実行中のコンテナ内でシェルを使用したいとします。これは、次のコマンドのように、 実行可能であるshを添えてexecコマンドを実行してできます。

docker exec -it <コンテナID> sh

redisコンテナのIDが5531133af6a1の場合、次のコマンドを実行してコンテナ内のシェルを起動します。

docker run exec -it 5531133af6a1 sh

コンテナ内のシェルに入れるはずです。

docker run exec -it 5531133af6a1 sh コマンドからの出力

ここで有効なシェルコマンドを実行できます。

実行中のコンテナからログにアクセスする

コンテナからのログを表示する場合は、ダッシュボードが非常に役立ちます。

Dockerダッシュボードにあるログ

また、logsコマンドを使用して、実行中のコンテナからログを取得することもできます。コマンドの一般的な構文は次のとおりです。

docker logs <コンテナID>

RedisコンテナのIDが5531133af6a1の場合、次のコマンドを実行してコンテナのログにアクセスします。

docker logs 5531133af6a1

ターミナルウィンドウにテキストの壁が表示されるはずです。

docker logs 5531133af6a1 コマンドからの出力

これはログ出力の一部にすぎません。-fまたは--followオプションを使用すれば、コンテナの出力ストリームにフックして、リアルタイムでログを取得できます。

それ以降のログは、ctrl + cを押すかウィンドウを閉じて終了しない限り、即座にターミナルに表示されます。ログウィンドウを終了しても、コンテナは実行を続けます。

実行中のコンテナを停止または強制終了する

ターミナルで実行中のコンテナは、ターミナルウィンドウを閉じるか、ctrl + cを押すだけで停止できます。ただし、バックグラウンドで実行されているコンテナを同じ方法で停止することはできません。

実行中のコンテナを停止するには、2つのコマンドがあります。

*docker stop <コンテナID>-SIGTERMシグナルをコンテナに送信して、コンテナを正常に停止しようとします。コンテナが制限時間内に停止しない場合、SIGKILLシグナルが送信されます。

*docker kill <コンテナID>-SIGKILLシグナルを送信してコンテナを即座に停止します。SIGKILLシグナルを受け取ったら無視することはできません。

IDがbb7fadc33178のコンテナを停止するには、docker stop bb7fadc33178コマンドを実行します。docker kill bb7fadc33178を使用すると、クリーンアップする機会を与えずにコンテナが即座に終了します。

ポートのマッピング

人気のWebサーバーであるNginxインスタンスを実行するとします。これは、公式のnginxイメージを使用して行うことができます。次のコマンドを実行してコンテナを実行します。

docker run nginx

Nginxは実行し続けることを意図しているため、-dまたは--detachオプションを使用することもできます。デフォルトでは、Nginxはポート80番で実行されます。しかし、http://localhost:80にアクセスしようとすると、次のように表示されます。

http://localhost:80

これは、Nginxがコンテナ内のポート80番で実行されているためです。コンテナは分離された環境であり、ホストシステムはコンテナ内で何が行われているのか何も知りません。

コンテナ内のポートにアクセスするには、そのポートをホストシステムのポートにマップする必要があります。これを行うには、docker runコマンドで-pまたは--portオプションを使用します。このオプションの一般的な構文は次のとおりです。

docker run -p <ホストポート:コンテナポート> nginx

docker run -p 80:80 nginxを実行すると、ホストマシンのポート80番がコンテナのポート80番にマッピングされます。次に、http://localhost:80アドレスにアクセスしてみます。

http://localhost:80

80:80の代わりにdocker run -p 8080:80 nginxを実行すると、ホストマシンのポート8080番でNginxサーバーが利用可能になります。しばらくしてポート番号を忘れた場合は、ダッシュボードを使用して確認できます。

Dockerダッシュボードのポートマッピング

Inspectタブには、ポートマッピングに関する情報が含まれています。ご覧のとおり、ポート80番をコンテナからホストシステムのポート8080番にマッピングしました。

コンテナ分離のデモ

コンテナの概念を紹介したときから、コンテナは分離された環境であると言ってきました。隔離されているとは、ホストシステムだけでなく、他のコンテナからも意味します。

このセクションでは、この分離について理解するために少し実験を行います。2つのターミナルウィンドウを開き、次のコマンドを使用して2つのUbuntuコンテナインスタンスを実行します。

docker run -it ubuntu

ダッシュボードを開くと、2つのUbuntuコンテナが実行されていることがわかります。

2つのUbuntuコンテナを実行する

上のウィンドウで、次のコマンドを実行します。

mkdir hello-world

mkdirコマンドは新しいディレクトリを作成します。両方のコンテナのディレクトリのリストを表示するには、両方のコンテナ内でlsコマンドを実行します。

両方のコンテナ内のlsコマンドからの出力

ご覧のように、hello-worldディレクトリは、下部のウィンドウではなく、上部のターミナルウィンドウで開いているコンテナ内に存在します。同じイメージから作成されたコンテナが互いに分離されていることを証明しています。

これは理解しておくべき重要なことです。しばらくコンテナ内で作業しているシナリオを想定します。次に、コンテナを「停止」し、翌日にもう一度docker run -it ubuntuを実行します。行った動作すべてが消失したことがわかります。

前のサブセクションから覚えていると思いますが、runコマンドは毎回新しいコンテナを作成して開始します。そのため、runコマンドではなく、startコマンドを使用して、以前に作成したコンテナを起動することを忘れないでください。

カスタムイメージの作成

Dockerクライアントを使用してコンテナを操作する多くの方法をしっかり理解したところで、今度はカスタムイメージの作成方法を学習します。

このセクションでは、イメージの構築、イメージからのコンテナの作成、および他のユーザーとの共有に関する多くの重要な概念を学びます。

以降のサブセクションに進む前にVisual Studio Codeと共に公式のDocker Extensionをインストールすることをお勧めします。

イメージ作成の基本

このサブセクションでは、Dockerfileの構造と一般的な手順に焦点を当てます。Dockerfileはテキストドキュメントであり、Dockerデーモンがイメージを作成してビルドするための一連の指示が含まれています。

イメージ作成の基本を理解するために、非常にシンプルなカスタムNodeイメージを作成します。始める前に、公式のnodeイメージがどのように機能するかを紹介します。次のコマンドを実行してコンテナを実行します。

docker run -it node

Nodeイメージは、起動時にNode REPLを開始するように構成されています。REPLはインタラクティブプログラムなので、-itフラグを使用します。

docker run -it node コマンドからの出力

ここで有効なJavaScriptコードを実行できます。そのように機能するカスタムNodeイメージを作成します。

まず、コンピュータの任意の場所に新しいディレクトリを作成し、その中にDockerfileという名前の新しいファイルを作成します。コードエディター内でプロジェクトフォルダを開き、次のコードをDockerfileに配置します。

FROM ubuntu

RUN apt-get update
RUN apt-get install nodejs -y

CMD [ "node" ]

前のサブセクションから、イメージに複数のレイヤーがあることを思い出してください。Dockerfileの各行は命令であり、各命令は新しいレイヤーを作成します。

Dockerfileを1行ずつ詳しく説明します。

FROM ubuntu

すべての有効なDockerfileは、FROM命令で始まる必要があります。この命令は、新しいビルドステージを開始し、ベースイメージを設定します。ubuntuをベースイメージとして設定することにより、Ubuntuイメージのすべての機能をイメージ内で使用できるようにする必要があります。

イメージでUbuntu機能を使用できるようになったので、Ubuntuパッケージマネージャー(apt-get)を使用してNodeをインストールできます。

RUN apt-get update
RUN apt-get install nodejs -y

RUN命令は、現在のイメージ上の新しいレイヤーでコマンドを実行し、結果を保持します。したがって、次の手順では、Nodeを参照できます。これはこの手順でNodeをインストールしたためです。

CMD [ "node" ]

CMD命令の目的は、実行中のコンテナにデフォルトを提供することです。これらのデフォルトには実行可能ファイルを含めることも、実行可能ファイルを省略することもできます。その場合は、ENTRYPOINT命令を指定する必要があります。Dockerfileに含めることができるCMD命令は1つだけです。また、シングルクォーテーションは無効です。

このDockerfileからイメージをビルドするために、buildコマンドを使用します。コマンドの一般的な構文は次のとおりです。

docker build <ビルドコンテキスト>

buildコマンドにはDockerfileとビルドコンテキストが必要です。コンテキストは、指定された場所にあるファイルとディレクトリのセットです。Dockerはコンテキスト内でDockerfileを探し、それを使用してイメージを構築します。

そのディレクトリ内のターミナルウィンドウを開き、次のコマンドを実行します。

docker build .

現在のディレクトリを意味するビルドコンテキストとして.を渡しています。/src/Dockerfileのような別のディレクトリ内にDockerfileを置くと、コンテキストは./srcになります。

ビルドプロセスが完了するまでに時間がかかる場合があります。完了すると、ターミナルにテキストの壁が表示されます。

docker build . コマンドからの出力

すべてがうまくいくと、最後にSuccessfully built d901e4d15263のようなものが表示されます。このランダムな文字列はイメージIDであり、コンテナIDではありません。このイメージIDでrunコマンドを実行して、新しいコンテナを作成して立ち上げることができます。

docker run -it d901e4d15263

Node REPLはインタラクティブプログラムであるため、-itオプションが必要であることを忘れないでください。コマンドを実行したらNode REPLにアクセスする必要があります。

カスタムイメージ内のNode REPL

ここでは、公式のNodeイメージと同じように、任意の有効なJavaScriptコードを実行できます。

実行可能イメージを作成する

前のサブセクションの実行可能イメージの概念を思い出してください。イメージは通常の実行可能ファイルと同じように追加の引数を取ることができます。このサブセクションでは、作成方法を学習します。

カスタムbashイメージを作成し、前のサブセクションでUbuntuイメージで行ったように引数を渡します。空のディレクトリ内にDockerfileを作成することから始め、次のコードをその中に配置します。

FROM alpine

RUN apk add --update bash

ENTRYPOINT [ "bash" ]

alpineイメージをベースとして使用しています。Alpine Linuxは、セキュリティ指向で軽量なLinuxディストリビューションです。

Alpineにはデフォルトでbashが付属していません。したがって、2行目では、Alpineパッケージマネージャーであるapkを使用してbashをインストールします。AlpineのapkUbuntuでいうapt-getです。最後の命令は、このイメージのエントリポイントとしてbashを設定します。ご覧のとおり、ENTRYPOINT命令はCMD命令と同じです。

イメージをビルドするには、次のコマンドを実行します。

docker build .

ビルドプロセスには時間がかかる場合があります。完了したら、新しく作成されたイメージIDを取得する必要があります。

docker build . コマンドからの出力

作成されたイメージからコンテナを実行するには、runコマンドを使用します。このイメージにはインタラクティブなエントリポイントがあるため、必ず-itオプションを使用してください。

カスタムイメージ内で実行されるbash

これで、Ubuntuコンテナで行ったように、このコンテナに任意の引数を渡すことができます。すべてのファイルとディレクトリのリストを表示するには、次のコマンドを実行できます。

docker run 66e867a1504d -c ls

-c lsオプションはbashに直接渡され、コンテナ内のディレクトリのリストを返す必要があります。

docker run 66e867a1504d -c ls コマンドからの出力

-cオプションはDockerクライアントとは何の関係もありません。これはbashコマンドラインオプションです。後続の文字列からコマンドを読み取ります。

Expressアプリケーションのコンテナ化

これまでは追加のファイルを含まないイメージのみを作成しました。このサブセクションでは、ソースファイルを含むプロジェクトをコンテナ化する方法を学習します。

プロジェクトコードリポジトリのクローンを作成した場合は、express-apiディレクトリ内に移動します。これは、ポート3000番で実行され、ヒットすると単純なJSONペイロードを返すREST APIです。

このアプリケーションを実行するには、次の手順を実行する必要があります。

  1. npm installを実行して、必要な依存関係をインストールします。
  2. npm run startを実行してアプリケーションを起動します。

Dockerfileの手順を使用して上記のプロセスを複製するには、次の手順を実行する必要があります。

  1. Nodeアプリケーションを実行できるベースイメージを使用します。
  2. package.jsonファイルをコピーし、npm run installを実行して依存関係をインストールします。
  3. 必要なプロジェクトファイルをすべてコピーします。
  4. npm run startを実行してアプリケーションを起動します。

次に、プロジェクトディレクトリ内に新しいDockerfileを作成し、次の内容をファイル中に配置します。

FROM node

WORKDIR /usr/app

COPY ./package.json ./
RUN npm install

COPY . .

CMD [ "npm", "run", "start" ]

ベースイメージとしてNodeを使用しています。WORKDIRは、WORKDIRのあとのRUNCMDENTRYPOINTCOPY、およびADDのすべての命令の作業ディレクトリを設定します。これは、ディレクトリにcdするようなものです。

COPY./package.jsonを作業ディレクトリにコピーします。前の行で作業ディレクトリを設定したので、.はコンテナ内の/usr/appを参照します。package.jsonがコピーされたら、RUNを使用して必要な依存関係をすべてインストールします。

CMDでは、npmを​​実行可能ファイルとして設定し、runstartを引数として渡します。命令はコンテナ内でnpm run startとして解釈されます。

ここで、docker build .を使用してイメージをビルドし、結果としてのイメージIDを使用して新しいコンテナを実行します。アプリケーションはコンテナ内のポート3000番で実行されるため、マッピングすることを忘れないでください。

express-api アプリケーションからの応答

コンテナが正常に実行されたら、http://127.0.0.1:3000にアクセスすると、シンプルなJSONがレスポンスとして表示されます。ホストシステムから他のポートを使用した場合は、3000を書き換えてください。

ボリュームの操作

このサブセクションでは、非常に一般的なシナリオを紹介します。ReactまたはVueを使用して、モダンなフロントエンドアプリケーションで作業していると想定します。プロジェクトコードリポジトリをクローンしたら、vite-counterディレクトリ内に移動します。これは、npm init vite-appコマンドで初期化されたシンプルなVueアプリケーションです。

このアプリケーションを開発モードで実行するには、次の手順を実行する必要があります。

  1. npm installを実行して、必要な依存関係をインストールします。
  2. npm run devを実行して、アプリケーションを開発モードで起動します。

Dockerfileの手順を使用して上記のプロセスを複製するには、次の手順を実行する必要があります。

  1. Nodeアプリケーションを実行できるベースイメージを使用します。
  2. package.jsonファイルをコピーし、npm run installを実行して依存関係をインストールします。
  3. 必要なプロジェクトファイルをすべてコピーします。
  4. npm run devを実行して、アプリケーションを開発モードで起動します。

そこに新しくDockerfile.devを作成し、以下の内容を記述します。

FROM node

WORKDIR /usr/app

COPY ./package.json ./
RUN npm install

COPY . .

CMD [ "npm", "run", "dev" ]

空想ではなく、実際にpackage.jsonファイルをコピーし、依存関係をインストールして、プロジェクトファイルをコピーし、npm run devを実行して開発サーバーを起動しました。

次のコマンドを実行してイメージをビルドします。

docker build -f Dockerfile.dev .

Dockerは、ビルドのコンテキスト内でDockerfileを探すようにプログラムされています。しかし、ファイルにDockerfile.devという名前を付けたので、-fまたは--fileオプションを使用して、Dockerにファイル名を知らせる必要があります。最後の.は、以前と同様にコンテキストを示します。

docker build -f Dockerfile.dev . コマンドからの出

開発サーバーはコンテナ内のポート3000番で実行されるため、コンテナを作成して起動するときにポートをマップするようにしてください。私の方ではhttp://127.0.0.1:3000にアクセスして、アプリケーションにアクセスできます。

ボリュームなしで実行される vite-counter アプリ

これは、新しいViteアプリケーションに付属するデフォルトのコンポーネントです。ボタンを押してカウントを増やすことができます。

すべての主要なフロントエンドフレームワークには、ホットリロード機能が付属しています。開発サーバーで実行中にコードに変更を加えた場合、変更はブラウザにすぐに反映されますが、一足先にこのプロジェクトのコードに変更を加えようとしても、ブラウザには何の変更も表示されません。

まあ、その理由はかなり簡単です。コードを変更する場合は、コンテナ内のコピーではなく、ホストシステムのコードを変更します。

この問題の解決策があります。コンテナ内にソースコードのコピーを作成する代わりに、コンテナにホストから直接ファイルにアクセスさせることができます。

そうするために、Dockerには、runコマンド用の-vまたは--volumeというオプションがあります。volumeオプションの一般的な構文は次のとおりです。

docker run -v <ホストディレクトリへの絶対パス>:<コンテナの作業ディレクトリへの絶対パス> <イメージID>

pwdシェルコマンドを使用して、現在のディレクトリの絶対パスを取得できます。私のホストディレクトリパスは/Users/farhan/repos/docker/docker-handbook-projects/vite-counter、コンテナアプリケーションの作業ディレクトリパスは/usr/app、イメージIDは8b632faffb17です。だから私のコマンドは次のようになります。

docker run -p 3000:3000 -v /Users/farhan/repos/docker/docker-handbook-projects/vite-counter:/usr/app 8b632faffb17

上記のコマンドを実行すると、sh: 1: vite: not foundというエラーが表示され、依存関係がコンテナ内に存在しないことを意味します。

このようなエラーが発生しないようにするには、ホストシステムに依存関係がインストールされている必要があります。ローカルシステムのnode_modulesフォルダを削除して、再試行してください。

sh: 1: vite: not found error output

しかし、Dockerfile.devの4行目を見ると、RUN npm installが明確に記述されています。

これがなぜ起こっているのかを説明しましょう。ボリュームを使用する場合、コンテナはホストシステムから直接ソースコードにアクセスします。ご存じのとおり、ホストシステムには依存関係をインストールしていません。

依存関係をインストールすると問題を解決できますが、まったく理想的ではありません。一部の依存関係は、インストールするたびにソースからコンパイルされるためです。また、OSとしてWindowsまたはMacを使用している場合、OS用にビルドされたバイナリは、Linuxを実行しているコンテナ内では機能しません。

この問題を解決するには、Dockerが持っている2種類のボリュームについて知っておく必要があります。

  • 名前付きボリューム:これらのボリュームには、コンテナ外部からの特定のソースがあります。たとえば、-v ($PWD):/usr/appです。
  • 匿名ボリューム:これらのボリュームには特定のソースがありません(例:-v/usr/app/node_modules)。コンテナが削除されると、匿名ボリュームは手動でクリーンアップするまで残ります。

node_modulesディレクトリが上書きされないようにするには、匿名ボリューム内に配置する必要があります。これを行うには、前のコマンドを次のように変更します。

docker run -p 3000:3000 -v /usr/app/node_modules -v /Users/farhan/repos/docker/docker-handbook-projects/vite-counter:/usr/app 8b632faffb17

ここでは、新しい匿名ボリュームの追加のみを変更しました。ここでコマンドを実行すると、アプリケーションが実行されていることがわかります。何か変更したら、ブラウザですぐに変更を確認できます。デフォルトのヘッダーを少し変更しました。

ボリュームで実行されるvite-counterアプリ

このコマンドは、繰り返し実行するには少し長すぎます。長いソースディレクトリの絶対パスの代わりにシェルコマンドで代用できます。

docker run -p 3000:3000 -v /usr/app/node_modules -v $(pwd):/usr/app 8b632faffb17

$(pwd)ビットは、現在のディレクトリへの絶対パスに置き換えられます。そのため、プロジェクトフォルダ内のターミナルウィンドウが開いていることを確認してください。

マルチステージビルド

Docker v17.05で導入されたマルチステージビルドは素晴らしい機能です。このサブセクションでは、vite-counterアプリケーションを再び使用します。

前のサブセクションでは、明確に開発サーバーを実行するためのDockerfile.devファイルを作成しました。VueまたはReactアプリケーションのプロダクションビルドを作成することは、多段階ビルドプロセスの完璧な例です。

最初に、次の図でプロダクションビルドがどのように機能するかを示します。

多段階のビルドプロセス

図からわかるように、ビルドプロセスには2つのステップまたはステージがあります。それらは次のとおりです。

  1. npm run buildを実行すると、アプリケーションがひとくくりのJavaScriptCSS、およびindex.htmlファイルにコンパイルされます。プロダクションビルドは、プロジェクトルートの/distディレクトリ内で利用できます。ただし、開発バージョンとは異なり、プロダクションビルドには手の込んだサーバーが付属していません。
  2. プロダクションファイルでサーバーを立ち上げるには、Nginxを使用する必要があります。ステージ1でビルドされたファイルをNginxのデフォルトのドキュメントルートにコピーし、利用可能にします。

前の2つのプロジェクトで行ったような手順を確認したい場合は、次のようになります。

  1. Nodeアプリケーションを実行できるベースイメージ(node)を使用します。
  2. package.jsonファイルをコピーし、npm run installを実行して依存関係をインストールします。
  3. 必要なプロジェクトファイルをすべてコピーします。
  4. npm run buildを実行してプロダクションビルドを作成します。
  5. 本番ファイルの実行を可能にする別のベースイメージ(nginx)を使用します。
  6. 本番ファイルを/distディレクトリからデフォルトのドキュメントルート(/usr/share/nginx/html)にコピーします。

さあ、作業に取り掛かりましょう。vite-counterプロジェクトディレクトリ内に新しいDockerfileを作成します。Dockerfileの内容は次のとおりです。

FROM node as builder

WORKDIR /usr/app

COPY ./package.json ./
RUN npm install

COPY . .

RUN npm run build

FROM nginx

COPY --from=builder /usr/app/dist /usr/share/nginx/html

まず複数のFROMがあることに気づいたかもしれません。マルチステージビルドプロセスでは、複数のFROMを使用することができます。最初のFROMは、nodeをベースイメージとして設定し、依存関係をインストールし、すべてのプロジェクトファイルをコピーして、npm run buildを実行します。最初のステージをbuilder命名します。

次に、第2段階では、ベースイメージとしてnginxを使用します。第1ステージで構築された/usr/app/distディレクトリから第2ステージのusr/share/nginx/htmlディレクトリにすべてのファイルをコピーします。COPY--fromオプションを使用すると、ステージ間でファイルをコピーできます。

イメージをビルドするには、次のコマンドを実行します。

docker build .

今回はDockerfileという名前のファイルを使用しているため、ファイル名を明示的に宣言する必要はありません。ビルドプロセスが完了したら、イメージIDを使用して新しいコンテナを実行します。Nginxはデフォルトでポート80番で実行されるため、マッピングすることを忘れないでください。

コンテナを正常に起動したら、http://127.0.0.1:80にアクセスすると、カウンターアプリが実行されているのがわかります。ホストシステムから他のポートを使用した場合は、80を書き換えてください。

実行中のdocker-counterアプリケーションのプロダクションビルド

このマルチステージビルドプロセスからの出力イメージは、ビルドされたファイルのみを含み、追加のデータを含まないNginxベースのイメージです。その結果、サイズが最適化され、軽量になっています。

ビルドされたイメージをDocker Hubにアップロードする

あなたはすでにかなり多くの画像を構築しました。このサブセクションでは、画像のタグ付けとDocker Hubへのアップロードについて学びます。先に進んで、無料のアカウントDocker Hubにサインアップしてください。

Docker Hubでサインアップ

アカウントを作成したら、Dockerメニューを使用してログインします。

Dockerメニューを使用してサインイン

または、ターミナルからコマンドを使用してログインできます。コマンドの一般的な構文は次のとおりです。

docker login -u <DockerのID>  --password <Dockerのパスワード>

ログインが成功すると、ターミナルにLogin Succeededのようなものが表示されます。

docker login コマンドを使用してサインインします

これでイメージをアップロードする準備が整いました。イメージをアップロードするには、まずそれらにタグを付ける必要があります。プロジェクトコードリポジトリのクローンを作成した場合は、vite-counterプロジェクトフォルダー内のターミナルウィンドウを開きます。

buildコマンドで-tまたは--tagオプションを使用してイメージにタグを付けることができます。このオプションの一般的な構文は次のとおりです。

docker build -t <タグ> <ビルドのコンテキスト>

タグの一般的な規則は次のとおりです。

<DockerのID>/<イメージ名>:<イメージのバージョン>

私のDocker IDはfhsinchyなので、画像にvite-counterという名前を付けたい場合、コマンドは次のようになります。

docker build -t fhsinchy/vite-controller:1.0 .

コロンの後にバージョンを定義しない場合、latestが自動的に使用されます。すべてがうまくいけば、ターミナルにSuccessfully tagged fhsinchy/vite-controller:1.0のようなものが表示されるはずです。私の場合、バージョンを定義していません。

このイメージをHubにアップロードするには、pushコマンドを使用できます。コマンドの一般的な構文は次のとおりです。

docker push <DockerのID>/<イメージタグとバージョン>

fhsinchy/vite-counterイメージをアップロードするには、コマンドは次のようにする必要があります。

docker push fhsinchy/vite-counter

プッシュが完了すると、次のようなテキストが表示されます。

イメージがDocker Hubに正常にプッシュされました

Hubのイメージは誰でも見ることができます。

Docker Hubでのイメージの表示

このイメージからコンテナを実行するための一般的な構文は次のとおりです。

docker run <DockerのID>/<イメージタグとバージョン>

このアップロードされたイメージを使用してvite-counterアプリケーションを実行するには、次のコマンドを実行します。

docker run -p 80:80 fhsinchy/vite-counter

そして、あなたはvite-counterアプリケーションが以前と同じように実行されているのを見るはずです。

アップロードされた画像を使用して実行されているvite-counterアプリケーション

任意のアプリケーションをコンテナ化し、Docker Hubまたはその他のレジストリを介してそれらを配布して、実行またはデプロイをとても簡単にすることができます。

Docker Composeを使用したマルチコンテナアプリケーションの操作

これまでは、1つのコンテナのみで構成されるアプリケーションのみを扱ってきました。 次に、複数のコンテナを持つアプリケーションを想定します。おそらく、データベースサービスが適切に機能するために必要なAPI、またはバックエンドAPIとフロントエンドアプリケーションを一緒に使用する必要があるフルスタックアプリケーションなどがあります。

このセクションでは、Docker Composeというツールを使用して、このようなアプリケーションを操作する方法について学びます。

Dockerのドキュメントによると下記のように記述されています。

Composeは、マルチコンテナのDockerアプリケーションを定義して実行するためのツールです。Composeでは、YAMLファイルを使用してアプリケーションサービスを設定します。次に、1つのコマンドで、構成からすべてのサービスを作成して開始します。

Composeはすべての環境で機能しますが、開発とテストにより重点を置いています。本番環境でComposeを使用することはお勧めしません。

Docker Composeの基本

プロジェクトコードリポジトリのクローンを作成した場合は、notes-apiディレクトリ内に移動します。これは、ノートを作成、読み取り、更新、および削除できる単純なCRUD APIです。アプリケーションは、データベースシステムとしてPostgreSQLを使用します。

プロジェクトにはすでにDockerfile.devファイルがありました。ファイルの内容は次のとおりです。

FROM node:lts

WORKDIR /usr/app

COPY ./package.json .
RUN npm install

COPY . .

CMD [ "npm", "run", "dev" ]

前のセクションで書いたものと同じです。package.jsonファイルをコピーし、依存関係をインストールして、プロジェクトファイルをコピーし、npm run devを実行して開発サーバーを起動します。

Composeの使用は、基本的に3つのステップのプロセスです。

  1. Dockerfileを使用し、アプリを定義するので、どこでも再現できます。
  2. アプリを構成するサービスをdocker-compose.ymlで定義して、隔離された環境で一緒に実行できるようにします。
  3. docker-compose upを実行すると、Composeが起動し、アプリ全体が実行されます。

サービスは基本的に、いくつかの追加要素を持つコンテナです。最初のYMLファイルを一緒に書き始める前に、このアプリケーションの実行に必要なサービスをリストアップしましょう。2つしかありません。

  1. api - プロジェクトルートのDockerfile.devファイルを使用して実行されるExpressアプリケーションコンテナ。
  2. db - 公式のpostgresイメージを使用して実行されるPostgreSQLインスタンス

プロジェクトルートで新しいdocker-compose.ymlファイルを作成し、最初のサービスを一緒に定義しましょう。.ymlまたは.yaml拡張子を使用できます。どちらも問題なく動作します。最初にコードを記述してから、コードを1行ずつ詳しく説明します。dbサービスのコードは次のとおりです。

version: "3.8"

services: 
    db:
        image: postgres:12
        volumes: 
            - ./docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
        environment:
            POSTGRES_PASSWORD: 63eaQB9wtLqmNBpg
            POSTGRES_DB: notesdb

すべての有効なdocker-compose.ymlファイルは、ファイルバージョンを定義することから始まります。執筆時点では、3.8が最新バージョンです。最新バージョンはこちらで調べることができます。

YMLファイル内のブロックは、インデントによって定義されます。私は各ブロックを通して、それらが何をするかを説明します。

servicesブロックは、アプリケーション内の各サービスまたはコンテナの定義を保持します。dbはservicesブロック内のサービスです。

dbブロックはアプリケーションの新しいサービスを定義し、コンテナを開始するために必要な情報を保持します。コンテナを実行するには、すべてのサービスに事前に構築されたイメージまたはDockerfileが必要です。dbサービスには、公式のPostgreSQLイメージを使用しています。

プロジェクトルートのdocker-entrypoint-initdb.dディレクトリには、データベースのテーブルをセットアップするためのSQLファイルが含まれています。このディレクトリは、初期化スクリプトを保持するためのものです。docker-compose.ymlファイル内のディレクトリをコピーする方法はないため、ボリュームを使用する必要があります。

environmentブロックは環境変数を保持します。有効な環境変数のリストは、Docker Hubのpostgres image pageにあります。POSTGRES_PASSWORD変数はサーバーのデフォルトのパスワードを設定し、POSTGRES_DBは指定された名前で新しいデータベースを作成します。

次に、apiサービスを追加します。次のコードをファイルに追加します。インデントとdbサービスを一致させるように注意してください。

    ##
    ## インデントを調整してください
    ##
    
    api:
        build:
            context: .
            dockerfile: Dockerfile.dev
        volumes: 
            - /usr/app/node_modules
            - ./:/usr/app
        ports: 
            - 3000:3000
        environment: 
            DB_CONNECTION: pg
            DB_HOST: db ## データベースサービスと同じ名前を指定
            DB_PORT: 5432
            DB_USER: postgres
            DB_DATABASE: notesdb
            DB_PASSWORD: 63eaQB9wtLqmNBpg

apiサービス用のビルド済みイメージはありませんが、Dockerfile.devファイルがあります。buildブロックはビルドのcontextと使用するDockerfileのfilenameを定義します。ファイルの名前がDockerfileのみの場合、filenameは不要です。

ボリュームのマッピングは、前のセクションで見たものと同じです。node_modulesディレクトリ用の1つの匿名ボリュームとプロジェクトルート用の1つの名前付きボリュームです。

ポートマッピングも前のセクションと同じように機能します。構文は<ホストシステムポート>:<コンテナポート>です。コンテナのポート3000番をホストシステムのポート3000番にマッピングしています。

environmentブロックでは、データベース接続のセットアップに必要な情報を定義しています。アプリケーションはKnex.jsをORMとして使用して、データベースに接続するためにこれらの情報を必要とします。DB_PORT: 5432DB_USER: postgresはすべてのPostgreSQLサーバーのデフォルトです。DB_DATABASE: notesdbおよびDB_PASSWORD: 63eaQB9wtLqmNBpgは、dbサービスの値と一致する必要があります。DB_CONNECTION: pgは、PostgreSQLを使用していることをORMに示します。

docker-compose.ymlファイルで定義されているすべてのサービスは、サービス名を使用してホストとして使用できます。そのため、apiサービスは、127.0.0.1のような値ではなくホストとして扱うことで、実際にdbサービスに接続できます。そのため、DB_HOSTの値をdbに設定しています。

docker-compose.ymlファイルが完成したので、アプリケーションを起動します。Composeアプリケーションは、dokcer-composeと呼ばれるCLIツールを使用してアクセスできます。Compose用のdocker-composeCLIは、DockerでいうdockerCLIです。サービスを開始するには、次のコマンドを実行します。

docker compose up

コマンドを実行すると、docker-compose.ymlファイルを読み込み、各サービスのコンテナを作成して開始します。先に進んで、コマンドを実行してください。サービスの数によっては、起動プロセスに時間がかかる場合があります。

完了すると、ターミナルウィンドウのすべてのサービスからのログが表示されます。

docker-compose up コマンドからの出力

アプリケーションはhttp://127.0.0.1:3000アドレスで実行されている必要があり、アクセスすると、次のようなJSONがレスポンスとして表示されます。

http://127.0.0.1:3000

APIには完全なCRUD機能が実装されています。エンドポイントについて知りたい場合は、/tests/e2e/api/routes/notes.test.jsファイルを参照してください。

upコマンドは、サービスのイメージが存在しない場合に自動的に作成します。イメージの再構築を強制したい場合は、upコマンドで--buildオプションを使用できます。ターミナルウィンドウを閉じるか、ctrl + cを押すと、サービスを停止できます。

デタッチモードでサービスを実行する

すでに述べたように、サービスはコンテナであり、他のコンテナと同様に、サービスはバックグラウンドで実行できます。デタッチモードでサービスを実行するには、upコマンドで-dまたは--detachオプションを使用できます。

現在のアプリケーションをデタッチモードで起動するには、次のコマンドを実行します。

docker-compose up -d

今回は、前のサブセクションで見た長いテキストの壁は表示されません。

docker-compose up -d コマンドからの出力

なおhttp://127.0.0.1:3000APIにアクセスできるはずです。

サービスの一覧表示

docker psコマンドと同様に、Composeには独自のpsコマンドがあります。主な違いは、docker-compose psコマンドは特定のアプリケーションのコンテナ部分のみを一覧表示することです。notes-apiアプリケーションの一部として実行されているすべてのコンテナを一覧表示するには、プロジェクトルートで次のコマンドを実行します。

docker-compose ps

プロジェクトディレクトリ内でコマンドを実行することが重要です。それ以外の場合は実行されません。コマンドからの出力は次のようになります。

docker-compose ps コマンドからの出力

Composeのpsコマンドはデフォルトで任意の状態のサービスを表示します。-a--allなどのオプションを使用する必要はありません。

実行中のサービス内でコマンドを実行する

notes-apiアプリケーションが実行中で、dbサービス内のpsqlCLIアプリケーションにアクセスするとします。それを行うためにexecと呼ばれるコマンドがあります。コマンドの一般的な構文は次のとおりです。

docker-compose exec <サービス名> <コマンド>

サービス名はdocker-compose.ymlファイルにあります。psqlCLIアプリケーションを起動するための一般的な構文は次のとおりです。

psql <データベース> <ユーザー名>

データベース名がnotesdbで、ユーザーがpsqlであるdbサービス内でpsqlアプリケーションを開始するには、次のコマンドを実行する必要があります。

docker-compose exec db psql notesdb postgres

psqlアプリケーションに直接アクセスする必要があります。

docker-compose exec db psql notesdb postgres コマンドからの出力

ここで有効なpostgresコマンドを実行できます。プログラムを終了するには、\qと入力してEnterキーを押します。

実行中のサービス内でシェルを開始する

execコマンドを使用して、実行中のコンテナ内でシェルを起動することもできます。コマンドの一般的な構文は次のとおりです。

docker-compose exec <サービス名> sh

コンテナに付属している場合は、shの代わりにbashを使用できます。apiサービス内でシェルを開始するには、コマンドは次のようにする必要があります。

docker-compose exec api sh

これにより、apiサービス内のシェルに直接アクセスできます。

docker-compose exec api sh コマンドからの出力

そこで、任意の有効なシェルコマンドを実行できます。exitコマンドを実行することで終了できます。

実行中のサービスからログにアクセスする

コンテナからログを表示する場合は、ダッシュボードが非常に役立ちます。

Dockerダッシュボードのログ

また、logsコマンドを使用して、実行中のサービスからログを取得することもできます。コマンドの一般的な構文は次のとおりです。

docker-compose logs <サービス名>

apiサービスからログにアクセスするには、次のコマンドを実行します。

docker-compose logs api

ターミナルウィンドウにテキストの壁が表示されるはずです。

docker-compose logs api コマンドからの出力

これはログ出力の一部にすぎません。-fまたは--followオプションを使用すると、サービスの出力ストリームにフックしてリアルタイムでログを取得できます。以降のログは、ctrl + cを押すかウィンドウを閉じて終了しない限り、ターミナルに即座に表示されます。ログウィンドウを終了しても、コンテナは実行を続けます。

実行中のサービスを停止する

ターミナル上で実行されているサービスは、ターミナルウィンドウを閉じるか、ctrl + cを押すことで停止できます。バックグラウンドでサービスを停止するには、いくつかのコマンドを使用できます。一つ一つ説明していきます。

  • docker-compose stop - SIGTERMシグナルを送信することにより、実行中のサービスを適切に停止しようとします。サービスが制限時間内に停止しない場合、SIGKILLシグナルが送信されます。
  • docker-compose kill - SIGKILLシグナルを送信して実行中のサービスを即座に停止します。SIGKILLシグナルは受信者が無視することはできません。
  • docker-compose down - SIGTERMシグナルを送信して実行中のサービスを適切に停止しようとし、その後コンテナを削除します。

サービスのコンテナを保持したい場合は、stopコマンドを使用できます。コンテナも削除する場合は、downコマンドを使用します。

フルスタックアプリケーションの作成

このサブセクションでは、ノートAPIにフロントエンドアプリケーションを追加し、それを完全なアプリケーションに変換します。このサブセクションのDockerfile.devファイル(nginxサービスのファイルを除く)は、前のサブセクションですでに見た他のファイルと同一であるため、説明しません。

プロジェクトコードリポジトリのクローンを作成した場合は、fullstack-notes-applicationディレクトリ内に移動します。プロジェクトルート内の各ディレクトリには、各サービスのコードと対応するDockerfileが含まれています。

docker-compose.ymlファイルから始める前に、アプリケーションがどのように機能するかを示す図を見てみましょう。

フルスタックノートアプリケーションの内部動作

以前のように直接リクエストを受け入れる代わりに、このアプリケーションでは、すべてのリクエストが最初にNginxサーバーによって受信されます。Nginxは、リクエストされたエンドポイントに/apiが含まれているかどうかを確認します。はいの場合、Nginxはリクエストをバックエンドにルーティングします。そうでない場合、Nginxはリクエストをフロントエンドにルーティングします。

これを行う理由は、フロントエンドアプリケーションを実行すると、コンテナ内では実行されないためです。コンテナから提供されるブラウザで実行されます。その結果、Composeネットワークが期待どおりに機能せず、フロントエンドアプリケーションがapiサービスを見つけることができません。

一方、nginxはコンテナ内で実行され、アプリケーション全体でさまざまなサービスと通信できます。

ここではNginxの構成については触れません。そのトピックは、この記事の範囲外ですが興味があれば/nginx/default.confファイルを読んでみてください。/nginx/Dockerfile.devのコードは次のとおりです。

FROM nginx:stable

COPY ./default.conf /etc/nginx/conf.d/default.conf

コンテナ内の設定ファイルを/etc/nginx/conf.d/default.confにコピーするだけです。

使い慣れたサービスを定義して、docker-compose.ymlファイルの記述をしてみましょう。dbおよびapiサービス。プロジェクトルートにdocker-compose.ymlファイルを作成し、そこに次のコードを配置します。

version: "3.8"

services: 
    db:
        image: postgres:12
        volumes: 
            - ./docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
        environment:
            POSTGRES_PASSWORD: 63eaQB9wtLqmNBpg
            POSTGRES_DB: notesdb
    api:
        build: 
            context: ./api
            dockerfile: Dockerfile.dev
        volumes: 
            - /usr/app/node_modules
            - ./api:/usr/app
        environment: 
            DB_CONNECTION: pg
            DB_HOST: db ## データベースサービスと同じ名前を指定
            DB_PORT: 5432
            DB_USER: postgres
            DB_DATABASE: notesdb
            DB_PASSWORD: 63eaQB9wtLqmNBpg

ご覧のとおり、これら2つのサービスは前のサブセクションとほぼ同じです。唯一の違いは、apiサービスのcontextです。これは、そのアプリケーションのコードがapiという名前の専用ディレクトリ内にあるためです。また、サービスを直接公開したくないため、ポートマッピングはありません。

次に定義するサービスはclientサービスです。次のコードを構成ファイルに追加します。

    ##
    ## インデントは調整してください
    ##
    
    client:
        build:
            context: ./client
            dockerfile: Dockerfile.dev
        volumes: 
            - /usr/app/node_modules
            - ./client:/usr/app
        environment: 
            VUE_APP_API_URL: /api

サービスにclientという名前を付けます。buildブロック内では、/clientディレクトリをcontextとして設定し、それにDockerfile名を付けています。

ボリュームのマッピングは、前のセクションで見たものと同じです。node_modulesディレクトリ用の1つの匿名ボリュームとプロジェクトルート用の1つの名前付きボリュームです。

environtment内のVUE_APP_API_URL変数の値は、clientからapiサービスへの各リクエストに追加されます。このようにして、Nginxは異なるリクエストを区別し、それらを適切に再ルーティングすることができます。

apiサービスと同様に、このサービスも公開したくないため、ここにはポートマッピングはありません。

アプリケーションの最後のサービスはnginxサービスです。これを定義するには、次のコードを構成ファイルに追加します。

    ##
    ## インデントを調整してください
    ##
    
    nginx:
        build:
            context: ./nginx
            dockerfile: Dockerfile.dev
        ports: 
            - 80:80

Dockerfile.devの内容はすでに話しました。サービスにはnginxという名前を付けます。buildブロック内では、/nginxディレクトリをcontextとして設定し、それにDockerfile名を付けています。

すでに図で示したように、このnginxサービスはすべてのリクエストを処理します。したがって、それを公開する必要があります。Nginxはデフォルトでポート80番で実行されます。コンテナ内のポート80番をホストシステムのポート80番にマッピングしています。

docker-compose.ymlファイルが完成したので、サービスを実行します。次のコマンドを実行して、すべてのサービスを開始します。

docker-compose up

ここでhttp://localhost:80にアクセスしてみましょう!

http://localhost:80/

ノートを追加および削除して、アプリケーションが正しく動作するかどうかを確認してください。マルチコンテナアプリケーションはこれよりもはるかに複雑になる可能性がありますが、この記事ではこれで十分です。

まとめ

この記事を読んでくださった皆さんに心から感謝します。あなたがこの時間を楽しんで、Dockerのすべての本質を学んだことを願っています。

今後の最新記事をキャッチアップするなら、フォローしてください。@frhnhsin


筆者紹介

Farhan Hasin Chowdhury

レストラン管理システムを開発するFoodQoでテックリードを務める傍ら、NPOで運営されるプログラミング情報コミュニティ「freeCodeCamp」にてライター活動も行っている。

翻訳者紹介

satoru

大学を半年で中退後、Webエンジニアとして就職。チームラボやダイヤモンドメディアを経てシリコンバレーを訪問。世界的な企業でエンジニアリングを学び、現在は教会にコントリビュートするエンジニアとしてフリーランスで活動している。このブログの運営者でもある。