Dockerを触っていると、最初は「とりあえず動けばOK」でDockerfileを書きがちです。ところが、チーム開発や本番運用に寄せていくと、Dockerfileは単なる起動手順ではなく ビルド速度・イメージサイズ・セキュリティ・再現性 を左右する重要な“設計書”になります。
この記事では、検索されやすいキーワード(Dockerfile ベストプラクティス / 最適化 / 軽量化 / キャッシュ / マルチステージ / セキュリティ / 非root / .dockerignore)を押さえながら、すぐ実務に使える書き方 を、ハンズオン形式で身につける構成にしました。
この記事でわかること
- Dockerfileベストプラクティスの全体像(速い・軽い・安全)
- ビルドキャッシュを効かせるDockerfileの書き方
- イメージ軽量化(slim/alpineの考え方、不要物の除去、マルチステージ)
- セキュアなDockerfile(非root、最小権限、秘密情報を埋め込まない)
apt-get/pip/npmの“やりがち”アンチパターンと改善例.dockerignoreの重要性と設定例- ハンズオン:悪いDockerfile→改善→最適化→セキュア化まで
Dockerfileの“良し悪し”は何で決まる?
実務で「良いDockerfile」とされるものは、だいたい次の条件を満たします。
1) 再現性(Reproducible)
誰が、いつ、どこでビルドしても、同じ成果物になりやすいこと。
依存関係の固定(lockファイルやバージョン固定)や、意図しないキャッシュの暴発を避けることが重要です。
2) ビルドが速い(Fast build)
CIで毎回遅いと開発体験が崩れます。
キャッシュを壊しにくいCOPY順序 と、依存インストール層の分離が鍵です。
3) イメージが軽い(Small image)
軽いほど配布が速く、起動が速く、脆弱性の面積も減ります。
マルチステージビルドや不要物削除が効きます。
4) セキュア(Secure)
“動けばOK”のDockerfileは、root実行や秘密情報混入など、事故の温床になりがちです。
最低限のガード(非root、最小パッケージ、シークレットの扱い)を入れましょう。
まず押さえるベストプラクティス10選
- 小さめのベースイメージを選ぶ(
slim/ distroless 等を検討) .dockerignoreを必ず書く(node_modulesや.gitの混入防止)- 依存関係のインストールは先に分離(キャッシュを最大化)
- ビルド成果物と実行環境を分ける(マルチステージ)
- RUNをまとめてレイヤー数と容量を減らす
- パッケージマネージャのキャッシュを消す(apt/pip/npm)
- バージョン固定(lockファイル/タグ固定)で再現性を高める
- rootで動かさない(USER指定)
- 秘密情報をDockerfileに書かない(ENVに埋めない)
- HEALTHCHECKや最小権限など運用前提の設計をする
このあと、ハンズオンで「悪い例→改善」を通して、体で覚えます。
ハンズオン:悪いDockerfileを改善していく(Node.js例)
準備:サンプルアプリを作る
作業ディレクトリを作って、以下を配置してください。
package.json
{
"name": "dockerfile-best-practices",
"version": "1.0.0",
"main": "server.js",
"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("Dockerfile Best Practices!"));
app.get("/health", (_req, res) => res.json({ ok: true }));
app.listen(3000, () => console.log("listening on 3000"));
STEP1:よくある“悪いDockerfile”(アンチパターン)
まず、あえて悪い例を作ってビルドします。
Dockerfile.bad
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
EXPOSE 3000
CMD ["node", "server.js"]
何が問題?
COPY . .が早すぎて キャッシュが壊れやすいnpm installは環境差が出やすい(lock未活用)→ 再現性が落ちるnode:20はslimより大きくなりやすい.dockerignoreがないと不要物混入で太る
ビルドしてサイズを見てみます:
docker build -t df-bad -f Dockerfile.bad .
docker images | head
STEP2:キャッシュが効くDockerfileに改善(依存の分離)
次に改善版です。
Dockerfile
FROM node:20-slim
WORKDIR /app
# 依存関係だけ先にコピー(キャッシュが効く)
COPY package.json package-lock.json* ./
RUN npm ci --omit=dev
# アプリ本体を後でコピー(変更頻度が高い)
COPY server.js ./
EXPOSE 3000
CMD ["npm", "start"]
ビルド:
docker build -t df-good:1.0 .
docker images | head
改善ポイント
- 依存のインストール層が再利用されやすい
npm ciは lockファイル前提で再現性が上がる(CI向き)
もし
package-lock.jsonがない場合は、ローカルでnpm iして生成してから進めると、より“実務の形”になります。
STEP3:.dockerignore でビルドコンテキストを減らす
Dockerfileが良くても、コンテキストがデカいと終わります。
以下を追加します。
.dockerignore
node_modules
npm-debug.log
.git
.gitignore
Dockerfile*
README.md
再ビルドして、体感的に速くなるか確認:
docker build -t df-good:1.1 .
STEP4:マルチステージで「ビルド用」と「実行用」を分離
今回のアプリはビルド工程が軽いですが、本番ではTypeScriptやフロントビルドで必須になります。
“形”として覚えるためにマルチステージにします。
Dockerfile(multi-stage)
# ---- build stage ----
FROM node:20-slim AS build
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci --omit=dev
COPY server.js ./
# ---- runtime stage ----
FROM node:20-slim
WORKDIR /app
COPY --from=build /app /app
EXPOSE 3000
CMD ["node", "server.js"]
ビルド:
docker build -t df-good:2.0 .
docker images | head
セキュリティベストプラクティス(Dockerfileで最低限やること)
1) 非rootユーザーで実行する(最重要)
Dockerfileを書き慣れてないと、rootで動かしがちです。
しかし本番では コンテナ突破時の被害を小さくする ために、できる限り非rootで動かします。
Node公式イメージには node ユーザーが用意されています。これを使います。
改善例
FROM node:20-slim
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci --omit=dev
COPY server.js ./
EXPOSE 3000
USER node
CMD ["node", "server.js"]
起動:
docker build -t df-secure:1.0 .
docker run --rm -p 3000:3000 df-secure:1.0
2) 秘密情報をDockerfileに書かない
やりがち:
ENV API_KEY=xxxxx
これは イメージに残る(履歴やレイヤーに残る)可能性があり危険です。
秘密情報は実行時に渡します。
例:
docker run --rm -e API_KEY=xxxx -p 3000:3000 df-secure:1.0
(本番は Compose / Kubernetes Secrets などへ)
3) apt-getの正しい書き方(キャッシュとサイズ)
Debian系(slim等)でよくやるのが apt-get。
悪い例:
RUN apt-get update
RUN apt-get install -y curl
良い例(1RUNにまとめる、不要キャッシュ削除):
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
--no-install-recommendsで余計な依存を減らす/var/lib/apt/listsを消して容量削減- 1つのレイヤーにまとめて効率化
4) COPYよりADDを乱用しない
ADD はURL取得やtar自動解凍など余計な挙動があります。
基本は COPY を使い、意図がある場合のみADDを使います。
5) HEALTHCHECK(運用で効く)
KubernetesやComposeでも死活監視は大事です。
アプリに /health があるならDockerfileでHealthcheckを入れる選択肢があります。
例:
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', r => process.exit(r.statusCode===200?0:1)).on('error', () => process.exit(1))"
(本番はオーケストレーター側で管理することも多いので、方針に合わせて採用してください)
ハンズオン:ビルド最適化の“効き”を確認する
1) レイヤーを確認(どこが太いか)
docker history df-good:2.0
2) 詳細設定を確認(CMD/USER/ENV)
docker inspect df-secure:1.0
3) キャッシュが効いているかを体感
server.js の文字を少し変えて再ビルド:
docker build -t df-good:2.1 .
依存インストールがスキップされればOK(ログで分かります)。
よくある質問(FAQ)
Q. alpineは使うべき?
「小さい」ことは魅力ですが、ネイティブ依存(node-gyp等)でハマることがあります。
まずは slim で安定運用し、必要に応じてalpineを検討するのが現実的です。
Q. FROMのタグは固定すべき?
本番では再現性のために固定が強いです(例:node:20.11.1-slim のように)。
ただしセキュリティ更新も取り込みたいので、チームの更新ポリシー(定期更新)とセットで考えます。
まとめ:これだけ守れば実務で強いDockerfileになる
Dockerfileベストプラクティスは、最終的に次の3つに集約されます。
- キャッシュ最適化:依存を先に、アプリを後に(COPY順序)
- 軽量化:.dockerignore、不要キャッシュ削除、マルチステージ
- セキュリティ:非root、最小パッケージ、秘密情報を埋めない
コメントを残す