Dockerfileで「COPY package*.jsonを実行し、その後に改めてCOPY . .と書くのは無駄ではないか?」「どうせ全部コピーするなら一度でいいのでは?」という疑問を持ったことはありませんか?
ですが、この分割した記述はDockerの処理を高速化するために、Dockerの機能を利用した上手な書き方です。
この記事を読めば、Dockerfileのキャッシュの仕組みと、それを利用した理想的なCOPYの書き方がわかり、あなたのDocker知識が一段と深まります。
Dockerfileでよくみられる記述
Dockerfileの中で以下のようにCOPYを複数回記述してある書き方があります。
例えば以下の#3と#5はどちらもCOPYの命令です。
# 1. ベースイメージの指定
FROM node:20-alpine
# 2. 作業ディレクトリの設定
WORKDIR /usr/src/app
# 3. 依存関係ファイルのコピー
COPY package*.json ./
# 4. 依存関係のインストール
RUN npm install
# 5. アプリケーションコードのコピー
# Dockerfileと同じディレクトリにある全てのファイルを、WORKDIRにコピー
COPY . .
# 6. アプリケーションが使用するポートの公開
EXPOSE 3000
# 7. コンテナ起動時に実行するコマンド
CMD [ "npm", "start" ]COPY . .|現在のディレクトリの全てのファイルをコピー
特に、2つ目のCOPY . .は、ルートディレクトリ(Dockerfileと同じディレクトリ)にある全てのファイルを、WORKDIRで指定したコンテナ内のディレクトリ(上記では /usr/src/app)にコピーしています。
このとき、#3で指定した「package.json」と「package-lock.json」も再度コピーされます。
これを見ると、同じファイルを何回もコピーするなんて冗長じゃないか?と思うかもしれません。
COPY . . のみにしてはいけない理由
そこで、以下のようにCOPY . .を先に実行して、COPY package*.json ./ を削除した場合は以下のようになります。
# 1. ベースイメージの指定
FROM node:20-alpine
# 2. 作業ディレクトリの設定
WORKDIR /usr/src/app
# 3. ファイルをコピー
COPY COPY . .
# 4. 依存関係のインストール
RUN npm install
# 5. アプリケーションが使用するポートの公開
EXPOSE 3000
# 6. コンテナ起動時に実行するコマンド
CMD [ "npm", "start" ]
コードはシンプルになりました。
しかも、初回に実行するだけでは元のDocerfileと全く同じ処理になります。
重い処理は何か?
ここで理解しておかなければいけないことは、Dockerfileの命令の中で比較的重い処理は#4の「RUN npm install」(npm install)だということです。
軽量化するためには、RUN npm installを実行しなくていいのであれば、避けるべきです。
npm installを毎回実行してしまう
ところが、以下の順番で命令を出すと、Dockerfileと同じ階層にあるいずれかのファイルに変更があった場合に、それがpackage.jsonでなかったとしても毎回 npm installを実行してしまいます。
# 3. ファイルをコピー
COPY COPY . .
# 4. 依存関係のインストール
RUN npm installつまり、イメージのビルドが重くなってしまうということです。
Dockerのレイヤーキャッシュによる効率化
レイヤーキャッシュとは何か?
Dockerには「レイヤーキャッシュ」という機能があります。
この、レイヤーキャッシュとは、Dockerイメージをビルドする際の時間と効率を大幅に改善する仕組みで、
「一度実行して成功したDockerfileの命令の結果を覚えておき、次回ビルド時に同じ命令を再実行しないようにする」機能のことです。
レイヤーキャッシュの仕組み
Dockerイメージは、Dockerfileに書かれた各命令(FROM, RUN, COPYなど)ごとに作成される、積み重ねられた読み取り専用の層(レイヤー)で構成されています。
Dockerfileの各行が実行されるたびに、その結果(ファイルシステムの変更)が新しいレイヤーとして保存されます。
Dockerは、各命令の内容(命令そのものと、COPYの場合はコピー元のファイルの内容)を基に、一意のハッシュ値(ID)を計算し、そのレイヤーを識別します。
再度ビルドコマンド(docker build)を実行した際、DockerはDockerfileの最初の命令から順に処理を進めます。
もし、現在の命令と、それより前のすべての命令が完全に一致するキャッシュされたレイヤーを見つけたら、そのレイヤー以降の処理をスキップし、キャッシュされた結果を再利用します。
更新頻度の低いpackage.jsonを分離する
このレイヤーキャッシュ機能を最大限に使う場合、更新頻度が低いpackage.jsonは分離して先にコピーを行い、npm installもすぐその後に分離した命令としてしまうことが有効となります。
他のファイルを変更し、イメージを再度ビルドしたとしても、以下の#3, #4は変更がないためスキップされ、#5からの実行となります。
# 3. 依存関係ファイルのコピー
COPY package*.json ./
# 4. 依存関係のインストール
RUN npm install
# 5. アプリケーションコードのコピー
COPY . .
# 6. アプリケーションが使用するポートの公開
EXPOSE 3000
# 7. コンテナ起動時に実行するコマンド
CMD [ "npm", "start" ]
この場合、#5の COPY . .で再度「package.json」と「package-lock.json」がコピーされます。
ですが、どちらも超軽量ファイルなのでコピーは一瞬です。
npm installを毎回実行するコストに比べたら無視できるレベルです。
npm installはいつ実行されるか?
上記のようにレイヤーを分割した場合、#3のCOPY package*.json ./ は、package.json または package-lock.json の中身に変更があった場合のみ#3を実行します。
イメージ内にコピーするファイルを指定するときに「*(アスタリスク)」を使っています。これは、「package~.json」に一致するファイルを全てコピーするという意味です。
このため「package.json」だけでなく「package-lock.json」もコピーします。package-lock.jsonもコピーすることはDockerビルド時の速度と信頼性を高めるためにほぼ必須です。
以上が、DockerfileでCOPYを複数回に分けて記述する理由です。

