- 技術 💻
Dockerfileの書き方|軽く・速く・安全なイメージを作るベストプラクティス
Dockerfileとは、Dockerのイメージ(アプリを動かすための環境一式)の作り方を、命令の順番に書いたファイルです。FROM でベースのイメージを選び、COPY でファイルを入れ、RUN でコマンドを実行し、CMD で起動のコマンドを指定する、というのが基本の流れです。
動くDockerfileを書くこと自体は難しくありませんが、書き方しだいでビルドの時間、イメージの大きさ、安全性が大きく変わります。この記事では、基本の命令を整理したうえで、実際にビルドして測った結果をもとに、効果の大きいベストプラクティスを紹介します。最後に、Node.jsとJavaのテンプレートも載せています。
※ 実測は、検証用に作った最小のベースイメージ(約1.7MB)で行いました。数値は、書き方による差を示すためのものです。
Dockerfileの主な命令
| 命令 | 役割 | 例 |
|---|---|---|
FROM |
ベースのイメージを指定する | FROM node:22-slim |
WORKDIR |
作業するディレクトリを指定する(なければ作る) | WORKDIR /app |
COPY |
ファイルをイメージに入れる | COPY package.json ./ |
RUN |
ビルド時にコマンドを実行する | RUN npm ci |
ENV |
環境変数を設定する(実行時にも残る) | ENV NODE_ENV=production |
ARG |
ビルド時だけの変数 | ARG APP_VERSION |
USER |
以降の命令と実行時のユーザーを指定する | USER node |
EXPOSE |
使うポートを示す(文書としての意味) | EXPOSE 3000 |
CMD |
コンテナの起動時に実行するコマンド(上書きしやすい) | CMD ["node", "server.js"] |
ENTRYPOINT |
コンテナの起動時に必ず実行するコマンド | ENTRYPOINT ["java", "-jar", "app.jar"] |
HEALTHCHECK |
コンテナが正常かを確認するコマンド | HEALTHCHECK CMD curl -f http://localhost:3000/health |
ベストプラクティス早見表
| やること | 効果 | 実測(この記事の検証) |
|---|---|---|
| 変更の少ないものから順に書く | 再ビルドが速くなる | 5.5秒 → 0.4秒 |
.dockerignore を書く |
不要なファイルと秘密の情報を入れない | 264MB → 1.7MB |
一時ファイルは同じ RUN で削除する |
イメージが小さくなる | 102MB → 1.7MB |
| マルチステージビルドにする | ビルド用のツールを最終イメージに入れない | 102MB → 1.7MB |
秘密の情報は --mount=type=secret で渡す |
履歴に残らない | ARGは履歴に平文で残った |
| root以外のユーザーで実行する | 乗っ取られたときの被害を小さくする | 書き込みが制限されることを確認 |
| ベースイメージのタグを固定する | 知らないうちに中身が変わらない | ― |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Dockerfileの書き方|軽く・速く・安全なイメージを作るベストプラクティス」
1. 変更の少ないものから順に書く(キャッシュを活かす)
Dockerは、命令ごとに結果(レイヤー)を保存し、前回と同じなら使い回します(キャッシュ)。ただし、ある命令が変わると、それ以降の命令はすべてやり直しになります。
次の2つのDockerfileで、アプリのコード(app.js)だけを変更して再ビルドしました。依存関係のインストールの代わりに、5秒かかる処理を入れています。
# 改善前:すべてをコピーしてからインストール
FROM base
WORKDIR /app
COPY . .
RUN sleep 5 && cat package.json > installed.txt # 依存関係のインストールの代わり
# 改善後:依存関係のファイルだけ先にコピー
FROM base
WORKDIR /app
COPY package.json ./
RUN sleep 5 && cat package.json > installed.txt
COPY . .
| 再ビルドの時間 | インストールの処理 | |
|---|---|---|
| 改善前 | 5.5秒 | やり直し(COPY . . が変わったため) |
| 改善後 | 0.4秒 | CACHED(使い回し) |
依存関係の定義ファイル(package.json、pom.xml、requirements.txtなど)だけを先にコピーしてインストールし、アプリのコードは最後にコピーすると、コードを変えるたびにインストールをやり直さずに済みます。実際のプロジェクトでは、インストールに数分かかることも多く、差はさらに大きくなります。
2. .dockerignore で不要なファイルを入れない
docker build . の . は、ビルドに使うファイルの置き場所(ビルドコンテキスト)です。ここにあるファイルは、まずDockerに送られ、COPY . . でイメージに入ります。
node_modules(200MB)、.git(50MB)、.env(秘密の情報)があるフォルダでビルドした結果です。
| ビルドコンテキスト | イメージの大きさ | .envがイメージに入ったか | |
|---|---|---|---|
| .dockerignoreなし | 262MB | 264MB | 入った |
| .dockerignoreあり | 193B | 1.7MB | 入らない |
# .dockerignore
node_modules
.git
.env
*.log
.dockerignoreがないと、ローカルの .env のような秘密の情報までイメージに入ってしまいます。イメージをレジストリに公開すると、誰でも中身を取り出せます。Dockerfileを書いたら、.dockerignoreもセットで用意します。
3. 一時ファイルは同じ RUN で削除する
イメージは、命令ごとのレイヤーを重ねたものです。後の命令でファイルを削除しても、前のレイヤーにはファイルが残ったままになります。
# 別々のRUN
RUN head -c 100000000 /dev/urandom > /tmp/big
RUN rm /tmp/big
# 同じRUN
RUN head -c 100000000 /dev/urandom > /tmp/big && rm /tmp/big
| イメージの大きさ | |
|---|---|
| 別々のRUNで削除 | 102MB(100MBが残った) |
| 同じRUNで削除 | 1.7MB |
docker history で確認すると、削除した命令のレイヤーは小さいままで、作成した命令のレイヤーに100MBが残っています。
RUN /bin/sh -c rm /tmp/big 8.19kB
RUN /bin/sh -c head -c 100000000 /dev/urando… 100MB
パッケージのインストールでも同じです。
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
--no-install-recommends で推奨パッケージを入れず、同じ RUN の中でパッケージの一覧のキャッシュを削除します。
4. マルチステージビルドにする
コンパイルやビルドには、SDKやビルドツール、依存関係のキャッシュなどが必要ですが、実行するときには不要です。マルチステージビルドでは、FROM を複数書き、ビルド用のステージで作った成果物だけを、実行用のステージにコピーします。
# ビルド用
FROM base AS build
WORKDIR /src
RUN head -c 100000000 /dev/urandom > /src/sdk-and-cache.bin \
&& echo "app binary" > /src/app
# 実行用
FROM base
COPY --from=build /src/app /app/app
| イメージの大きさ | |
|---|---|
| 1つのステージ(ビルド用のファイルが残る) | 102MB |
| マルチステージ | 1.7MB |
イメージが小さくなると、ダウンロードや起動が速くなり、脆弱性の対象になるパッケージも減ります。
5. 秘密の情報は ARG ではなく secret で渡す
非公開のパッケージをインストールするためのトークンなど、ビルド時に秘密の情報が必要になることがあります。ARGで渡すと、イメージの履歴に平文で残ります。
FROM base
ARG NPM_TOKEN
RUN echo "using token" > /tmp/log
docker build --build-arg NPM_TOKEN=npm_SuperSecret123 -t app .
docker history --no-trunc app
RUN |1 NPM_TOKEN=npm_SuperSecret123 /bin/sh -c echo "using token" > /tmp/log
ARG NPM_TOKEN=npm_SuperSecret123
トークンがそのまま表示されました。イメージを受け取った人は、誰でもこの情報を見られます。
BuildKitの secretマウントを使うと、秘密の情報はその RUN の間だけファイルや環境変数として使え、イメージには残りません。
FROM base
RUN --mount=type=secret,id=npm_token,env=NPM_TOKEN \
npm ci
docker build --secret id=npm_token,src=$HOME/.npm_token -t app .
同じように docker history で確認すると、トークンは表示されず、完成したイメージの中にも残っていないことを確認できました。
6. root以外のユーザーで実行する
USER を指定しないと、コンテナの中のプロセスはroot(管理者)で動きます。アプリに脆弱性があって乗っ取られた場合、rootで動いていると被害が大きくなります。
FROM base
WORKDIR /app
COPY --chown=app:app app.js .
USER app
CMD ["/bin/id"]
uid=1001(app) gid=1001(app) groups=1001(app)
実行ユーザーが app になり、/etc などシステムの場所には書き込めなくなりました。
注意点として、WORKDIR で作られたディレクトリはrootの持ち物になるため、アプリがそのディレクトリにファイルを書き込む必要がある場合は、chown で持ち主を変えておきます。公式のNode.jsのイメージには node というユーザーが、多くのイメージには同様の一般ユーザーが用意されています。
7. ベースイメージを選び、タグを固定する
| 書き方 | 問題点・特徴 |
|---|---|
FROM node / FROM node:latest |
ビルドのたびに中身が変わる可能性がある。避ける |
FROM node:22 |
メジャーバージョンを固定。フルのイメージで大きい |
FROM node:22-slim |
不要なパッケージを減らした小さなイメージ。多くの場合の第一候補 |
FROM node:22-alpine |
さらに小さいが、C言語のライブラリがmuslのため、一部のパッケージで問題が起きることがある |
FROM node:22-slim@sha256:... |
ダイジェストで完全に固定。再現性が最も高い |
バージョンを固定すると同時に、定期的に更新する仕組み(Dependabotなど)も用意しておくと、セキュリティの修正を取り込めます。
CMDとENTRYPOINTの違い
| CMD | ENTRYPOINT | |
|---|---|---|
| 役割 | 起動時の既定のコマンド | 起動時に必ず実行するコマンド |
docker run イメージ 別のコマンド |
置き換わる | 置き換わらない(引数として追加される) |
| 向いている場面 | 一般的なアプリ | コンテナを1つのコマンドのように使う |
ENTRYPOINT ["java", "-jar", "app.jar"]
CMD ["--spring.profiles.active=prod"] # ENTRYPOINTの既定の引数
exec形式で書く
CMD ["node", "server.js"] # exec形式(推奨)
CMD node server.js # シェル形式
シェル形式では、アプリが /bin/sh -c の子プロセスとして起動されるため、docker stop の停止の合図(SIGTERM)がアプリに届かず、正常に終了できずに強制終了されることがあります。CMDとENTRYPOINTは、[ ] を使うexec形式で書きます。
COPYとADDの違い
| COPY | ADD | |
|---|---|---|
| ローカルのファイルをコピー | できる | できる |
| URLからダウンロード | できない | できる |
| tarファイルの自動展開 | しない | する |
ADD は、意図しない展開やダウンロードが起きることがあるため、通常は COPY を使います。tarの展開が必要な場合だけ ADD を使います。
テンプレート:Node.js
# 依存関係のインストール
FROM node:22-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
# 実行用
FROM node:22-slim
ENV NODE_ENV=production
WORKDIR /app
COPY --from=deps --chown=node:node /app/node_modules ./node_modules
COPY --chown=node:node . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]
TypeScriptなどでビルドが必要な場合は、ビルド用のステージを追加し、npm ci(開発用も含む)→ npm run build を行って、出力されたファイルだけを実行用のステージにコピーします。
テンプレート:Java(Spring Boot)
# ビルド用
FROM eclipse-temurin:21-jdk AS build
WORKDIR /src
COPY mvnw pom.xml ./
COPY .mvn .mvn
RUN ./mvnw -B dependency:go-offline
COPY src src
RUN ./mvnw -B package -DskipTests
# 実行用
FROM eclipse-temurin:21-jre
RUN useradd --system --uid 1001 app
WORKDIR /app
COPY --from=build --chown=app:app /src/target/*.jar app.jar
USER app
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
pom.xmlを先にコピーして依存関係をダウンロードし、ソースコードは後からコピーする(キャッシュを活かす)- 実行用は JDKではなくJREのイメージにする(コンパイラーが不要なため小さくなる)
- テストはCIで実行し、ここでは
-DskipTestsでビルドの時間を短くしている
よくある質問
Q. イメージの大きさはどう確認しますか?
A. docker images で一覧を、docker history イメージ名 でレイヤーごとの大きさを確認できます。どの命令で大きくなったかを調べるときに便利です。
Q. docker-compose.ymlとの違いは何ですか? A. Dockerfileは1つのイメージの作り方、docker-compose.ymlは複数のコンテナ(アプリ・DB・キャッシュなど)の組み合わせと起動の方法を書くファイルです。
Q. Dockerfileの書き方をチェックするツールはありますか?
A. docker build --check で、Docker自体のチェック機能を使えます。また、hadolintというリンターもよく使われています。CIに組み込むと、レビューの前に基本的な問題を見つけられます。
まとめ
- 変更の少ないものから順に書き、依存関係のインストールをキャッシュさせる
- .dockerignore で、node_modules・.git・.envなどを入れない
- 一時ファイルは同じRUNで削除する。後の命令で削除しても小さくならない
- マルチステージビルドで、ビルド用のツールを最終イメージに入れない
- 秘密の情報はARGではなくsecretマウントで渡す
- root以外のユーザーで実行し、ベースイメージのタグを固定する
- CMDとENTRYPOINTはexec形式で書く
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Dockerfileの書き方|軽く・速く・安全なイメージを作るベストプラクティス」
株式会社RJCでは、開発環境や実行環境を整える技術も、実際に手を動かしながら仲間と学んでいます。こうした知識を共有しながら、一緒に成長を楽しんでいます。
株式会社RJC ― SI事業・SES事業・AI駆動開発。未経験からエンジニアに育てるのが得意な会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。