はじめに
Dockerfile は「アプリをコンテナで動かすための手順書」ですが、実務ではそれ以上の意味を持ちます。
ビルドが遅いと、開発もCIも待ち時間が増えて疲弊します。安全でない Dockerfile は、脆弱性だけでなく「意図しない更新」「権限の過剰」「秘密情報の混入」など、運用で痛い事故を起こします。
そして厄介なのが、速度と安全性は“あとから足す”と手戻りが大きいことです。
最初から「速い構造」「安全な前提」「再現できる成果物」を意識しておくと、チーム全体の生産性が上がり、トラブル対応も減ります。
この記事では、ありがちな Dockerfile を題材にしながら、速さ(キャッシュ・レイヤー・依存管理)と安全性(最小権限・秘密情報・再現性)を同時に満たす書き方を、座学→ハンズオンで整理します。
※“入門の次”として実務で効くポイントに寄せています。
座学
1) ビルドが遅くなる典型:キャッシュが壊れるDockerfile
Docker のビルドが速い理由は「レイヤーキャッシュ」です。逆に遅いDockerfileは、毎回キャッシュを捨てる構造になっています。
よくある例:
COPY . .を最初にやってしまう(小さな変更で依存インストールまで毎回やり直し)npm install/pip installが毎回走る.dockerignoreが無くて無駄なファイルまで送っている(node_modules、.git、ビルド成果物)
基本はこれです:
- 依存の定義ファイル(package-lock.json / requirements.txt)だけ先にCOPY
- 依存インストールを先に実行
- アプリ本体は最後にCOPY
これだけで「コードを少し変えただけのビルド」が劇的に速くなります。
2) BuildKitで“速さ”を一段上げる:キャッシュマウント
近年のDockerは BuildKit が前提になりつつあり、BuildKit を使うと 依存ダウンロードのキャッシュが効くようになります。
代表が RUN --mount=type=cache です。
- Node:
~/.npmや/root/.cacheのようなキャッシュ領域をビルド間で再利用 - Python:pip のダウンロードキャッシュを再利用
- OSパッケージ:aptのキャッシュを(用途次第で)再利用
レイヤーキャッシュだけに頼るより、“ダウンロードそのもの”が速くなるのが効きます。
CIで特に体感差が出ます。
3) 安全性の基本:最小権限(rootで動かさない)
Dockerfileの安全性で一番効くのは、実は難しい脆弱性対策よりも、「rootでアプリを動かさない」です。
- 侵害されたときの被害範囲が減る
- ファイルの書き込み先が制御しやすい
- 本番運用の“当たり前”に近づく
「root以外ユーザーを作る」「必要なディレクトリだけ権限付与」「実行ユーザーを固定」
この3点をテンプレ化すると、チームでブレません。
4) “秘密情報を入れない”は速度より重要:ビルド時のシークレット管理
やりがち事故:
.envや認証キーをCOPYしてしまうARG TOKEN=...を使ってRUNでアクセスし、履歴に残る- ビルドログやレイヤーに秘密が残る
BuildKit には --mount=type=secret があり、レイヤーに残さず一時的に秘密情報を渡せます。
速度というより「事故を防ぐ仕組み」です。
5) 再現性:同じDockerfileでも“昨日と今日で別物”になる問題
Dockerfileが安全でも、ビルドするたび中身が変わると運用が崩れます。
FROM ubuntu:latestのような可変タグapt-get update && apt-get installでその日のリポジトリ状態に依存npm install(ロック無し)で依存が変化
対策は段階的に:
- ベースイメージは 固定タグ、可能なら digest固定
- Nodeは
npm ci、Pythonはロック(requirements + hash など) LABELでビルド情報(commit / build time)を残す
「再現性」は速度にも効きます。差分が減るほどキャッシュが当たりやすくなるからです。
ハンズオン
ここでは “よくある遅くて危ないDockerfile” を、速く・安全に改造します。題材はNodeアプリ(最小のHTTP)にします。
0) サンプルアプリ作成
app/ を作って以下を配置します。
package.json
{
"name": "fast-safe-dockerfile",
"version": "1.0.0",
"main": "server.js",
"type": "module",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.19.2"
}
}
server.js
import express from "express";
const app = express();
app.get("/", (_req, res) => res.send("OK: fast & safe Dockerfile"));
app.listen(3000, () => console.log("listening on :3000"));
ロックファイルを作ります:
cd app
npm install
1) 悪い例:毎回遅くて、運用も不安
Dockerfile.bad
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
EXPOSE 3000
CMD ["npm", "start"]
ビルド:
docker build -f Dockerfile.bad -t demo:bad .
このDockerfileの問題点:
COPY . .が先なので、コードが1行変わるだけでnpm installまで毎回やり直し- root実行が前提(ユーザー未指定)
- 不要ファイル(.git など)までビルドコンテキストに入りやすい
2) まず速くする:依存とアプリを分けてキャッシュを効かせる
Dockerfile.fast
FROM node:20-slim
WORKDIR /app
# 依存定義だけ先にコピー(変更頻度が低い)
COPY package.json package-lock.json ./
# lock前提でインストール(再現性も上がる)
RUN npm ci
# アプリ本体は最後にコピー(変更頻度が高い)
COPY server.js ./
EXPOSE 3000
CMD ["npm", "start"]
ビルド:
docker build -f Dockerfile.fast -t demo:fast .
体感ポイント:
server.jsを変更して再ビルドすると、npm ciがキャッシュでスキップされやすいnpm installではなくnpm ciなので、CIでもブレにくい
3) BuildKitでさらに速く:ダウンロードキャッシュを使う(CIで効く)
Dockerfile.fastbuildkit
# syntax=docker/dockerfile:1.7
FROM node:20-slim
WORKDIR /app
COPY package.json package-lock.json ./
# npmキャッシュをマウントしてダウンロードを高速化
RUN --mount=type=cache,target=/root/.npm \
npm ci
COPY server.js ./
EXPOSE 3000
CMD ["npm", "start"]
ビルド(BuildKit有効化):
DOCKER_BUILDKIT=1 docker build -f Dockerfile.fastbuildkit -t demo:fastbuildkit .
ポイント:
- レイヤーキャッシュが外れても「ダウンロードが速い」ので立て直しが効く
- CIでリモートキャッシュと組み合わせるとさらに強い(次のステップで触れます)
4) 安全にする:非rootユーザーで実行する
Dockerfile.safe
# syntax=docker/dockerfile:1.7
FROM node:20-slim
WORKDIR /app
# 依存定義だけ先にコピー
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
COPY server.js ./
# 実行ユーザーを作成(最小権限)
RUN useradd -m -u 10001 appuser \
&& chown -R appuser:appuser /app
USER appuser
EXPOSE 3000
CMD ["npm", "start"]
起動して確認:
docker build -f Dockerfile.safe -t demo:safe .
docker run --rm -p 3000:3000 demo:safe
# http://localhost:3000 を開く
ここで得られる安心:
- アプリがrootで動かない
- 書き込みが必要な場合は「どこに書くか」が明確になる
- 事故が起きても被害を抑えやすい
5) 秘密情報を“入れずに使う”:BuildKit secret(事故防止)
例として「プライベートレジストリにアクセスするトークン」が必要なケースを想定します。
注意:ARGやENVで入れるのは避ける(レイヤーや履歴に残り得ます)
BuildKit secret を使う例(疑似):
# syntax=docker/dockerfile:1.7
FROM node:20-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=secret,id=npm_token \
--mount=type=cache,target=/root/.npm \
sh -c 'echo "//registry.npmjs.org/:_authToken=$(cat /run/secrets/npm_token)" > .npmrc && npm ci && rm -f .npmrc'
COPY server.js ./
CMD ["node", "server.js"]
ビルド時に secret を渡す:
DOCKER_BUILDKIT=1 docker build \
--secret id=npm_token,src=./.npm_token \
-t demo:secret .
ポイント:
- 秘密情報がイメージに残らない
- 誤って
docker historyなどに出にくい - 「秘密はビルドに混ぜない」を仕組みで担保できる
6) 仕上げ:.dockerignore で“送らないもの”を固定する
ビルドが遅い原因は Dockerfile だけではありません。
ビルド時に Docker が送る「コンテキスト」が巨大だと、それだけで遅いです。
.dockerignore(例)
node_modules
npm-debug.log
.git
.gitignore
Dockerfile*
README.md
これで、不要ファイルがビルドに混ざらず、速度・安全性(意図しない混入)両方に効きます。
まとめ
Dockerfile を「速く・安全に」するコツは、テクニックの寄せ集めではなく、構造を整えることです。
- 速さ:
- 依存定義 → インストール → アプリ本体の順にしてキャッシュを最大化
- BuildKit の
cache mountでダウンロードを再利用し、CIでも速くする .dockerignoreで“送らない”を明文化する
- 安全:
- rootで動かさない(最小権限)
- 秘密情報はイメージに入れない(BuildKit secret)
- 再現性(ロック、固定タグ/可能ならdigest)で「同じものを作れる」に寄せる
「速いビルド」は開発体験を上げ、「安全な前提」は事故確率を下げます。
Dockerfile を運用資産として扱う感覚が身につくと、コンテナ運用全体が安定します。
コメントを残す