「ローカルでは動いたのに」が消えた日——BuildKit 2026年版でチーム開発が変わった話

新メンバーのオンボーディングで2時間溶かした経験から整理したDockerのリアルな知見。キャッシュ戦略・本番同等環境の作り方など、実際に効果があったものだけ紹介します。

ローカル環境と本番が微妙に違う問題、もう嫌になりませんか?

「ローカルでは動いたのに本番でコケる」——これ、本当に何度経験しても嫌になりますよね。先月も、うちのチームで新しいメンバーがオンボーディング中に「自分のMacだけなぜかビルドが死ぬ」という状況になって2時間溶かしました。原因はDockerfileの RUN --mount=type=cache の書き方がBuildKitのバージョンによって微妙に挙動が違う、というやつでした。

2026年時点でDockerのエコシステムはだいぶ成熟してきて、ビルド速度も開発体験も2〜3年前と比べると正直別物になっています。ただ、その進化についていけていないまま古い書き方を惰性で使っているプロジェクトも多い。今回は、最近うちのチームで整理し直したDockerのベストプラクティスを、実際に効果があったものに絞って書いていきます。

セキュリティ周りの話(SBOM、eBPFによるランタイム保護など)はコンテナセキュリティ完全ガイド2026で詳しく書いたので、今回はビルド効率化と開発環境の本番同等性にフォーカスします。


BuildKit 2026年の実力、マジで別次元になっていた

Docker 27.x(2026年4月時点のlatestはv27.3)ではBuildKitがデフォルト有効どころか、内部エンジンが刷新されています。特に効いているのが以下の3点です。

キャッシュマウントの挙動が安定した

以前は --mount=type=cache がCI環境で微妙に効いたり効かなかったりして「本当に使えるのか?」という感じでしたが、2026年現在は各主要CIサービス(GitHub Actions、GitLab CI)とのキャッシュバックエンド統合がちゃんと整備されています。

実際にGoのプロジェクトで試した結果:

# syntax=docker/dockerfile:1.8
FROM golang:1.25-alpine AS builder

WORKDIR /app

# go.mod/go.sumのキャッシュを分離することで依存取得をスキップ
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
    go mod download

COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    CGO_ENABLED=0 go build -o /app/server ./cmd/server

# 本番イメージ:ディストロレスで最小構成
FROM gcr.io/distroless/static:nonroot
COPY --from=builder /app/server /server
USER nonroot:nonroot
ENTRYPOINT ["/server"]

これで何が変わったか、数字で見てみましょう:

xychart-beta
  title "Goプロジェクトビルド時間比較(秒)"
  x-axis ["初回", "依存変更なし", "ソースのみ変更"]
  y-axis "ビルド時間(秒)" 0 --> 300
  bar [240, 180, 210]
  bar [240, 18, 32]

左のバー(青)が旧来の書き方、右のバー(オレンジ)がキャッシュマウント活用後です。依存変更なしのケースで180秒 → 18秒。地味に毎日のCIサイクルにじわじわ効いてくる数字で、積み上げると月単位でけっこうな時間になります。

--secret でビルド時の認証情報を安全に渡す

これ、2024年頃から使えるようになっていたんですが、ちゃんと活用しているプロジェクトがまだ少ない気がします。.env をADD/COPYしてビルドすると認証情報がイメージレイヤーに残るのは有名な問題ですが、2026年はちゃんと --secret を使いましょう。

# ビルド実行時
docker build \
  --secret id=npm_token,env=NPM_TOKEN \
  --secret id=github_token,env=GITHUB_TOKEN \
  -t myapp:latest .
# syntax=docker/dockerfile:1.8
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./

# --secretでマウント。ビルド後にはレイヤーに残らない
RUN --mount=type=secret,id=npm_token \
    NPM_TOKEN=$(cat /run/secrets/npm_token) \
    npm ci --ignore-scripts

これをやっていない状態でイメージを docker history してみると、環境変数がそのまま見えることがある。正直、初めて見たとき結構ゾッとしました。まだ直していないDockerfileがあるなら、今日直してほしいです。


Docker Compose V2、2026年の実践構成

watch モードが本番と乖離しない開発の鍵になった

Docker Compose V2の watch 機能(docker compose watch)、最初は「bind mountでよくない?」と思ってあまり使っていなかったんですが、ちゃんと向き合ったら意外と良かった。

bind mountの問題って、ホストのファイルシステムをそのままコンテナに突っ込むので「本番イメージでは動かない依存関係がローカルに入っていて気づかない」という事故が起きやすいんですよね。watch モードはファイル変更を検知して特定のアクション(syncrebuild)を実行するので、本番のイメージビルドフローを維持しながらホットリロードができます。

