はじめに
Docker を触り始めると、まず「docker run は動くけど、自分のアプリをどうやって“イメージ化”すればいいの?」にぶつかります。そこで登場するのが Dockerfile です。
Dockerfile は、環境構築の手順書を“機械が再現できる形”に落とし込むためのファイルで、これが書けるようになると「誰のPCでも同じ手順で動く」「CIで同じビルドができる」「配布できる」という強さが一気に手に入ります。
一方で、入門者がつまずきやすいのも Dockerfile です。
- なんとなく
RUN apt-get install ...を重ねていたらビルドが遅い - キャッシュが効いたり効かなかったりして意味が分からない
- どこに
COPYを置くと良いのか、WORKDIRの意味が曖昧 CMDとENTRYPOINTの違いがふわっとしている
この記事では、Dockerfile の“基本文法”だけで終わらせず、なぜそう書くのか(レイヤ・キャッシュ・再現性)まで含めて理解できるようにまとめます。最後に、実際に手元で動かして確かめるハンズオンも用意します。
座学
1) Dockerfile は「レイヤを積む設計図」
Dockerfile の各命令(例:FROM RUN COPY)は、多くの場合 イメージのレイヤを1枚ずつ増やします。
このレイヤ構造があるからこそ、Docker は前回と同じ手順の部分を再利用(キャッシュ)できます。
ここが超重要で、Dockerfile を“速く・壊れにくく”書けるかは、ほぼ キャッシュの効かせ方で決まります。
- 変更が頻繁なもの(アプリコード)を早い段階で
COPYするとキャッシュが崩れて遅くなりがち - 逆に、依存インストールなど“変わりにくいもの”を先に固めると速くなる
2) 最低限覚える命令と役割
入門でまず押さえたいのはこのあたりです。
FROM: ベースとなるイメージを決める(OS/ランタイム)WORKDIR: 作業ディレクトリ(以降のRUNCOPYの基準)COPY/ADD: ファイルをイメージ内へコピー(基本はCOPY)RUN: ビルド時にコマンド実行(依存インストールなど)ENV: 環境変数の設定EXPOSE: “このコンテナはこのポートを使う想定”のメタ情報CMD: コンテナ起動時のデフォルトコマンドENTRYPOINT: 起動コマンドの固定化(CMDと組み合わせることが多い)
ポイント:
RUNはビルド時(イメージ作成時)CMD/ENTRYPOINTは実行時(コンテナ起動時)
3) キャッシュが効く/効かないのルール(感覚でOK)
Docker は「その命令の結果が前回と同じなら再利用」します。
しかし “前回と同じ” を判断する材料に、例えば次が含まれます。
COPYで取り込むファイルの内容が変わった- その前のレイヤが変わった(上に積む前提が変わる)
RUNのコマンド文字列が変わった
だから、たとえば Node.js でよくある最適化がこれです。
package.json/package-lock.jsonだけ先にCOPYnpm ciを先に実行- 最後にアプリコード全体を
COPY
こうすると、アプリコードが変わっても依存インストールのキャッシュが残りやすくなります。
4) “よくある事故”を避ける入門ベストプラクティス
入門段階でも効く、最低限のコツです。
WORKDIRを最初に決める(相対パスの混乱を防ぐ)COPY . .を早く置かない(キャッシュが崩れやすい)- 依存インストールは“ロックファイル”を使う(再現性が上がる)
.dockerignoreを作る(node_modulesやdistを送らない)CMDは配列形式(JSON形式)を優先(シェル解釈の差を減らす)
.dockerignore は地味ですが、ビルドが速くなるだけでなく、不要物の混入やサイズ増大を防げます。
ハンズオン
ここでは Node.js の最小アプリで「Dockerfileを書いて→ビルドして→起動して→キャッシュを体感する」までやります。
(Node 以外でも考え方は同じなので、入門の型として使えます)
0) 事前準備(ファイル作成)
任意の空ディレクトリで以下を用意します。
package.json
{
"name": "dockerfile-beginner",
"version": "1.0.0",
"main": "server.js",
"type": "commonjs",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.19.2"
}
}
server.js
const express = require("express");
const app = express();app.get("/", (_req, res) => {
res.send("Hello from Dockerfile!");
});app.listen(3000, () => {
console.log("listening on :3000");
});
.dockerignore
node_modules
npm-debug.log
.DS_Store
.git
1) まずは“素直な”Dockerfileを書いて動かす
Dockerfile
FROM node:20WORKDIR /appCOPY package.json package-lock.json* ./
RUN npm installCOPY . .EXPOSE 3000
CMD ["npm", "start"]
ポイント:
WORKDIR /appで以降の作業を/appに固定- 依存定義だけ先にコピーしてから
npm install(キャッシュが効きやすい) CMDは配列形式
2) ビルドして起動
docker build -t dockerfile-beginner:1 .
docker run --rm -p 3000:3000 dockerfile-beginner:1
ブラウザで http://localhost:3000 にアクセスしてHello from Dockerfile! が出れば成功です。
3) キャッシュを“体感”する
server.js の文言を少しだけ変えて、もう一度ビルドしてください。
docker build -t dockerfile-beginner:2 .
ここで、依存インストール(RUN npm install)が毎回走らず、キャッシュが使われるはずです。
逆に package.json を変更すると、依存インストールが再実行されます。
これが「変更が少ないものを先に固める」設計の効果です。
4) 入門の次に効く:npm ci とロックファイル
現場では npm install より npm ci が好まれがちです。理由は単純で、ロックファイル通りに厳密に入れる=再現性が上がるからです。
ロックファイル(package-lock.json)がある前提で、Dockerfile をこう変えます。
FROM node:20
WORKDIR /appCOPY package.json package-lock.json ./
RUN npm ciCOPY . .
EXPOSE 3000
CMD ["npm", "start"]
「再現性」は入門のうちに意識しておくと、後でCI/CDに乗せる時に効きます。
まとめ
Dockerfile 入門で押さえるべき本質は「命令の暗記」よりも、次の3点です。
- Dockerfile はレイヤを積む設計図で、ビルドの速さはキャッシュ設計で決まる
- 変更が少ないものを先に、変更が多いものを後に置くとキャッシュが効く
.dockerignoreとロックファイル運用で、速度・サイズ・再現性が安定する
この段階まで来ると、どの言語・フレームワークでも「とりあえず動くDockerfile」から「運用で困らないDockerfile」へ進める土台ができます。
コメントを残す