Docker
Docker Imageについて
Docker Image とはコンテナを作成するための雛形。作成するにはdockerfileを作成してdocker buildコマンドを実行する必要がある。
dockerfileは以下のように書く。
# ベースにしたいイメージとそのタグ
FROM node:14
# RUNとかCOPYコマンド実行する際の実行先ディレクトリを固定する
WORKDIR /app
COPY package.json /app
RUN npm install
# アプリ内からコピーしたいファイル、フォルダのディレクトリ / それらを保存したいコンテナ内のディレクトリ
COPY . /app
# COPY . ./ でもいける。何故ならWORKDIRで実行先を固定しているから。
# コンテナ内のどのポートを外部に公開するかを指定。(アプリを80でlistenしてたら80)
EXPOSE 80
# イメージからコンテナができた時、実行するコマンド。指定しないとベースイメージのコマンドが実行される。
CMD ["node","server.js"]
dockerfile作成後、docker build コマンドを実行してイメージを作成。
docker imageのレイヤーについて
レイヤーとはdockerfileに書かれているWORKDIRとかRUNとかCOPYのコード一行一行を指す。
例えば、一度buildされたimageを再buildする際、コピーするコードが全く変更されていなかった場合、キャッシュされた内容が実行されるためbuildに時間がかからない。
ただし、なんらかの変更があった場合はdockerがそれを検知して、変更があった箇所に関連するレイヤーと、そのレイヤー以降のレイヤーが全て再実行される。
そのため、キャッシュを使ってbuildの速度を上げたいのであれば、再実行されるレイヤーが少なくなるようにdockerfileを記載することが重要。
docker image の中身を見る
dokcer image inspect 〇〇〇〇(dokcer image ID)
上記コマンドでimageの細かな情報を確認できる。詳しい容量、OSの種類、レイヤーなど。
docker image を共有
docker hubアカウントを作成してdocker hubを使って共有できる。
docker hub上でリポジトリを作成し、ローカルのビルド済みイメージをリポジトリに対してpushするだけ。
他のユーザーがそのイメージをローカルにpullしてあとはコンテナを作成するだけ。
Volumesについて
dockerで扱うデータの種類を三つに分類したとき、volumeは永続的に保持したいデータに対して使用されるdockerの機能である。
- アプリのソースコードのデータ(imageに保持されるデータ)
開発者自身が書き、docker imageをbuildしてrunしたコンテナ内に保持される。なお、imageが一度buildされてしまったら再度buildしないと内容は書き換えられない。
外部から書き込まれたデータなどの保持はできない。 - 一時的なデータ(containerに保持されるデータ)
実行中のコンテナに保持される一時的なデータ。メモリや一時ファイルに保持され、頻繁に更新やクリアが発生する。
当然、コンテナが止まれば保持されているデータは削除される。 - 永久的なデータ(container&volumeに保持されるデータ)
ユーザーの登録情報など、データベースに保存される永続的なデータ。コンテナが止まったり、リスタートしても失われるべきではないデータ。
例えば、imageをベースにコンテナを作ったとする。コンテナ内のアプリはユーザーがデータを入力できる機能があり、それを保持、参照ができるものだとする。
アプリの機能にアップデートがあったため、一度コンテナを落として再度、イメージをビルド、新しいイメージから新しいコンテナを生成した時、前回のコンテナで保持していたデータは再現できない。
なぜなら前のコンテナで保持していたデータは削除時にコンテナと共に消えてしまったから。
こうしたケースでユーザの入力情報などを保持するためにvolumeは存在している。
volume内のデータはどこに保持されるのか
volume内のデータはハードドライブの中に保存される。なので、コンテナがなくなったとしてもハードドライブから再度volume内のデータを読み取ることで削除前と同じ情報を得ることができる。
volumeの種類
二種類あり、名前付きvolumeと名無しvolumeがある。
- 名無しvolume
コンテナを削除(rm)したと同時に消えてしまう。
※なお、--rmオプションをつけずにコンテナを作成して、その後削除した場合、名無しvolumeは残ってしまう。同じイメージから別のコンテナを作成するとまた名無しvolumeができてしまい、結果的に不要なvolumeが蓄積していってしまうので注意。
なお、後ほどbind mountsでも紹介するが、名無しvolumeはbind mountsによるコードの上書きを保護する役目もあるため、使える。
他にも、コンテナ作成時にコピーする必要のない変更がないファイルやフォルダなどをあらかじめ名無しボリュームとして指定することで時間短縮にもつながる。 - 名前付きvolume
コンテナに依存せず、ハードドライブに残り続ける。
コンテナ作成時にvolumeを作成するには以下のようにコマンドを実行。
# -v volume名:アプリ内のvolumeに保存したいデータへのパス
docker run -v testVolume:/app/data
- bind mounts
ローカルのソースコードの内容をコンテナ内のコードとしてシェアできる手法。
詳しくは次のコメント参照。
Bind Mountsについて
ローカル環境でソースコードを編集した場合、通常であれば再度イメージをビルドしてコンテナを再作成しない限りは変更箇所はコンテナに反映できないが、bind mountsを使用すればローカルのディレクトリをコンテナ内に共有することができる。
ローカルの指定したディレクトリをアプリに共有するので、コンテナを止めてリスタートしても内容はそのまま生き残る。ローカルでbindしているソースコード自体を削除しない限りは存在し続ける。
設定方法
名前付きボリュームを設定するときの書き方と同じ。ただし、名前付きvolumeを設定する時にvolume名を書いていた部分にシェアしたいファイル/フォルダの絶対パスを書く必要がある。
# -v コードをシェアしたいファイル/フォルダのパス:コンテナ内のパス
docker -v "/Users/aaaa/Desktop/app:/app"
なお、今自分がいるディレクトリの絶対パスを取得したいときは、ディレクトリ内任意のファイルかフォルダを右クリックで「copy path」を選択すれば可能。
また、macの場合「-v $(pwd):/app」でも指定が可能。(pwd=present working directory)
注意
- node_modulesが消える
bind mountsを行う際、dockerfile内で先に依存ライブラリのインストールを行うと、bind mountsによってコンテナ内のソースコードが上書きされてしまい、インストールしたnode_modulesも消えてしまう。
回避するには、以下のようにnode_moduleを別のボリュームに保持しておく。
# 名無しvolumeを追加してnode_modulesを保持する
docker -v /Users/aaaa/Desktop/app:/app "-v /app/node_modules"
bind mounts時に上書きされるよりも、ディレクトリ指定の詳細度が高いほどそちらのコードが優先されてvolumeに保持される仕組みらしい。
なので、ここでは左のアプリ全ての内容を保持しているvolumeよりも/app/node_modulesと指定しているvolumeの方が詳細度が高いので、こちらのvolumeのnode_modulesの内容が優先して保持されることになる。
- read only
bind mountsはコンテナからの読み書きが可能である。そのため、コードの内容によってはローカルのソースコードを変更できてしまう可能性がある。
回避するには
somedirectories/app:ro
と書く。
バインドしたいディレクトリの後に: r o と書くことでコンテナからは読み取りのみに指定することができる。
.dockerignore ファイル
.dockerignore ファイルを作成し、イメージに含めたくないファイルなどをgitignoreのように指定することができる。
例えば、dockerfileやgit関係、lintrcなど。
ARG
dockerfileの中に記載することで、dockerfileの中のみで使用できる変数を定義することができる。
ただし、ソースコードの中でARGで定義した値を参照することはできない。
# 変数を定義
ARG DEFAULT_PORT=80
# 環境変数を定義
ENV PORT $DEFAULT_PORT
ENV
ARG同様変数をセットできる。ただし、ソースコード内でも使用可能な環境変数を定義することができる。
例えば以下のように書けば、ビルド時にデフォルトのポートを80に設定し、そのまま公開することができる。
# 環境変数を定義
ENV PORT 80
# 環境変数を$をつければそのまま使える。
EXPOSE $PORT
// PORT = 80
app.listen(process.env.PORT);
なお、.envファイルを事前に用意して公開するポートとしてコンテナ作成時に使用したい場合は、コンテナ作成時のrunコマンドのオプションで「--env-file env ファイルがあるディレクトリ」とつけることで.envファイルをコンテナ作成時に読み込める。
注意
環境変数をdockerimageに含める場合、DBのパスワードなど認証情報などの重要なデータを含めないように。dockerfileを共有すると全てわかってしまうため。
network について
docker container同士で通信したいとき、networkにdockerコンテナを所属させることでコミュニケーションさせることができる。
※コンテナのポートを外部に公開して通信させることもできるが。
コマンド
ネットワークの作成
docker network create ネットワーク名
通信させるコンテナをネットワークに所属させる
docker run -d --name mongoDB --network ネットワーク名 mongo
※上記コードはmongodbのイメージを使ったコンテナ作成してる。
また、外部からコンテナへのアクセスが可能なポート番号(-p オプション)を指定してないが、やり取りする対象がネットワーク内のコンテナに限られる場合は不要。
DBコンテナの場合、APIとのやり取りのみできれば良いので、不要。
他のコンテナとやり取りしている箇所のコードを修正
node.jsのコードからmongoDBの入ったコンテナにアクセスしている。
URLのドメインのところにコンテナ名を入れるだけで、同じネットワークに所属しているコンテナ同士は通信が可能になる。
mongoose.connect(
'mongodb://mongoDB :27017/swfavorites',
{ useNewUrlParser: true },
(err) => {
if (err) {
console.log(err);
} else {
app.listen(3000);
}
}
);
注意
フロントエンドアプリケーションも同様にバックエンドアプリの入ったコンテナとnetworkを使ってやり取りができそうに思えるが、できない。
フロントエンドのコードは実際にはブラウザで実行されるため、ブラウザがAPIに対するリクエストのエンドポイントを見たときに、イメージ名部分を理解できない。
そのため、APIのポートはブラウザからでもアクセスできるように公開し、フロントエンドのコードは公開されているAPIのエンドポイントを書く必要がある。