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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?