Dockerfile の書き方 — 最小構成から始める
概要
Dockerfile は「アプリを動かす環境をコードで定義するファイル」です。ただ、最初から公式のベストプラクティスを全部読もうとすると情報量が多く、最初の一歩でつまずきがちです。
この記事では 動く最小構成の Dockerfile から始め、そこに 1 つずつ改善を積み上げていきます。各ステップで「なぜそう書くのか」を明示するので、コピペではなく理解して書けるようになることを目指します。
環境
- Docker Engine 24 以降
- 題材: Node.js の極小 Web アプリ(考え方は Python / Go でも共通)
docker --version # Docker version 24.x.x, build ...
ステップ0: 題材のアプリ
まずコンテナ化する対象を用意します。標準モジュールだけで動く HTTP サーバーです。
// app.js
const http = require("http");
const port = process.env.PORT || 3000;
http
.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("hello from container\n");
})
.listen(port, () => console.log(`listening on ${port}`));
// package.json
{
"name": "minimal-app",
"version": "1.0.0",
"private": true,
"scripts": { "start": "node app.js" }
}
ステップ1: とにかく動く最小の Dockerfile
FROM node:20 WORKDIR /app COPY . . CMD ["node", "app.js"]
| 命令 | 役割 |
|---|---|
FROM |
土台となるベースイメージ |
WORKDIR |
作業ディレクトリの指定・作成 |
COPY |
ホスト → イメージへのコピー |
CMD |
起動時に実行するコマンド |
docker build -t minimal-app . docker run --rm -p 3000:3000 minimal-app curl http://localhost:3000 # hello from container
ステップ2: CMD は exec 形式で書く
CMD node app.js # shell 形式(/bin/sh -c 経由) CMD ["node", "app.js"] # exec 形式(推奨)
shell 形式はシェルを 1 枚挟むため、docker stop の停止シグナル(SIGTERM)がアプリ本体に届きにくく、終了処理が正しく走らないことがあります。exec 形式を基本にしましょう。
ステップ3: レイヤーキャッシュを効かせる(COPY の順序)
COPY . . だけだと、コードを 1 行変えるたびに依存インストールもやり直しになります。Docker は命令ごとにレイヤーを作り、変化がなければキャッシュを再利用するため、変化しにくいものを先にコピーします。
FROM node:20 WORKDIR /app COPY package*.json ./ RUN npm install --omit=dev COPY . . CMD ["node", "app.js"]
app.js だけの編集なら npm install レイヤーはキャッシュ再利用され、再ビルドが大幅に速くなります。
ステップ4: .dockerignore で不要物を持ち込まない
node_modules npm-debug.log .git .gitignore .env Dockerfile .dockerignore
.dockerignore は ビルドコンテキストに送るファイルを絞る ための仕組みで、速度とセキュリティの両方に効きます。特に .env のような秘匿ファイルは必ず除外してください。
ステップ5: イメージを軽くする
FROM node:20-slim
alpine はさらに小さいですが、musl 依存でネイティブモジュールとの相性問題が出ることがあります。まず slim、必要なら alpine を検証してからという順序が安全です。
ステップ6: マルチステージビルドで成果物だけを残す
ビルド時に必要でも実行時に不要なものは、最終イメージから外せます。
FROM node:20 AS build WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build # dist/ を生成すると仮定(仮置き: 構成依存) FROM node:20-slim AS runtime WORKDIR /app ENV NODE_ENV=production COPY package*.json ./ RUN npm install --omit=dev COPY --from=build /app/dist ./dist CMD ["node", "dist/app.js"]
COPY --from=build で前ステージの成果物だけを引き込むのがポイントです。
ステップ7: root を避ける
FROM node:20-slim WORKDIR /app COPY package*.json ./ RUN npm install --omit=dev COPY . . USER node CMD ["node", "app.js"]
node イメージには最初から node ユーザーが用意されています。非 root で動かすことで攻撃面を減らせます。
完成形
FROM node:20-slim WORKDIR /app ENV NODE_ENV=production COPY package*.json ./ RUN npm install --omit=dev COPY . . USER node EXPOSE 3000 CMD ["node", "app.js"]
EXPOSE は「このコンテナは 3000 番を使う」という宣言(ドキュメント的な意味)で、実際の公開は -p で行います。
確認方法
docker build -t minimal-app . docker images minimal-app # サイズ確認 docker run --rm -p 3000:3000 minimal-app curl http://localhost:3000 # hello from container docker run --rm minimal-app id # uid=1000(node) を確認
注意点
latestタグに依存しない(node:20-slimのようにメジャーを固定)apt-get installはキャッシュ削除まで同一RUNに(&& rm -rf /var/lib/apt/lists/*)- 秘匿情報を
ENV/COPYで焼き込まない(イメージ履歴に残る) CMDとENTRYPOINTの使い分け(固定の実行体はENTRYPOINT、可変引数はCMD。最初はCMDだけで十分)
まとめ
Dockerfile は最初から完璧に書く必要はありません。最小構成 → キャッシュ順序 → .dockerignore → 軽量化 → マルチステージ → 非 root、の順で積み上げれば、「なぜその1行があるのか」を説明できる Dockerfile になります。最小構成から始めて、必要な分だけ足していきましょう。
