はじめに
この1年と少しDockerを使用してきましたが、正直まだ基本的なところで理解できていないところもありました。
なので改めてドキュメントを1から読み、まとめてみました。
主に以下のドキュメントを読み、まとめています。説明の順番や内容等は一部ドキュメントと異なる部分があります。
Dockerの概要と使用することで得られるメリット
- Dockerとはアプリケーションを開発、デプロイ、運用するためのオープンなプラットフォームです。
- Dockerを使用することで、アプリケーションをインフラから分離することができ、アプリケーションの開発をスムーズに進めることができます。
- Dockerを使用することで、アプリケーションを管理するのと同じようにインフラの管理をすることができます。
Dockerが提供する機能について
-
Dockerは、アプリケーションをパッケージ化し、コンテナと呼ばれるゆるやかに隔離された環境で実行する機能を提供します。この隔離された環境とセキュリティにより、特定のホスト上で多くのコンテナを同時に実行することができます。
-
コンテナは軽量で、アプリケーションの実行に必要なものがすべて含まれているため、**ホストに現在インストールされているものに頼る必要がありません。**作業中にコンテナを簡単に共有することができ、共有した全員が同じ方法で動作する同じコンテナを手に入れることができます。
-
Docker社のコンテナベースのプラットフォームは、移植性の高いワークロードを可能にします。Dockerコンテナは、開発者のローカルラップトップ、データセンター内の物理マシンや仮想マシン、クラウド事業者、あるいはそれらが混在する環境でも実行することができます。
Dockerが使用している技術について
Dockerは、プログラミング言語「Go」で書かれており、**Linuxカーネルのいくつかの機能を利用して機能を実現しています。**Dockerは、namaspaceと呼ばれる技術を用いて、コンテナと呼ばれる隔離された作業空間を提供しており、コンテナを実行すると、Dockerはそのコンテナのための名前空間のセットを作成します。
Dockerのアーキテクチャについて
-
Dockerはクライアント・サーバー型のアーキテクチャを採用しています。
-
DockerクライアントはDockerデーモンと対話し、デーモンはDockerコンテナの構築、実行、配布などの処理を行います。
-
Dockerクライアントとデーモンは同じシステム上で動作しますが、DockerクライアントをリモートのDockerデーモンに接続することもできます。Dockerクライアントとデーモンは、REST APIを使って、UNIXソケットやネットワークインターフェースを介して通信し、もうひとつのDockerクライアントはDocker Composeでこれはコンテナのセットで構成されたアプリケーションを扱うことができます。
アーキテクチャのイメージに関しては、以下のDockerのドキュメントの図が分かりやすいです。
主な登場人物(用語)
-
Docker daemaon (dockerd)
- Dockerデーモン(dockerd)は、Docker APIリクエストをリッスンし、イメージ、コンテナ、ネットワーク、ボリュームなどのDockerオブジェクトを管理します。
-
Docker client (docker)
- Dockerクライアント(docker)は、DockerユーザーがDockerと対話するための主な手段です。docker runなどのコマンドを使用すると、クライアントはこれらのコマンドをdockerdに送信し、dockerdがそれを実行します。dockerコマンドはDocker APIを使用します。Dockerクライアントは、複数のデーモンと通信することが可能です。
-
Docker registries
- Dockerレジストリは、Dockerイメージを保存します。Docker Hubは誰もが利用できるパブリックレジストリで、DockerはデフォルトでDocker Hub上のイメージを探すように設定されています。
-
Docker objects
- イメージ、コンテナ、ネットワーク、ボリュームなどがあります。
Docker objectsについて具体的に説明します
IMAGESとは
**イメージは、Dockerコンテナを作成するための手順が記載された読み取り専用のテンプレートのことです。**多くの場合、イメージは他のイメージをベースにして、さらにカスタマイズされています。例えば、ubuntuのイメージをベースにして、Apache Webサーバーとアプリケーションをインストールし、アプリケーションの実行に必要な設定情報を追加したイメージを作成することができるます。独自のイメージを作成するには、イメージの作成と実行に必要な手順を定義する簡単な構文を持つDockerfileを作成します。
Dockerfileの各命令は、イメージの中にレイヤーを作成します。Dockerfileを変更してイメージを再構築すると、変更されたレイヤーだけが再構築されます。これが、他の仮想化技術と比較して、イメージが軽量、小型、高速である理由の一つです。
コンテナの実行時には、孤立したファイルシステムが使用されます。この**カスタムファイルシステムもコンテナイメージによって提供されます。**イメージにはコンテナのファイルシステムが含まれているため、アプリケーションの実行に必要なすべてのもの(すべての依存関係、設定、スクリプト、バイナリなど)が含まれていなければなりません。イメージには、環境変数、実行するデフォルトコマンド、その他のメタデータなど、コンテナのその他の設定も含まれています。
CONTAINERSとは
コンテナとは、イメージの実行可能なインスタンスのことです。
もう少し具体的に言うと、コンテナとはホストマシン上の他のすべてのプロセスから分離された、あなたのマシン上の別のプロセスのことです。この隔離は、カーネルのnamespaceとcgroupsを利用したもので、Linuxには昔からある機能です。Dockerは、これらの機能を親しみやすく、使いやすくするために努力してきました。
Docker APIやCLIを使って、コンテナの作成、起動、停止、移動、削除を行うことができます。また、コンテナを1つまたは複数のネットワークに接続したり、ストレージを取り付けたり、現在の状態に基づいて新しいイメージを作成したりすることも可能です。デフォルトでは、コンテナは他のコンテナやそのホストマシンから隔離されています。コンテナのネットワーク、ストレージ、その他の基本的なサブシステムを他のコンテナやホストマシンからどの程度隔離するかを制御することができます。
コンテナは、そのイメージと、コンテナの作成時や起動時に指定した設定オプションによって定義され、コンテナが削除されると、永続的なストレージに保存されていない状態の変更はすべて消えます。
コンテナ作成の例
例1
docker run -i -t ubuntu /bin/bash
-iは--interactive
のことです。Keep STDIN open even if not attached. つまりインタラクティブモード
のこと。-tは、--tty
のことです。Allocate a pseudo-TTY. つまり仮想TTYを割り当てて起動します。
docker run --help
で各種オプションの説明を見ることができます。
このコマンドを打った際に、実際に実行されることは以下の流れです。
- ubuntuのイメージがローカルにない場合は、
docker pull ubuntu
を手動で実行したように、Dockerは設定されたレジストリからイメージを引き出します。 - 次に
docker container create
を手動で実行したように、新しいコンテナが作成されます。 - そしてDockerは、その最終層として、**コンテナに読み書き可能なファイルシステムを割り当てます。**これにより、実行中のコンテナは、そのローカルファイルシステムにファイルやディレクトリを作成したり、変更したりすることができます。
- 今回はネットワークオプションを指定していないのでDockerは、コンテナをデフォルトネットワークに接続するためのネットワークインターフェースを作成します。これには、コンテナへのIPアドレスの割り当ても含まれます。デフォルトでは、コンテナはホストマシンのネットワーク接続を使って外部ネットワークに接続できます。
- Dockerはコンテナを起動し、/bin/bashを実行します。コンテナはインタラクティブに実行され、ターミナルに接続されているため(-iおよび-tフラグによる)、キーボードを使って入力を行い、出力はターミナルに記録されます。
- exitと入力して/bin/bashコマンドを終了させると、**コンテナは停止します
が削除されません。**再度起動するか、削除することができます。
例2
docker run -d -p 80:80 docker/getting-started
-
-d
はコンテナをdetachedモード(バックグラウンド)で動かすようにしています。 -
-p 80:80
は、ホストのport80番をコンテナのport80番にマッピングしています。 -
docker/getting-started
は使用するイメージです。
Go言語を使用してコンテナを0から作る様子の講演会は以下のリンクから視聴することができます!
またdocker run
のリファレンスについては以下に書いています。
Dockerfileの書き方の例
Dockerfileとは、コンテナイメージを作成するためのテキストベースの指示スクリプトです。
こちらをcloneします。cloneできたら、appディレクトリに移動し、Dockerfileという名前でファイルを作成します。そこに以下のように書いてみます。
FROM node:12-alpine
RUN apk add --no-cache python g++ make
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]
ターミナルを開いて、Dockerfileのあるappディレクトリに移動します。次に、docker buildコマンドを使ってコンテナイメージを構築します。
docker build -t getting-started .
docker run -dp 3000:3000 getting-started
//-dpは-dと-pを繋げて書いているものです
- node:12-alpineイメージのダウンロードが開始されます。
- イメージがダウンロードされた後、アプリケーションをコピーし、yarnを使ってアプリケーションの依存関係をインストールします。CMDディレクティブは、このイメージからコンテナを起動する際に実行するデフォルトのコマンドを指定します。
- つぐに-tフラグでイメージにタグを付けます。これは単に、最終的なイメージの人間が読める名前と考えてください。イメージにgetting-startedという名前を付けたので、コンテナを実行するときにそのイメージを参照することができます。
- docker buildコマンドの最後にある
.
は、DockerがカレントディレクトリでDockerfileを探すように指示します。 - docker runコマンドによって、上記で作成されたgetting-startedイメージを使用してコンテナが起動します。
- http://localhost:3000 にアクセスするとアプリケーションを見ることができます。
Dokcerコンテナを操作するその他コマンド
//コンテナのidを取得する
docker ps
//コンテナをストップする
docker stop <the-container-id>
//ストップ済のコンテナを削除する
docker rm <the-container-id>
//動いているコンテナをストップして削除する
docker rm -f <the-container-id>
現状のサンプルアプリでの問題点
上の例で簡単なtodoアプリを起動したが、現状のままではコードを変更するたびに、コンテナをrestartする必要があります。またコンテナを起動するたびにtodoリストが空に戻ってしまいます。
上記の理由とファイルシステムについて
コンテナが実行されると、イメージのさまざまなレイヤーをファイルシステムとして使用します。また、各コンテナには、ファイルの作成、更新、削除を行うための独自の「スクラッチスペース」が用意されています。同じイメージを使用していても、他のコンテナでは変更が反映されません。
各コンテナが起動するたびにイメージ定義からスタートします。コンテナではファイルの作成、更新、削除が可能ですが、コンテナが削除されるとそれらの変更は失われ、すべての変更はそのコンテナに隔離されてしまいます。ボリュームを使えば、このような状況を変えることができます。
Volumeを使用してデータを永続化しよう
Volumeは、コンテナの特定のファイルシステムパスをホストマシンに戻す機能を提供します。コンテナ内のディレクトリをマウントすると、そのディレクトリの変更がホストマシンにも反映されます。コンテナの再起動にかかわらず、同じディレクトリをマウントすれば、同じファイルが表示されます。
より詳しい説明はこちら。
Volumeには2種類あります。
named volumeとbind mountです。
named volumeについて
現在使用しているtodoアプリはSQLite Databaseを使用してデータを保存しています。実際にはこちらにのファイルに保存されています。/etc/todos/todo.db
named volume(単なるデータのbucketと考えて大丈夫)を使う手順は以下。
- ボリュームを作成
docker volume create todo-db
- データが保存されているディレクトリにアタッチ(マウントと呼ばれる)して、データを永続化する。
docker run -dp 3000:3000 -v todo-db:/etc/todos getting-started
-
-v todo-db:/etc/todos
のところでtodo-dbというbucketを/etc/todosにマウントしています。ちなみにdocker volume create todo-dbを省略しても、Dockerは私たちがnamed volumeを使用したいことを認識し、自動的にボリュームを作成してくれます。
- コンテナがtodo.dbファイルに書き込むと、ボリューム内のホストにデータが保存されます。
- 実際にbucketがどこに作成されているのかが気になったら、以下のコマンドを実行すると良いです。
docker volume inspect todo-db
//実行結果 Mountpointが実際にデータが格納されている場所です
[
{
"CreatedAt": "2021-04-11T02:18:36Z",
"Driver": "local",
"Labels": {},
"Mountpoint": "/var/lib/docker/volumes/todo-db/_data",
"Name": "todo-db",
"Options": {},
"Scope": "local"
}
]
named volumeはデータがどこに保存されているかを気にする必要がないため、単にデータを保存したい場合に最適です。
bind mountについて
bind mountでは、ホスト上の正確なマウントポイントを制御します。バインドマウントは、データの永続化にも使用できますが、多くの場合、**コンテナに追加データを提供するために使用されます。**アプリケーションの開発では、バインドマウントを使ってソースコードをコンテナにマウントすることで、コードの変更をコンテナに反映させ、その変更をすぐに確認できるようにします。
docker run -dp 3000:3000 \
-w /app -v "$(pwd):/app" \
node:12-alpine \
sh -c "yarn install && yarn run dev"
-
-w /app
では、作業ディレクトリ、つまりコマンドが実行される現在のディレクトリを設定します。 -
-v "$(pwd):/app"
では、コンテナ内のホストからカレントディレクトリを/appディレクトリにバインドマウントします。
こうすることでカレントディレクトリのファイルに何か変更を加えても、コンテナを再起動する必要がなく、コンテナ内のファイルにも変更が反映されます。
bind mountを使用することは、ローカル開発のセットアップでは非常に一般的です。そのメリットは、**開発マシンにすべてのビルドツールや環境をインストールする必要がないことです。**たった1つのdocker runコマンドで、開発環境が引き出され、すぐに使えるようになります。また次に説明するDocker Composeを使用することで、これはコマンドを単純化するのに役立ちます。
Multi container appsで役割ごとにコンテナを分けよう
上記までの説明では1つのコンテナのみでアプリを起動してきました。
しかし1つのコンテナでは解決できない問題があります。例えば以下のような問題です。
- APIやフロントエンド、データベースでバージョンを分離したい場合。
- ローカルではデータベース用のコンテナを使うが、本番ではデータベース用のマネージドサービスを使いたいと思う場合。
そういった場合、
API用のコンテナ、フロントエンド用のコンテナ、データベース用のコンテナと分けることができます。
例えば先程のtodoアプリでsqliteではなくMySQLを使用する場合を考えてみます。
以下のような図でコンテナ同士が通信する必要があります。
Container networkingについて
コンテナは、デフォルトでは分離して実行され、同じマシン上の他のプロセスやコンテナについては何も知らない状態です。このような状態で、あるコンテナが別のコンテナと対話できるようにするには同じネットワークにコンテナを設置する必要があります。
2つのコンテナが同じネットワーク上にあれば、お互いに通信することができます。そうでない場合は通信できません。
- ネットワークを作成する。
docker network create todo-app
- MySQLコンテナを起動し、ネットワークに接続します。コマンドは以下のようになります。
-
--network todo-app
で、todo-appというネットワークに接続されています。-e
ではデータベースの初期化に使用するいくつかの環境変数を定義しています。
-
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
- todoアプリを起動します。
-
--network todo-app
で、todo-appネットワークに接続しています。MYSQL_HOST=mysql
で、先程--network-alias mysql
として設定したmysql aliasをHOSTとしています。通常は、mysqlは有効なホスト名ではありませんが、Dockerはネットワークエイリアスを持つコンテナのIPアドレスに解決することができます。
-
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 Composeを使用して楽をしよう
**Docker Composeは、マルチコンテナ・アプリケーションの定義と共有を支援するために開発されたツールです。**Composeでは、YAMLファイルを作成してサービスを定義し、1つのコマンドですべてを起動したり、すべてを停止したりすることができます。
Composeを使うことの大きな利点は、アプリケーション・スタックをファイルで定義し、それをプロジェクト・リポジトリのルートに置いておき(バージョン管理されている)、他の人が簡単にプロジェクトに貢献できることです。
誰かがあなたのレポジトリをクローンしてcomposeアプリを起動するだけでいいのです。
プロジェクトのルートでdocker-compose.yml
というファイルを作成する。
- まずファイル内でComposeのファイルフォーマットのversionを宣言する。versionの一覧はこちらです。
-
version: "3.7"
.
-
- 次にアプリケーションの一部として動作させたいサービス(=コンテナ)のリストを定義します。
services:
例として先程のtodoアプリとMySQLコンテナの定義を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
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:
appサービスについて
- servicesの名前を
app
としています。サービスの名前ではなんでも好きなものを付けることができます。同時にこれはnetwork aliasになります。 -
-w /app
で指定していた作業ディレクトリはworking_dir
で定義することができます。 -
-v "$(pwd):/app"
でbind mountしていたのは、volumes
で定義することがができます。docker-composeではカレントディレクトリからの相対パスを使用できます。
mysqlサービスについて
- docker runのコマンドでは
-v todo-mysql-data:/var/lib/mysql
のオプションでnamed volumeが自動的に作成されたが、Composeで実行する場合はそうはいきません。トップレベルのvolume:
セクションでボリュームを定義し、サービスコンフィグでマウントポイントを指定する必要があります。
Docker Composeを使用して定義したサービスを起動しよう
-
docker-compose up -d
でバックグランドで起動することができます。-d
を付けずに起動してログをリアルタイムで見ることもできます。起動すると以下のような表示が出てきます。 -
createing networkと表示されていますね! デフォルトでは、Docker Composeはアプリケーションスタック専用のネットワークを自動的に作成します(コンポーズファイルで定義していないのはそのためです)。
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
注意ポイント
アプリの起動時には、MySQLが起動して準備が整うのを実際に待ってから接続しようとします。Dockerには、他のコンテナを起動する前に、他のコンテナが完全に立ち上がり、実行され、準備が整うのを待つためのビルトインのサポートはありません。 Node ベースのプロジェクトでは、wait-port依存関係を使用することができます。同様のプロジェクトが他の言語/フレームワークにも存在します。
全てのコンテナを終了する
すべてを破壊する準備ができたら、docker-compose down
を実行するか、Docker Dashboardでアプリ全体のゴミ箱を押すだけです。コンテナは停止し、ネットワークも削除されます。 volumeは削除されません。
volumeも一緒に削除したい場合は、docker-compose down --volumes
を実行する必要があります。
ベストプラクティスを学ぼう
-
イメージをbuildしたらdocker scanコマンドを使用してセキュリティ上の脆弱性をスキャンします。
- 例えばgetting-startedというタグでイメージをビルドした場合は、このようなコマンドを打ちます。
docker scan getting-started
- 例えばgetting-startedというタグでイメージをビルドした場合は、このようなコマンドを打ちます。
-
docker image history getting-started
を使用すると、getting-startedというイメージ内の各レイヤーの作成に使われたコマンドを見ることができます。
ビルド時間短縮のための工夫
前提として、Dockerイメージは、レイヤーが変更されると、下流のレイヤーもすべて再作成する必要があります。
例えば以下のようなDockerfileがあるとします。
FROM node:12-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]
上記のDockerfileだと、イメージに変更を加えたときにCOPY . .
の部分が影響受け、下流のyarnの依存関係を再インストールするところも再び実行されます。つまりイメージに変更を加えるたびに同じ依存関係を使って依存関係を再インストールしています。これを解決するために以下のような最適な形にDockerfileを変更します。
FROM node:12-alpine
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --production
COPY . .
CMD ["node", "src/index.js"]
またDockerfileと同じフォルダに、.dockerignore
というファイルを以下の内容で作成します。
node_modules
.dockerignoreファイルは、イメージに関連するファイルだけを選択的にコピーする簡単な方法です。今回のケースでは、2回目のCOPYステップ(COPY ..
)でnode_modulesフォルダを省略する必要があります。
そうしないと、RUNステップでコマンドによって作成されたファイルを上書きしてしまう可能性があるからです。
Multi-stage buildsを使用しよう
マルチステージビルドは、複数のステージを使ってイメージを作成するための非常に強力なツールです。マルチスタージビルドにはいくつかのメリットがあります。
- ビルド時の依存性とランタイムの依存性の分離することができる
- アプリの動作に必要なものだけを出荷することで全体のイメージサイズを縮小することができる
例えばReactを使用したアプリケーションの例で考えてみます。
Reactアプリケーションを構築する際には、JSコード(通常はJSX)やSASSスタイルシートなどを静的なHTML、JS、CSSにコンパイルするためにNode環境が必要です。サーバーサイドレンダリングを行わないのであれば、本番環境のビルドにNode環境は必要ありません。静的リソースを静的なnginxコンテナに入れて出荷すればいいのです。
すると以下のようにマルチステージビルドを使用してDockerfileを書くことができます。
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
この例では、1つのステージ(buildと呼ばれる)で、Nodeを使用して実際にReactのビルドを行います。2番目のステージ(FROM nginx:alpineから始まる)では、buildステージからファイルをコピーしています。最終的なイメージは、最後のステージが作成されている(出力をnginxコンテナにコピーしている)だけです。
以上になります。
次回はLaravel&Reactを使用した環境構築について記事を書いてみようと思います。