Dockerfile実践

目次

概要

Dockerfileは、コンテナイメージの作り方を記述するファイルです。単に動くimageを作るだけでなく、再現性、セキュリティ、cache効率、運用しやすさに影響します。

要点

Dockerfileは「環境構築手順」ではなく「配布する実行環境の設計」です。layer、cache、multi-stage build、非root実行、secretの扱いを意識すると、安全で速いimageに近づきます。

Dockerfileの基本

最小例です。

FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
CMD ["node", "server.js"]

代表的な命令です。

命令 役割
FROM base image
WORKDIR 作業ディレクトリ
COPY ファイルをimageに入れる
RUN build時にコマンド実行
ENV 環境変数
USER 実行ユーザー
EXPOSE 想定ポート
CMD container起動時の既定コマンド
ENTRYPOINT 起動時に必ず実行するコマンド

layerとcache

Dockerfileの各命令はlayerを作ります。build cacheは、前のlayerが変わらなければ再利用されます。

flowchart TD A["FROM"] --> B["COPY package.json"] B --> C["RUN npm ci"] C --> D["COPY source"] D --> E["RUN npm run build"]

依存関係のinstallをsource copyより前に置くと、ソース変更だけでは npm ci のlayerを再利用できます。

悪い例です。

COPY . .
RUN npm ci

良い例です。

COPY package*.json ./
RUN npm ci
COPY . .

cacheを効かせる書き方

Dockerのcacheは「前の命令までの入力が同じなら、同じlayerを再利用できる」という考え方で動きます。つまり、頻繁に変わるものを早い段階で COPY すると、その後ろの重い処理まで毎回やり直しになります。

依存関係のファイルとアプリケーションのソースを分けるのが基本です。

FROM node:22-bookworm-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

FROM deps AS build
COPY . .
RUN npm run build

Pythonでも同じです。

FROM python:3.12-slim AS app
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]

BuildKitを使える環境では、package managerのdownload cacheをlayerに焼き込まずに、build cacheとして使えます。

# syntax=docker/dockerfile:1.7
FROM node:22-bookworm-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN npm run build

この書き方では、/root/.npm のcacheはimageの中身には残りません。buildを速くしつつ、runtime imageを肥大化させないための実践的な折衷です。

cacheを効かせる設計は、次の順で考えると整理しやすくなります。

変化しにくい 変化しやすい
base image アプリケーションコード
OS package install テスト対象のファイル
language runtime 静的アセット
dependency lockfile 設定ファイル

よくある失敗は、.dockerignore が弱いまま COPY . . を早く書いてしまうことです。node_modules.gitdist、coverage、localの .env がcontextに入ると、cacheが壊れやすくなり、buildも遅くなります。

multi-stage build

multi-stage buildは、build用の環境と実行用の環境を分けます。

FROM node:22 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html

build toolやdev dependenciesを最終imageに入れずに済みます。

flowchart LR B["builder image<br>toolchainあり"] --> D["dist/"] D --> R["runtime image<br>配信に必要なものだけ"]

multi-stage buildは「小さくする」ためだけではありません。build、test、runtimeを分けると、CIの中で同じDockerfileから目的別のtargetを作れます。

FROM node:22-bookworm-slim AS base
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

FROM base AS test
COPY . .
RUN npm test

FROM base AS build
COPY . .
RUN npm run build

FROM nginx:1.27-alpine AS runtime
COPY --from=build /app/dist /usr/share/nginx/html
docker build --target test -t app:test .
docker build --target runtime -t app:runtime .

targetを分けておくと、CIではtestまで、releaseではruntimeまで、というように同じ設計を使い回せます。

非root実行

container内でもroot実行は避けるのが基本です。

RUN addgroup -S app && adduser -S app -G app
USER app

rootで動かすと、脆弱性があったときの影響が大きくなります。アプリケーションが書き込むディレクトリだけ権限を付け、不要な権限を持たせないようにします。

imageを小さくする

imageが大きいと、pullが遅くなり、脆弱性スキャン対象も増えます。

小さくする方法です。

  • .dockerignore を使う
  • multi-stage buildを使う
  • 不要なpackageを入れない
  • build cacheとruntimeを分ける
  • base imageを見直す

.dockerignore の例です。

node_modules
dist
.git
.env
*.log

secretを入れない

Dockerfileにsecretを書いてはいけません。

悪い例です。

ENV API_TOKEN=secret

image layerや履歴に残る可能性があります。secretはruntimeの環境変数、secret manager、CI/CDのsecret機能で渡します。

BuildKit secrets

private registryやprivate package repositoryに接続するため、build中だけtokenが必要になることがあります。この場合も ARGENV で渡すのは避けます。BuildKitのsecret mountを使うと、secretをlayerに残さず、特定の RUN 命令だけに渡せます。

# syntax=docker/dockerfile:1.7
FROM node:22-bookworm-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
COPY . .
RUN npm run build

build時にsecretを渡します。

docker build \
  --secret id=npmrc,src="$HOME/.npmrc" \
  -t app:local .

この場合、.npmrc はbuild中の一時的なmountとして見えるだけです。image historyや最終imageのfilesystemに残す前提で設計しないことが重要です。

secretの扱いは、次の3段階で分けます。

種類 渡し方 Dockerfileに書くか
build中だけ必要なtoken BuildKit secret mount 書かない
runtimeで必要なsecret orchestrator、secret manager、CI/CD secret 書かない
公開してよい設定値 ENV または設定ファイル 書いてよい

ARG はbuild-time parameterですが、secretを安全に隠す仕組みではありません。proxy URLやpackage mirrorのような非secret設定には使えますが、credentialには使わない方が安全です。

healthcheck

HEALTHCHECK は、containerが生きているだけでなく、アプリケーションとして応答できるかを確認します。

HEALTHCHECK --interval=30s --timeout=3s \
  CMD wget -qO- http://localhost:8080/health || exit 1

ただし、healthcheckが重すぎると負荷になります。軽量で依存が少ないendpointを使います。

build contextと.dockerignore

Docker buildでは、Dockerfileのある場所だけでなく、build contextに含まれるファイルがdaemonへ送られます。

docker build -t app .

この . がbuild contextです。不要なファイルが含まれると、buildが遅くなり、secretを誤って送る危険もあります。

flowchart LR C["build context"] --> D["Docker daemon"] D --> L["layers"] Ignore[".dockerignore"] --> C

.dockerignore には、imageに不要なものを書きます。

.git
node_modules
dist
.env
*.log
coverage

COPY . . は便利ですが、contextに含まれるものをまとめてimageに入れるため、.dockerignore とセットで考えます。

build contextはlocal directoryだけとは限りません。Git repositoryやremote tarballをcontextにできます。ただし、Dockerfileから参照できるのはcontextに含まれるファイルだけです。親ディレクトリのファイルを COPY ../secret . のように直接持ち込むことはできません。

flowchart TD A["作業ディレクトリ"] --> B["build context"] G[".git"] -.dockerignore.-> X["送らない"] N["node_modules"] -.dockerignore.-> X E[".env"] -.dockerignore.-> X B --> C["builder"] C --> D["COPYできる範囲"]

contextを小さくすると、次の効果があります。

効果 理由
buildが速くなる daemonやremote builderへ送る量が減る
cacheが壊れにくい 無関係なファイル変更がbuild入力に入りにくい
secret混入を避けやすい .env やcredentialをcontextから除外できる
CIが安定する localにはあるがCIにはないファイルへの依存を減らせる

base imageとdigest

base imageは、アプリケーションの土台です。サイズだけで選ぶと、debugしにくい、必要なlibcがない、security updateの運用が難しい、といった問題が出ます。

選択肢 向いている場面 注意点
debian / ubuntu 汎用性と互換性を重視する imageは大きめ
slim 互換性を保ちつつ小さくしたい 必要なtoolが省かれる
alpine 小さいimageが必要 musl libc差分で詰まることがある
distroless runtimeだけを最小化したい shellがなくdebugしにくい
language official image Node、Python、Goなどの標準環境 tagの更新方針を確認する

tagは便利ですが、同じtagが将来も同じ中身を指すとは限りません。再現性を強めたい場合はdigest固定を検討します。

FROM node:22-bookworm-slim@sha256:...

digest固定にはtrade-offがあります。完全に固定するとbuildは再現しやすくなりますが、security updateは自動で入らなくなります。そのため、固定する場合は「定期的にdigestを更新する」「scanで更新必要性を検知する」という運用とセットにします。

runtime設定

Dockerfileはbuildだけでなく、containerの起動時の振る舞いも決めます。CMDENTRYPOINTENVEXPOSEUSERWORKDIR はruntimeの読みやすさに直結します。

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
USER 10001
EXPOSE 8000
ENTRYPOINT ["python", "-m", "uvicorn"]
CMD ["app:app", "--host", "0.0.0.0", "--port", "8000"]

ENTRYPOINT は「このimageの主役となる実行ファイル」、CMD は「既定の引数」と考えると分かりやすいです。

docker run app
docker run app app:app --host 0.0.0.0 --port 9000

運用では、次の点を明示しておくと事故が減ります。

設定 意味 注意
WORKDIR 相対pathの基準 / のまま作業しない
USER 起動ユーザー rootのままにしない
EXPOSE 想定ポート 実際のpublishではない
ENV 公開してよい既定値 secretを入れない
ENTRYPOINT 主コマンド shell formよりexec formが扱いやすい
CMD 既定引数 composeやrunで上書きされる

scanとSBOM

imageには、アプリケーションコードだけでなく、OS package、language runtime、native library、依存package、設定ファイルが含まれます。脆弱性対応では「アプリの依存だけを見る」では足りません。

flowchart TD I["container image"] --> A["application code"] I --> D["language dependencies"] I --> R["runtime"] I --> O["OS packages"] I --> C["configuration"]

実務では、次のような確認を組み合わせます。

観点
vulnerability scan Docker Scout、Trivy、Grype
SBOM Syft、Docker build attestation
base image更新 Renovate、Dependabot、Docker Scout
署名と検証 cosign、registry policy
不要ファイル確認 docker image historydocker run --rm -it image sh

scan結果は、件数だけで判断しません。重要なのは、修正版があるか、runtimeに到達するpathか、外部入力から悪用可能か、base imageの更新で直るかです。

また、SBOMは「何が入っているか」を説明するための材料です。監査、incident response、license確認、脆弱性影響調査の入口になります。

トラブルシューティング

Dockerfileの問題は、build時、起動時、runtimeの3段階で切り分けます。

症状 見る場所 よくある原因
buildが遅い --progress=plain contextが大きい、cacheが効いていない
cacheが効かない COPY の順序 source全体を先にcopyしている
imageが大きい docker image history build tool、cache、不要ファイルが残っている
containerがすぐ終了 docker logs foreground processがない、CMDが間違い
ファイルが見つからない WORKDIRCOPY context外のファイルを期待している
permission error USER、file owner 非root化後に書き込み先権限がない
secretが残る docker history、layer ENVARG、copy済みファイルに含まれる

調査時によく使うコマンドです。

docker build --progress=plain -t app:debug .
docker image history app:debug
docker run --rm app:debug
docker logs <container>
docker exec -it <container> sh

runtime imageにshellがない場合は、debug用targetを別に用意する手もあります。

FROM runtime AS debug
USER root
RUN apt-get update && apt-get install -y --no-install-recommends curl procps \
  && rm -rf /var/lib/apt/lists/*

本番imageにdebug toolを入れっぱなしにするのではなく、調査用targetとして分けるのが安全です。

運用チェックリスト

Dockerfileをレビューするときの観点です。

観点 確認
base image 公式・更新状況・サイズ
cache 依存installとsource copyの順序
runtime build toolが最終imageに残っていないか
user rootで動いていないか
secret ENV やlayerにsecretがないか
package 不要なpackageを入れていないか
healthcheck 軽量で意味のある確認か
reproducibility tagやdigestの固定方針
context .dockerignore で不要ファイルを除外しているか
scan image scanとSBOMの確認があるか
command ENTRYPOINTCMD の意図が分かるか

本番運用では、image scan、SBOM、署名、registryの権限も関わります。Dockerfileは開発用の便利ファイルではなく、供給chainの入口でもあります。

Docker セキュリティベストプラクティス(OWASP版)

Open Web Application Security Project (OWASP) は、Docker コンテナのセキュリティに関する Cheat Sheet を公開。

イメージスキャンと脆弱性管理

Docker イメージビルド時の脆弱性スキャン

docker build --build-arg SCAN=true .
docker scout cves myapp:latest

Trivy(OSSの脆弱性スキャナ)による検査:

trivy image myapp:latest

ルートユーザーの回避

Dockerfile で非ルートユーザーを指定:

FROM ubuntu:22.04

# ユーザーを作成
RUN useradd -m -u 1000 appuser

# ファイルの所有権を変更
COPY --chown=appuser:appuser app/ /app/

# ユーザーを切り替え
USER appuser

CMD ["./app"]

マルチステージビルド による最小化

最終イメージサイズを削減し、攻撃面を減らす:

# Stage 1: Build
FROM golang:1.21 AS builder
WORKDIR /build
COPY . .
RUN CGO_ENABLED=0 go build -o app

# Stage 2: Runtime
FROM scratch
COPY --from=builder /build/app /
CMD ["/app"]

scratch ベースイメージを使用することで、OS全体のオーバーヘッドを排除。

機密情報の管理

ビルド時シークレット(BuildKit)

# --mount=type=secret を使用
RUN --mount=type=secret,id=docker_token \
    curl -H "Authorization: token $(cat /run/secrets/docker_token)" \
    https://api.github.com/user

ビルドコマンド:

docker buildx build --secret docker_token=~/.docker_token .

実行時環境変数

Kubernetes Secrets または Docker Secrets を利用。

イメージレイヤキャッシュの活用

頻繁に変わるコマンドを下に配置して、キャッシュヒット率を上げる:

FROM node:20-alpine

# 変わらない層
RUN apk add --no-cache python3 build-base

# 次に変わらない層
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# 最後に変わる層
COPY . .

Network Policies と Egress の制限

Kubernetes での Pod間通信を制限:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: app-policy
spec:
  podSelector:
    matchLabels:
      app: myapp
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: backend

Docker Compose と本番運用

開発環境と本番環境の分離

# docker-compose.yml (開発環境)
version: '3.8'
services:
  app:
    build: .
    ports:
      - "5000:5000"
    volumes:
      - .:/app
    environment:
      DEBUG: "true"

  db:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: devpass123

本番環境では:

# docker-compose.prod.yml
version: '3.8'
services:
  app:
    image: registry.example.com/myapp:1.2.3
    restart: always
    environment:
      DEBUG: "false"

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password

ヘルスチェックの実装

FROM python:3.11-alpine

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD python -c "import requests; requests.get('http://localhost:8000/health')"

ログ管理

コンテナログを直接ファイルに書かず、stdout/stderr に出力して、Docker ログドライバで処理:

CMD ["python", "-u", "app.py"]  # -u で出力をバッファしない

Docker Volumes と Persistence

Named Volumes(推奨)

docker volume create mydata
docker run -v mydata:/data myimage

Bind Mounts(開発用)

docker run -v /host/path:/container/path myimage

Volume ドライバ(NFS、S3等)

docker run --volume-driver nfs \
  --mount src=mydata,dst=/data \
  myimage

Base Image の選択ガイド

Base Image サイズ比較

Base Image サイズ 用途
ubuntu:24.04 ~70 MB 汎用、ツール豊富
debian:bookworm-slim ~30 MB 標準、バランス
alpine:latest ~7 MB 最小化、軽量
python:3.11 ~900 MB Python全セット
python:3.11-slim ~150 MB Python最小化
python:3.11-alpine ~50 MB Python+Alpine
node:22-alpine ~180 MB Node+Alpine
scratch 0 MB バイナリのみ(Go等)

Alpine vs Debian

Alpine の利点:

  • 非常に小さい
  • Package manager (apk) が高速
  • セキュリティ更新が迅速

Alpine の課題:

  • mulibc 使用(glibc との非互換)
  • ビルドツールが少ない
  • 一部のバイナリが動作しない

実装例:

# 開発環境:Debian slim(ツール豊富)
FROM python:3.11-bookworm

# 本番環境:Alpine(小さい)
FROM python:3.11-alpine

Digest による不変性の確保

Base image を pin することで、再現性を保証します。

# 危険:image タグは時間とともに変わる
FROM node:22-alpine

# 推奨:digest で固定(SHA256ハッシュ)
FROM node:22-alpine@sha256:abc123def456...

digest を取得するには:

docker inspect node:22-alpine --format='{{index .RepoDigests 0}}'
# Output: node@sha256:abc123def456...

digest を含めた Dockerfile:

# Node.js 22.0.0, Alpine 3.20 (2024-05-10時点)
FROM node:22.0.0-alpine@sha256:abc123def456...

Build Arguments と Build-time Variables

Dockerfile のビルド時に変数を渡す:

ARG NODE_VERSION=22
ARG REGISTRY=docker.io

FROM ${REGISTRY}/node:${NODE_VERSION}-alpine

ARG BUILD_DATE
ARG VCS_REF
ARG VERSION=1.0.0

LABEL \
  org.opencontainers.image.created="${BUILD_DATE}" \
  org.opencontainers.image.revision="${VCS_REF}" \
  org.opencontainers.image.version="${VERSION}"

ビルド時:

docker build \
  --build-arg NODE_VERSION=22 \
  --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
  --build-arg VCS_REF=$(git rev-parse --short HEAD) \
  --build-arg VERSION=1.2.3 \
  -t myapp:1.2.3 \
  .

Image Scanning と Vulnerability Detection

Docker Scout

# Local image をスキャン
docker scout cves myapp:latest

# Output example:
# 3 packages with known vulnerabilities
# │ CVE-2024-1234   │ HIGH   │ openssl
# │ CVE-2024-5678   │ MEDIUM │ curl

Trivy(オープンソース)

trivy image myapp:latest

# 詳細レポート出力
trivy image --format json --output report.json myapp:latest

Software Bill of Materials (SBOM)

SBOM は image に含まれるすべてのパッケージ・依存関係の一覧です:

# SBOM を生成
docker build --sbom=true .

# または
syft myapp:latest > sbom.json

SBOM の構造(CycloneDX 形式):

{
  "bom-format": "CycloneDX",
  "specVersion": "1.5",
  "version": 1,
  "components": [
    {
      "type": "library",
      "name": "openssl",
      "version": "3.1.2",
      "purl": "pkg:deb/debian/openssl@3.1.2"
    }
  ]
}

Runtime の最適化

ENTRYPOINT vs CMD

# CMD のみ
FROM alpine
CMD ["echo", "hello"]
# docker run myimage          → "hello" 出力
# docker run myimage echo foo → "foo" 出力

# ENTRYPOINT + CMD(推奨)
FROM alpine
ENTRYPOINT ["sh", "-c"]
CMD ["echo hello"]
# docker run myimage              → "hello" 出力
# docker run myimage "echo foo"   → "foo" 出力

# ENTRYPOINT のスクリプト化
FROM node:22-alpine
COPY entrypoint.sh /
ENTRYPOINT ["/entrypoint.sh"]
CMD ["node", "server.js"]

entrypoint.sh の例:

#!/bin/sh
set -e

# ヘルスチェック
if [ -n "$HEALTH_CHECK" ]; then
  echo "Running health check..."
  curl -f http://localhost:8000/health || exit 1
fi

# セットアップ
if [ "$RUN_MIGRATIONS" = "true" ]; then
  npm run migrate
fi

# メインプロセス実行
exec "$@"

Layer の最適化テクニック

RUN コマンドの連鎖

# 非効率:各行が個別 layer
RUN apt-get update
RUN apt-get install -y curl git vim
RUN apt-get clean

# 効率的:単一 layer
RUN apt-get update && \
    apt-get install -y curl git vim && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

Intermediate images の削除

# 不要な images を削除
docker image prune

# dangling images のみ削除
docker image prune -f

Security Best Practices

Secrets の分離

# 危険:secret を COPY や RUN で記録
COPY secret.txt .
RUN echo $SECRET > config.txt

# 推奨:BuildKit secrets を使用
# Dockerfile
RUN --mount=type=secret,id=api_key \
    API_KEY=$(cat /run/secrets/api_key) \
    npm run build

# ビルド実行
docker build \
  --secret api_key=/path/to/secret \
  .

Network Isolation

# Network access を必要な場合のみ許可
FROM alpine:latest
RUN apk add --no-cache curl
RUN curl https://example.com/file.tar.gz | tar xz

User & Permissions

FROM python:3.11-alpine

# ユーザ作成(非root)
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# ファイルのオーナーシップ
COPY --chown=appuser:appgroup . /app

WORKDIR /app
USER appuser

CMD ["python", "app.py"]

Docker Compose とマルチコンテナ管理

Development Compose

version: '3.8'

services:
  app:
    build: .
    ports:
      - "8000:8000"
    environment:
      DATABASE_URL: postgresql://postgres@db/myapp
      DEBUG: "true"
    depends_on:
      - db
    volumes:
      - .:/app

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_PASSWORD: password
      POSTGRES_DB: myapp
    ports:
      - "5432:5432"

Production Compose

version: '3.8'

services:
  app:
    image: registry.example.com/myapp:1.2.3@sha256:abc...
    restart: always
    environment:
      DATABASE_URL: postgresql://user@db.internal/myapp
      DEBUG: "false"
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 3s
      retries: 3

  db:
    image: postgres:15-alpine@sha256:xyz...
    restart: always
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "postgres"]
      interval: 10s
      timeout: 3s
      retries: 3

secrets:
  db_password:
    file: /run/secrets/db_password.txt

よくあるエラーと対策

エラー 原因 対策
no space left on device Image が大きすぎる Multi-stage build で runtime を削減
permission denied Root 以外で実行不可 USER で非root ユーザに切り替え
entrypoint is not a shell ENTRYPOINT で sh/bash が必須 ENTRYPOINT を shell 形式で記述
dependency not found Package manager のキャッシュ古い --no-cache で cache を無視

Dockerfile Linting

hadolint

hadolint Dockerfile

# Output:
# DL3007 warning: Using latest is prone to errors
# DL3009 warning: Delete the apt-get lists after installing

まとめ

Dockerfileは、動くimageを作るだけのファイルではありません。layerとcacheを意識し、multi-stage buildでruntimeを小さくし、非root実行とsecret分離を徹底すると、運用しやすいimageになります。さらに、build context、base image、digest、scan、SBOMまで含めて考えると、Dockerfileはsupply chainの入口としてレビューできるようになります。

参考文献

公式・標準

解説・補助