Dockerイメージ入門:仕組み・作り方・最適化まで

Dockerを学び始めると、最初は「コンテナを起動できた!」で満足しがちですが、少し運用寄りのことをやろうとすると必ず壁になるのが Dockerイメージ です。
「なぜビルドが遅いのか」「なぜイメージが太るのか」「なぜキャッシュが効かないのか」「なぜ本番で動くはずが動かないのか」──これらの多くは、イメージの作り方と構造(レイヤー/キャッシュ/ビルドコンテキスト)を理解すると一気に解像度が上がります。

この記事では、Dockerイメージを “なんとなく作る” から “意図して作る” にステップアップするために、基礎から実践までを丁寧にまとめます。ハンズオンは「手元で動く」ことにこだわり、最後は 最適化(軽量化)レジストリ運用(push/pull) まで繋げます。



Dockerイメージとは?(コンテナとの違いを腹落ちさせる)

Dockerの世界を一言で言うと、「アプリを動かすための環境を“成果物”として持ち運ぶ」 です。
ここでいう成果物が Dockerイメージ です。

  • イメージ(image):実行に必要なファイル一式(OSの一部、ライブラリ、アプリ本体、設定など)をまとめた“読み取り専用”の塊
  • コンテナ(container):そのイメージを元に起動した“実行中のプロセス” + “書き込み可能な層(コンテナの差分)”

つまり、イメージは設計図ではなく「実行できる形に固めたパッケージ」 に近いです。
コンテナは「そのパッケージを展開して動かしている状態」です。

この違いが分かってくると、「なぜコンテナを消してもイメージは残るのか」「なぜ同じイメージから複数コンテナを起動できるのか」も自然に理解できます。


Dockerイメージの構造:レイヤー(Layer)でできている

Dockerイメージを理解する上で最重要なのが レイヤー です。
Dockerfileの命令の多くは、実行されるたびに「新しいレイヤー」を作ります。

たとえば、以下のようなイメージのイメージ(語感…)です。

  1. FROM node:20-slim(ベースとなるレイヤー)
  2. WORKDIR /app(設定のレイヤー)
  3. COPY package.json ...(ファイルを追加するレイヤー)
  4. RUN npm ci ...(依存をインストールしたレイヤー)
  5. COPY server.js ...(アプリ本体を追加するレイヤー)

この仕組みの何が嬉しいかというと、同じ操作は再利用できる(キャッシュが効く) ことです。
逆に言うと、キャッシュを壊すような書き方をすると、毎回最初から作り直しになりビルドが遅くなります。


ハンズオン:Dockerイメージを作って動かす(Node.js最小構成)

ここからは手を動かします。
「Dockerfileを書いて build して run する」という一連を体に入れましょう。

前提

  • Docker Desktop(Windows/Mac)または LinuxにDockerが入っている
  • ターミナル(PowerShell / Terminal / bash など)

1) サンプルアプリを用意

作業ディレクトリを作って、以下3つのファイルを置きます。

package.json

{
  "name": "docker-image-handson",
  "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("Hello Docker Image!"));
app.get("/health", (_req, res) => res.json({ ok: true }));

app.listen(3000, () => console.log("listening on 3000"));

(任意)README.md を置いても良いですが、後で .dockerignore を学ぶときに「余計なファイルが混入し得る」ことが分かります。


2) 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"]

ここで大事なのは COPYの順序 です。
もし最初に COPY . . と全部コピーしてしまうと、ソースのちょっとした変更でも「レイヤーが変わった」と判定され、npm ci が毎回実行されがちです。これは体感でかなり遅くなります。

依存 → アプリ の順番に分けるのは、Dockerfileの定番テクニックです。


3) ビルド(docker build)

docker build -t docker-image-handson:1.0 .
  • -t はタグ付け(名前:バージョン
  • . はビルドコンテキスト(このフォルダの中身をDockerが参照する範囲)

よくある誤解として、「Dockerfileだけ見てビルドされる」と思いがちですが、実際には ビルドコンテキスト がめちゃくちゃ重要です。
巨大なディレクトリを . にすると、その分ビルドが重くなります(次の章で .dockerignore を入れます)。


4) 起動(docker run)と動作確認

docker run --rm -p 3000:3000 docker-image-handson:1.0
  • -p 3000:3000 はポートの転送(ホスト:コンテナ)
  • --rm は終了後にコンテナを自動削除(作業が散らからない)

別ターミナルで動作確認:

curl http://localhost:3000/
curl http://localhost:3000/health

ブラウザで http://localhost:3000/ を開いてもOKです。


“イメージを観察する”と理解が一段上がる(便利コマンド)

