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 で焼き込まない(イメージ履歴に残る)
  • CMDENTRYPOINT の使い分け(固定の実行体は ENTRYPOINT、可変引数は CMD。最初は CMD だけで十分)

まとめ

Dockerfile は最初から完璧に書く必要はありません。最小構成 → キャッシュ順序 → .dockerignore → 軽量化 → マルチステージ → 非 root、の順で積み上げれば、「なぜその1行があるのか」を説明できる Dockerfile になります。最小構成から始めて、必要な分だけ足していきましょう。

\ 最新情報をチェック /

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください