先に要点
- Dockerfile は、Docker で使う コンテナイメージの作り方を書くファイルです。
- 最初に押さえたい命令は
FROMRUNCOPYWORKDIRCMDの5つです。 - 命令の順番でビルドキャッシュの効きが大きく変わり、悪い順序だと毎回
npm ciが走って数十秒、改善版だと1秒未満で終わります。 RUNとCMDの混同はexecutable file not found in $PATHという典型エラーで表面化します。本記事では現象と回避まで示します。
Docker は少し分かってきたけど、Dockerfile になると急に読めない、という人は多いです。
特に、英単語が並んでいるだけに見えて、どこから見ればいいのか分からなくなりやすいです。
でも Dockerfile は、全部を一気に理解しなくても大丈夫です。 最初は「このコンテナをどうやって作るかを書いた手順書」と考えるとかなり入りやすいです。
この記事では、Docker Docs の Dockerfile overview と Dockerfile reference、および build cache のドキュメントを確認しながら、Dockerfile とは何か、何を書くものか、最初にどの命令から読めばよいかを初心者向けに整理します。そのうえで、監査でも指摘されやすい「命令の順番でビルド時間が変わる話」と「RUN と CMD を取り違えたときに出る実際のエラー」まで踏み込みます。 Docker や Docker Compose の全体像から先に見たいなら、Dockerとは?コンテナで何がうれしい?初心者向けに仕組み・メリット・使いどころを解説 と Docker Composeとは?複数コンテナをまとめて動かす基本を初心者向けに解説 もつながりやすいです。
Dockerfileとは何か
Dockerfile は、コンテナイメージをどう作るかを書くファイルです。 かなりざっくり言うと、「この環境をどう組み立てて、最後に何を動かすか」を順番に書くメモです。
たとえば、アプリを動かすには
- ベースになるOSやランタイム
- 必要なパッケージ
- ソースコード
- 起動コマンド
が必要です。 Dockerfile は、それを一段ずつ積み上げていく形で書きます。各命令はそれぞれ1つの「レイヤー」になり、上から順に積み重なってイメージができあがります。このレイヤーの考え方が、後で出てくるキャッシュの話に直結します。
そのため、Dockerfile が読めるようになると、「このコンテナは何を前提にしているのか」がかなり見えやすくなります。 開発環境、CI/CD、本番イメージの理解にもつながるので、実務でも地味に効きます。
何を書くものなのか
Dockerfile には、主に次のことを書きます。
土台
どのイメージを土台にするか。FROM で決めます。
追加するもの
パッケージや依存をどう入れるか。RUN で実行します。
置く場所
ソースをどこへ運び、どのディレクトリで作業するか。COPY と WORKDIR です。
起動時のふるまい
コンテナ起動時に何を動かすか。CMD で決めます。
要するに、「コンテナの中身」と「起動時のふるまい」を決めるファイルです。
Docker Docs でも、Dockerfile はイメージを組み立てるための命令(instructions)を書くテキストファイルとして説明されています。 初心者向けには「コンテナのレシピ」という説明もかなり近いです。
最初に読むべき命令はこの5つ
Dockerfile には他にも命令がありますが、最初は次の5つで十分です。
1. FROM
FROM は、どのイメージを土台にするかを決める命令です。
FROM node:22-alpine
この場合は、Node.js 22 が入った軽量イメージを土台にしています。 最初にここを見ると、「このコンテナは何の環境から始まるのか」が分かります。
実務でも、古いバージョンを使っていないか、軽量イメージか、公式イメージか、といった確認でまず見られやすいです。
2. RUN
RUN は、イメージ作成中にコマンドを実行する命令です。
RUN apk add --no-cache curl
これは、コンテナが起動した後に毎回走るのではなく、イメージを作るとき(ビルド時)に一度だけ実行されます。 ここを誤解しやすいので、「作成時の処理」と覚えておくと分かりやすいです。
3. COPY
COPY は、ローカルのファイルをコンテナ側へコピーする命令です。
COPY . /app
アプリのソースコードや設定ファイルを、コンテナの中へ持っていくときによく使います。 初心者は「ローカルとコンテナは別の場所」だと意識しておくと、COPY の意味がつかみやすいです。
4. WORKDIR
WORKDIR は、その後の作業ディレクトリを決める命令です。
WORKDIR /app
これがあると、後続の RUN や CMD が /app を基準に動きます。
Docker Docs でも、RUN CMD COPY などの後続命令に影響する重要な命令として説明されています。
5. CMD
CMD は、コンテナ起動時にデフォルトで実行するコマンドを指定する命令です。
CMD ["node", "server.js"]
ここを見ると、「このコンテナは最終的に何を動かしたいのか」が分かります。 RUN と混ざりやすいですが、ざっくり言えば
- RUN: 作るときに実行(ビルド時)
- CMD: 起動するときに実行(実行時)
です。この違いを取り違えると、後述する executable file not found in $PATH のようなエラーにつながります。
まずはこう読むと分かりやすい
Dockerfile を上から順に読むだけでもいいのですが、初心者は次の順で見るとかなり入りやすいです。
この順だと、「何の環境で、どこに、何を置いて、どう組み立てて、何を起動するか」が自然に追えます。
最小の例で見るとこうなる
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["npm", "start"]
この例を日本語で読むと、だいたいこうです。
- Node.js 22 の軽量イメージを土台にする
- 作業ディレクトリを
/appにする - 依存定義ファイルを先にコピーする
npm ciで依存を入れる- 残りのソースコードをコピーする
- 起動時に
npm startを実行する
このレベルで読めれば、初心者としてはかなり十分です。 ただし、なぜ「依存定義ファイルを先にコピーする」のか、その理由が次の章のキャッシュの話です。ここが分かると、Dockerfile の良し悪しが一段見えるようになります。
キャッシュが効かない悪い順序と、改善版を並べてみる
監査でも指摘されやすいのが、「命令の順番でビルド時間がまるで変わる」という点です。理屈は単純で、Docker は各命令(レイヤー)の結果をキャッシュし、あるレイヤーが変わると、それ以降のレイヤーは全部作り直すという仕組みだからです。特に COPY は対象ファイルの内容ハッシュで判定するため、運ぶファイルが1バイトでも変われば、その下の命令はすべてキャッシュ無効になります。
悪い例: ソース全体を先にコピーしてしまう
FROM node:22-alpine
WORKDIR /app
# ここで先にソース全体をコピーしてしまっている
COPY . .
RUN npm ci
CMD ["npm", "start"]
これだと、app.js を1行直しただけでも COPY . . のレイヤーが無効になり、その下の RUN npm ci も毎回やり直しになります。実際に2回目のビルドを走らせると、こうなります。
$ docker build -t myapp .
=> [2/4] WORKDIR /app 0.0s
=> [3/4] COPY . . 0.2s
=> [4/4] RUN npm ci 58.7s
=> exporting to image 1.4s
RUN npm ci に CACHED が付かず、毎回フルでネットワーク取得とインストールが走っているのが分かります。依存が多い案件だとここが30〜90秒かかり、コードを直すたびに待たされます。
改善版: 依存定義だけ先にコピーする
FROM node:22-alpine
WORKDIR /app
# 依存定義だけ先に運んでインストールする
COPY package.json package-lock.json ./
RUN npm ci
# ソースは後でコピーする
COPY . .
CMD ["npm", "start"]
この順なら、package.json と package-lock.json が変わらない限り RUN npm ci のレイヤーは再利用されます。ソースだけ直して2回目をビルドすると、出力はこう変わります。
$ docker build -t myapp .
=> CACHED [2/5] WORKDIR /app 0.0s
=> CACHED [3/5] COPY package.json package-loc... 0.0s
=> CACHED [4/5] RUN npm ci 0.0s
=> [5/5] COPY . . 0.2s
=> exporting to image 0.6s
RUN npm ci に CACHED が付き、所要時間が 0.0s になっています。先ほど60秒近くかかっていた依存インストールが丸ごとスキップされ、ソースのコピーとイメージ出力だけで終わります。
| 項目 | 悪い順序(COPY . . を先に) | 改善版(依存定義を先に) |
|---|---|---|
| ソース1行変更後の2回目ビルド | npm ci が毎回再実行 | npm ci は CACHED でスキップ |
| 依存インストールの所要時間(目安) | 30〜90秒(毎回) | 0.0s(キャッシュ再利用) |
| キャッシュが壊れる条件 | ソースを1文字でも変えるたび | package.json / lock を変えたときだけ |
| 体感 | 毎回待たされる | ほぼ待たない |
数値はマシンや回線、依存の量で前後しますが、「ソースだけ変えたのに依存インストールが毎回走る」なら、ほぼこの順序ミスです。docker build の出力に CACHED が並んでいるかどうかで、すぐ見分けられます。
なお、より効かせたいときは、ビルドコンテキストに余計なファイルを送らないよう .dockerignore に node_modules .git .env などを書いておくのも効果的です。これらは頻繁に変わるため、入れたままだと COPY . . のハッシュが毎回変わってキャッシュを壊しやすくなります。
RUNとCMDの混同で起きる実際のエラー
「RUN は作るとき、CMD は起動するとき」という違いを取り違えると、ビルドは通るのに起動でこける、あるいはその逆が起きます。よくあるパターンを現象から見ていきます。
パターン1: CMD に書くべき起動コマンドを RUN で実行してしまう
たとえばサーバー起動を RUN に書いてしまうケースです。
RUN ["node", "server.js"]
RUN はビルド時に実行され、しかもそのコマンドが終了するまでビルドが先に進みません。サーバーは終了しないので、ビルドがそこで止まったまま固まります。起動コマンドは RUN ではなく CMD(または ENTRYPOINT)に書くのが正解です。
パターン2: 存在しない実行ファイルを CMD の exec 形式で指定する
CMD を JSON 配列(exec 形式)で書いた場合、Docker はシェルを介さず、指定したファイルを直接実行しようとします。そのため、パスが通っていなかったり実行権限がなかったりすると、コンテナ起動時に次のようなエラーが出ます。
$ docker run myapp
docker: Error response from daemon: failed to create task for container:
failed to create shim task: OCI runtime create failed: runc create failed:
unable to start container process: exec: "start.sh": executable file not found in $PATH: unknown.
- 現象:
docker buildは成功するのに、docker runした瞬間にexecutable file not found in $PATHで落ちる。 - 原因: exec 形式の
CMD ["start.sh"]はシェルを通さないため、PATH上にstart.shが見つからない。あるいは実行権限が無い。 - 確認手順: コンテナに入って中身を見る。
docker run --rm -it myapp shで入り、ls -l /app/start.shとecho $PATHを確認する。ファイルがあるのにPATHに含まれていなければこのパターン。 - 回避: 絶対パスかカレント相対で指定する(
CMD ["/app/start.sh"]またはCMD ["./start.sh"])。実行権限が無ければRUN chmod +x /app/start.shを加える。シェルの機能(変数展開やパイプ)が要るなら、シェル形式CMD start.shにする手もある。
パターン3: alpine なのに bash を前提にしてしまう
node:22-alpine など Alpine ベースのイメージには bash が入っていません。シェルスクリプトの1行目を #!/bin/bash にしていたり、CMD ["bash", "start.sh"] と書いていると、こうなります。
$ docker run myapp
exec: "bash": executable file not found in $PATH: unknown
シェル形式で書いていた場合は、内部のスクリプト実行時に次の形で出ることもあります。
/bin/sh: 1: bash: not found
- 現象:
bash: not foundやbash: executable file not foundが起動時に出る。 - 原因: Alpine の標準シェルは
/bin/sh(busybox ash)で、bashは別途入れないと無い。 - 確認手順:
docker run --rm myapp sh -c "which bash || echo no-bash"でno-bashが出れば確定。 - 回避: スクリプトを
#!/bin/shに直して sh で済ませるか、どうしても bash が要るならRUN apk add --no-cache bashを入れる。
RUN の合図
「ビルド時に一度だけやってほしい準備」。依存インストール、ビルド、権限付与など。
CMD の合図
「コンテナが起動したら動かし続けてほしい本体」。サーバープロセスやワーカーなど。
迷ったら「これはイメージに焼き込みたい結果か、それとも起動のたびに動かしたいプロセスか」で分けると、ほとんど判別できます。
実務で何が大事になるのか
ベースイメージ選び
FROM で何を使うかはかなり大事です。
重すぎるイメージを使うとビルドも配布も遅くなりやすいですし、古いイメージを放置するとセキュリティ面も気になります。Alpine は軽い反面、glibc 非互換でネイティブ依存がこける場合があるため、安定重視なら slim、互換性重視なら通常版(Debian ベース)という選び方も覚えておくと安全です。
COPY の順番
前章の通り、依存ファイルだけ先に COPY してインストールし、その後にソース全体を COPY する書き方は、キャッシュ効率に直結します。
これは、ソースコードを少し変えただけで毎回全部の依存インストールが走るのを避けるためです。
CMD だけでは足りないこともある
アプリによっては ENTRYPOINT や HEALTHCHECK も出てきます。
ただ、最初の段階ではそこまで広げなくても大丈夫です。
Laravel や Node.js の案件では、ローカル開発用 Dockerfile と本番イメージ用 Dockerfile を分けることもあります。開発用はツール込みで少し厚め、本番用は必要最小限にして軽く保つ、という考え方はかなり現実的です。本番用ではマルチステージビルドでビルド成果物だけを最終イメージへ運び、ビルドツールを残さないのが定石です。
初心者がよく混乱する点
Dockerfile はコンテナそのものではない
Dockerfile はあくまで「作り方」です。 実際に動くのは、その Dockerfile から作られたイメージとコンテナです。
RUN と CMD が混ざる
ここは本当に多く、しかも前章のとおり実際のエラーにつながります。 最初は「作るときの RUN、起動時の CMD」で分けて覚えるだけで十分です。
COPY すればローカルと完全に同期されると思ってしまう
COPY は、ビルド時点のファイルを持っていく命令です。 ローカル編集をリアルタイムで反映する話とは別なので、開発中のマウントや Docker Compose の設定とは分けて考えた方が分かりやすいです。
Dockerfileに関するよくある質問
Q. ビルドの2回目が毎回遅いのですが、原因の見分け方は?
A. docker build の出力で RUN npm ci や RUN npm install に CACHED が付いているかを見ます。付いていないなら、COPY . . を依存インストールより先に書いている可能性が高いです。依存定義ファイルだけ先に COPY する順に直すと、依存が変わらない限りそのレイヤーは 0.0s で再利用されます。
Q. CMD で executable file not found in $PATH が出ます。何を疑えばいいですか?
A. exec 形式の CMD ["x"] はシェルを介さず直接 x を探すため、パスが通っていない・実行権限が無い・そもそもファイルが無い、のいずれかです。docker run --rm -it イメージ名 sh で入って ls -l と echo $PATH を確認し、絶対パス指定や RUN chmod +x で直します。
Q. RUN と CMD はどう使い分けますか?
A. RUN は「ビルド時に一度だけ実行してイメージに焼き込む処理」、CMD は「コンテナ起動時に動かすデフォルトのプロセス」です。サーバー起動を RUN に書くとビルドが固まり、依存インストールを CMD に書くと起動のたびに走ってしまいます。
Q. COPY と ADD はどう使い分けますか?
A. ファイルをコピーするだけなら COPY を使うのが安全です。ADD は tar 展開や URL ダウンロードもできてしまうため、意図しない挙動の原因になります。基本は COPY、特殊用途のみ ADD です。
Q. RUN を何個も書くと遅くなるのですか?
A. RUN ごとにレイヤーが増えるため、イメージサイズが膨らみます。よく一緒に動かすコマンドは && で連結して1つの RUN にまとめると、イメージが軽くなりキャッシュも効きやすくなります。
Q. .dockerignore は何を書けば良いですか?
A. node_modules .git .env tmp/ *.log など、コンテナに含めたくないファイルを書きます。これだけでビルドが速くなり、機密情報の混入も防げます。頻繁に変わるファイルを除外すると COPY . . のキャッシュも壊れにくくなります。
Q. Dockerfile から作ったイメージをチームで共有するには?
A. Docker Hub、GitHub Container Registry、AWS ECR、GCP Artifact Registry などのコンテナレジストリに push して、docker pull で配布します。CI/CD パイプラインで自動 push する構成が一般的です。
まとめ
Dockerfile は、コンテナイメージの作り方を書くファイルです。
最初は FROM RUN COPY WORKDIR CMD の5つが読めればかなり十分で、全部を覚えようとしなくて大丈夫です。
そのうえで、実務で効いてくるのは「命令の順番でキャッシュの効きが変わる」ことと、「RUN と CMD の取り違えが起動時エラーになる」ことの2点です。依存定義を先に COPY して CACHED を出す、起動コマンドは CMD に書く、という当たり前を外さないだけで、ビルドの待ち時間も起動トラブルもかなり減ります。
続けて読むなら、Dockerとは?コンテナで何がうれしい?初心者向けに仕組み・メリット・使いどころを解説 と Docker Composeとは?複数コンテナをまとめて動かす基本を初心者向けに解説 がかなりつながりやすいです。
参考リンク
- Docker Docs: Dockerfile overview
- Docker Docs: Dockerfile reference
- Docker Docs: Using the build cache
- Docker Docs: CMD and ENTRYPOINT interaction