動いたら終わり、ではなく、ここでイメージを観察します。
「どのレイヤーが重いか」「どんな設定で動くか」を見れるようになると、トラブル対応が速くなります。

イメージ一覧

docker images

レイヤー履歴(どの命令で容量が増えたか)

docker history docker-image-handson:1.0

詳細情報(環境変数、CMD、EXPOSE、レイヤーなど)

docker inspect docker-image-handson:1.0

inspect は情報量が多いので、慣れてきたら --format を使って必要な項目だけ抜くのもおすすめです(例:起動コマンドだけ見る等)。


ハンズオン:イメージが太る原因=ビルドコンテキスト(.dockerignore)

次に、現場でめちゃくちゃ効くのが .dockerignore です。
これを入れないと、node_modules.git がビルド対象に入ってしまい、ビルドが遅い/容量が増える/意図しないファイルが混入する などの原因になります。

.dockerignore

node_modules
npm-debug.log
.git
.gitignore
Dockerfile
.dockerignore

この状態で再ビルドしてみてください:

docker build -t docker-image-handson:1.0 .

ビルドログの「転送されるコンテキストサイズ」が小さくなるのが分かるはずです。


ハンズオン:マルチステージビルド(本番で定番の軽量化)

アプリによっては、ビルドにしか使わないツール(コンパイラ、ビルド依存、テストツールなど)が増えて、イメージが大きくなります。
そのときに使うのが マルチステージビルド です。

ポイントはシンプルで、

  • build用ステージ:必要なもの全部使ってビルドする
  • runtime用ステージ:実行に必要な成果物だけコピーして動かす
    という二段構成にすることです。

マルチステージ版Dockerfile

# ---- 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 docker-image-handson:1.1 .

サイズ比較:

docker images | head

この例は小規模なので差が劇的ではないかもしれませんが、フロントエンドビルド(Vite/Next.js)やPythonでビルド依存があるとき、差が顕著に出ます。


タグ(tag)の設計:latestは便利だが危険もある

Dockerイメージのタグは「バージョン管理」の要です。

よくある運用例:

  • 開発:myapp:devmyapp:latest
  • 本番:myapp:1.0.3myapp:<git-sha>

latest は「最新っぽい」だけで、誰がいつ更新したlatestかが曖昧 になります。
CI/CDや本番では「再現性」が重要なので、固定タグ(SemVerやコミットSHA)を推奨します。

タグ付け例:

docker tag docker-image-handson:1.1 yourname/docker-image-handson:1.1

レジストリ(Docker Hub)にpush/pullして配布する

イメージの真価は「配布できる」ことです。
ローカルで作って終わりではなく、レジストリに載せるところまでやると一気に“使える知識”になります。

1) ログイン

docker login

2) push

docker push yourname/docker-image-handson:1.1

3) pull(別PCやサーバで)

docker pull yourname/docker-image-handson:1.1

運用で効く:掃除(prune)と移動(save/load)

Dockerを触り続けると、イメージやキャッシュが溜まってディスクを圧迫します。

使ってないイメージを削除

docker image prune

使ってないもの全部(慎重に)

docker system prune

オフライン環境へ渡す(save/load)

docker save -o handson.tar docker-image-handson:1.1
docker load -i handson.tar

よくあるハマりどころ(現場で起きがち)

キャッシュが効かない(毎回npm installが走る)

原因の多くはDockerfileの順序です。
依存解決(npm ci)より前に COPY . . をしていると、ファイル変更で依存レイヤーが壊れます。
package.jsonを先にコピー → npm ci → アプリコピー が鉄板です。

イメージが肥大化する

  • .dockerignore がない
  • runtimeに不要なものを持ち込んでいる
  • ビルド用ツールが入りっぱなし
    .dockerignore + マルチステージ が基本解決策です。

秘密情報をDockerfileに書いてしまった

ENV API_KEY=... のように書くと、イメージに残る可能性があります。
秘密情報は 実行時に環境変数Secret管理 で渡すのが原則です。


次のステップ(この記事の続き候補)

  • Dockerfileのベストプラクティス(USER/権限/最小権限)
  • イメージ脆弱性スキャン(Trivy)とCI組み込み
  • 署名(cosign)やSBOM(サプライチェーン対策)
  • buildx で amd64/arm64 マルチアーキテクチャ対応

まとめ

Dockerイメージは、Docker運用の中心にある“成果物”です。
レイヤー構造とキャッシュを理解し、.dockerignore とマルチステージを使えるようになると、ビルドの速さ・軽さ・安全性 が一気に上がります。


投稿日

カテゴリー:

投稿者:

コメント

“Dockerイメージ入門:仕組み・作り方・最適化まで” への1件のコメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です