# compose.yaml(V2ではdocker-compose.ymlよりcompose.yamlが推奨)
services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
      target: development  # マルチステージの開発用ターゲット
      cache_from:
        - type=gha  # GitHub Actionsのキャッシュを再利用
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
          ignore:
            - node_modules/
        - action: rebuild
          path: package.json
        - action: rebuild
          path: go.mod
    environment:
      - ENV=development
    ports:
      - "8080:8080"
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started

  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_DB: myapp_dev
      POSTGRES_USER: dev
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U dev -d myapp_dev"]
      interval: 5s
      timeout: 5s
      retries: 5
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:7.4-alpine
    command: redis-server --save 60 1 --loglevel warning

secrets:
  db_password:
    environment: DB_PASSWORD

volumes:
  postgres_data:
# 起動
docker compose up --watch

# ファイル変更を検知して自動でsync/rebuildが走る

この構成に切り替えてから「開発環境のみに存在するパッケージがあって本番でクラッシュした」問題はほぼなくなりました。bind mountからの移行は若干面倒ですが、やる価値は確実にあります。

マルチステージビルドで環境ごとの分岐を整理する

開発・テスト・本番でDockerfileを別々に持つのはもう古い。2026年はマルチステージで全部一本化するのが定番です。個人的には「Dockerfile.dev があるリポジトリを見たらそっと声をかけてあげてほしい」くらいには思っています。

# syntax=docker/dockerfile:1.8

# ──── 共通ベース ────
FROM node:22-alpine AS base
WORKDIR /app
COPY package.json package-lock.json ./

# ──── 依存インストール ────
FROM base AS deps
RUN --mount=type=cache,target=/root/.npm \
    npm ci --ignore-scripts

# ──── 開発用 ────
FROM deps AS development
ENV NODE_ENV=development
CMD ["npm", "run", "dev"]

# ──── テスト用 ────
FROM deps AS test
COPY . .
RUN --mount=type=cache,target=/root/.npm \
    npm test

# ──── ビルド ────
FROM deps AS build
COPY . .
RUN npm run build

# ──── 本番 ────
FROM node:22-alpine AS production
WORKDIR /app
ENV NODE_ENV=production

# 本番依存のみ
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci --omit=dev --ignore-scripts

COPY --from=build /app/dist ./dist

USER node
CMD ["node", "dist/index.js"]

Dockerfileのステージと各環境のマッピングを整理するとこんな感じです:

ステージ用途イメージサイズ目安
developmentローカル開発(watch使用)〜500MB
testCI上のユニット・統合テスト〜500MB
buildアーティファクト生成〜500MB
production本番デプロイ〜120MB

本番イメージが120MB台に収まると、コールドスタートの体感速度がかなり変わります。ECSやKubernetesでのデプロイ時間でも効いてきますね。


開発チームでのCI/CD統合、こう設計した

アーキテクチャ全体のフローはこんな感じです:

flowchart TB
    subgraph Developer["開発者ローカル"]
        DC["docker compose watch"]
        DL["docker buildx bake"]
    end

    subgraph CI["GitHub Actions"]
        BUILD["Build\n(BuildKit + GHAキャッシュ)"]
        TEST["Test Stage\n(--target test)"]
        SCAN["Image Scan\n(Trivy + SBOM生成)"]
        PUSH["ECR Push"]
    end

    subgraph Registry["レジストリ"]
        ECR["Amazon ECR"]
        CACHE["Cache Layer\n(type=gha)"]
    end

    subgraph Deploy["デプロイ"]
        ECS["ECS Fargate"]
        K8S["EKS"]
    end

    DC --> DL
    DL --> BUILD
    BUILD --> CACHE
    CACHE --> BUILD
    BUILD --> TEST
    TEST --> SCAN
    SCAN --> PUSH
    PUSH --> ECR
    ECR --> ECS
    ECR --> K8S

GitHub Actions側の設定も載せておきます。ポイントはキャッシュバックエンドとbuildx bakeの組み合わせです:

# .github/workflows/build.yml
name: Build & Push

on:
  push:
    branches: [main, develop]

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      id-token: write  # OIDC

    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
        with:
          version: latest
          driver-opts: image=moby/buildkit:v0.17.0  # 2026年4月時点のstable

      - name: Configure AWS Credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: ap-northeast-1

      - name: Login to ECR
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build and push (test stage)
        uses: docker/build-push-action@v6
        with:
          context: .
          target: test
          cache-from: type=gha
          cache-to: type=gha,mode=max
          push: false

      - name: Build and push (production)
        uses: docker/build-push-action@v6
        with:
          context: .
          target: production
          tags: ${{ secrets.ECR_REGISTRY }}/myapp:${{ github.sha }}
          cache-from: type=gha
          push: ${{ github.ref == 'refs/heads/main' }}
          sbom: true  # SBOM自動生成(BuildKit v0.15+)
          provenance: true

sbom: true は最近デフォルトにしています。SBOM生成がビルドフローにここまでスムーズに統合されているのは2〜3年前には想像できなかった。インシデント対応のベストプラクティス2026でも触れていますが、サプライチェーン攻撃の文脈でSBOMは「あると便利」から「ないとまずい」くらいの位置づけになりつつあります。


実際に困った問題と、2026年時点での回答

M1/M2/M3 MacとLinux本番環境のアーキテクチャ差異

正直、これは2026年になってもたまにハマります。特にネイティブライブラリを含むイメージ(OpenSSL依存のPythonパッケージなど)で linux/amd64linux/arm64 の挙動差が出ることがある。

チームでの対策はシンプルで、ビルドを必ず --platform linux/amd64 で明示するか、CI上でマルチプラットフォームビルドを通すかのどちらかにしました:

# ローカルでamd64向けにビルドして検証
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --tag myapp:multiarch \
  --push .
# compose.yamlでもplatformを明示
services:
  api:
    build:
      context: .
      platforms:
        - linux/amd64
        - linux/arm64

マルチプラットフォームビルドのビルド時間は単独プラットフォームの約1.6〜2倍になります。それでも「M3 MacでOKだったのにECSでコケる」という無駄なデバッグ時間よりずっとましなので、割り切って入れるのが正解だと思っています。

イメージが肥大化する問題

「気づいたら本番イメージが2GBになってた」という話、あるある過ぎます。これはCIにチェックを仕込むのが一番確実です:

# compose.yamlに診断ツールを追加
services:
  dive:
    image: wagoodman/dive:latest
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    command: ["myapp:latest"]
    environment:
      DIVE_CI: "true"  # 効率しきい値以下だとexit 1
# CIで使う場合
docker run --rm \
  -e DIVE_CI=true \
  -v /var/run/docker.sock:/var/run/docker.sock \
  wagoodman/dive:latest \
  myapp:${{ github.sha }}

DIVE_CI=true にするとイメージ効率が設定値(デフォルト0.9)を下回るとビルド失敗にできます。地味に便利で、一度入れたらもう外せなくなりました。「なぜか太った」を未然に防げるのが個人的には一番の価値だと思っています。

DockerfileのLintを自動化する

Terraformで3年分の失敗から学んだ話と同じように、インフラ定義のLintは早めに自動化しておくと後が楽です。Hadolintをpre-commitとCIの両方で動かしています:

# .hadolint.yaml
failure-threshold: warning
ignore:
  - DL3008  # apt-get install --no-install-recommends(プロジェクト方針で許容)
  - DL3009  # Delete apt-get lists(キャッシュマウント使用時は不要)
trusted-registries:
  - docker.io
  - gcr.io
  - public.ecr.aws
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/hadolint/hadolint
    rev: v2.13.0
    hooks:
      - id: hadolint-docker
        args: [--config, .hadolint.yaml]

CI側にも入れてPRで検知できるようにすると、レビューで「FROM ubuntu使うなよ」と言わずに済む。これも地味に便利で、レビュアーの精神衛生にやさしいです。


まとめ

2026年時点のDocker活用、チームで整理してみてわかったことをまとめます。

  • BuildKitのキャッシュマウントは今すぐ使うべき。依存変更なしのビルドが10分の1になることも普通にある。syntax=docker/dockerfile:1.8 を先頭に書くだけでBuildKitの最新機能が使えます
  • --secret を使っていないDockerfileは今日直してほしいdocker history でパスワードが見える状態のままにしておくのはもう言い訳できない
  • Compose V2の watch モードで「ローカルのみ動く問題」が激減した。bind mountからの移行は若干面倒だけど、やる価値は確実にある
  • マルチステージビルドで環境別Dockerfileはやめる。一本化すると管理が楽になるし、本番イメージのサイズも自然に減っていく
  • SBOMとDiveのCI統合は最低限やっておく。セキュリティ要件が厳しくなっている2026年では「入れていない」がそもそもリスクになってきている

次にやるべきアクションとしては、まず既存のDockerfileに syntax=docker/dockerfile:1.8 を追加してキャッシュマウントを導入するのが一番効果が出やすいです。ビルド時間が半分以下になったら、その次にDive導入でイメージサイズを見直す、という順番で進めると成果が見えやすくて良いと思います。

皆さんのチームではDockerfileどう管理してますか?「もっとこっちの方が良かった」みたいな知見があればぜひ聞かせてください。正直まだ全部が完璧に回っているわけではなくて、特にマルチプラットフォームビルドの時間最適化はまだ改善の余地があると感じています。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事