Docker-in-Docker(DinD)とDocker-out-of-Docker(DooD):CI/CDランナーの実践設定

Docker tutorial - IT technology blog
Docker tutorial - IT technology blog

CI/CDパイプラインを構築していてコンテナランナー内でDockerイメージをビルドする必要がある場合、必ずぶつかる疑問があります:DinDを使うべきか、DooDを使うべきか?私自身も違いを理解するまでかなりの時間がかかりました。この記事では、同じ苦労をしなくていいよう、要点をまとめています。

5分でできる — GitLab RunnerでDooD を設定する

すぐに動かしたい方のために、GitLab Runner向けのDooD(Docker-out-of-Docker)設定方法を紹介します。これは私が本番環境で使っていて、最も安定している方法です。

ステップ1:Docker executorでGitLab Runnerをインストール

# GitLab Runner をインストール
curl -L --output /usr/local/bin/gitlab-runner \
  https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64
chmod +x /usr/local/bin/gitlab-runner

# ランナーを登録 — executor: docker を選択
gitlab-runner register \
  --url https://gitlab.com \
  --registration-token YOUR_TOKEN \
  --executor docker \
  --docker-image docker:24 \
  --docker-volumes /var/run/docker.sock:/var/run/docker.sock

ステップ2:イメージビルド用の.gitlab-ci.ymlファイル

build-image:
  image: docker:24
  services: []   # DooD使用時はdindサービス不要
  variables:
    DOCKER_HOST: unix:///var/run/docker.sock
  script:
    - docker build -t myapp:$CI_COMMIT_SHORT_SHA .
    - docker push myapp:$CI_COMMIT_SHORT_SHA

これで完了です。ランナーはホストのDockerデーモンと直接通信してDockerイメージをビルドできるようになります。思ったよりシンプルです。

DinDとDooD — 実際どこが違うのか?

名前は似ていますが、動作の仕組みはまったく異なります。正しく理解することで適切な選択ができ、パイプラインが突然失敗したときもどこをデバッグすればいいかわかります。

Docker-in-Docker (DinD)

DinDとは、コンテナの内部でDockerデーモンを動かすことを意味します。コンテナランナーは独自のデーモンを起動し、ホストから完全に独立して動作します。

# .gitlab-ci.yml でDinDを使用
build-dind:
  image: docker:24
  services:
    - docker:24-dind          # サービスコンテナ内で動作する独立したデーモン
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
    DOCKER_CERT_PATH: "/certs/client"
    DOCKER_TLS_VERIFY: 1
    DOCKER_HOST: tcp://docker:2376
  script:
    - docker build -t myapp:latest .
    - docker push myapp:latest

DinDはコンテナを--privilegedで実行する必要があります。このフラグにより、コンテナはカーネルサブシステム(namespaces、cgroups、overlay filesystem)にアクセスでき、内部で別のDockerデーモンを起動することが可能になります。

Docker-out-of-Docker (DooD)

DooD は新しいDockerデーモンを作成しません。代わりに、コンテナランナーがホストのDockerデーモンソケット/var/run/docker.sock)をコンテナ内にマウントします。コンテナ内のdockerコマンドは、実際にはホストのデーモンと通信しています。

# DooD でコンテナを実行 — ホストソケットをマウント
docker run -v /var/run/docker.sock:/var/run/docker.sock \
  -it docker:24 sh

# このコンテナ内で、以下のコマンドはホスト上でコンテナを実行する
docker run hello-world

簡単な比較

  • DinD:独立したデーモン、より高い隔離性、--privilegedが必要、遅い(毎回イメージを再pullする必要がある)
  • DooD:ホストのデーモンを使用、速い(イメージキャッシュを共有)、シンプル、ただしコンテナがDocker上でホストと同等の権限を持つ

詳細解説 — DinDはなぜ–privilegedが必要なのか?

Dockerデーモンはnamespaces、cgroups、overlay filesystemなどのカーネルサブシステムへのアクセスが必要です。コンテナ内で実行する場合、これらの権限はデフォルトで制限されています。--privilegedフラグはその制限のほとんどを解除します。

これがDinDの最大の弱点でもあります。--privilegedで実行するコンテナは、悪用されるとホストへの脱出(container escape)が可能になります。公開済みのコンテナエスケープCVEの中には、このフラグが関係するものが少なくありません。そのため、多くの企業のセキュリティチームは本番クラスターで--privilegedを完全にブロックしています。

TLSを使ったより安全なDinD設定

# セルフホストDinDランナー用docker-compose.yml
version: '3.8'
services:
  dind:
    image: docker:24-dind
    privileged: true
    environment:
      DOCKER_TLS_CERTDIR: /certs
    volumes:
      - dind-certs-ca:/certs/ca
      - dind-certs-client:/certs/client
      - dind-storage:/var/lib/docker
    networks:
      - ci-network

  gitlab-runner:
    image: gitlab/gitlab-runner:latest
    volumes:
      - ./config:/etc/gitlab-runner
      - dind-certs-client:/certs/client:ro
    environment:
      DOCKER_HOST: tcp://dind:2376
      DOCKER_TLS_VERIFY: 1
      DOCKER_CERT_PATH: /certs/client
    networks:
      - ci-network
    depends_on:
      - dind

volumes:
  dind-certs-ca:
  dind-certs-client:
  dind-storage:

networks:
  ci-network:

この設定では、DinDデーモンはTLS経由で通信します。ポート2375(非暗号化)は開放しません。

応用編 — Kaniko:Dockerデーモン不要でイメージをビルド

KanikoはKubernetesクラスターで--privilegedのセキュリティレビューが承認されなかったときに見つけた解決策です。議論するより切り替えることにしたら、問題がすぐに解決しました。KanikoはDockerfileからDockerイメージをデーモンなしでビルドし、完全にユーザースペースで動作します。特別な権限は一切不要です。

# Kaniko使用時の.gitlab-ci.yml
build-kaniko:
  image:
    name: gcr.io/kaniko-project/executor:v1.21.0-debug
    entrypoint: [""]
  script:
    - /kaniko/executor
      --context $CI_PROJECT_DIR
      --dockerfile $CI_PROJECT_DIR/Dockerfile
      --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
      --cache=true
      --cache-repo $CI_REGISTRY_IMAGE/cache

Kanikoのビルド時間はDooD比で約20〜30%遅くなります。デーモンによるレイヤーキャッシュがないためです。セキュリティが速度より優先される場合、このトレードオフは十分受け入れられます。

GitHub Actions — DooD 組み込み済み

GitHubホスト型ランナーにはDockerが最初からインストールされているので、そのまま使えます:

# .github/workflows/build.yml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build Docker image
        run: docker build -t myapp:${{ github.sha }} .

      - name: Login to registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Push image
        run: docker push ghcr.io/${{ github.repository }}:${{ github.sha }}

セルフホスト型GitHub ActionsランナーはGitLab DooD と同様の設定です。ソケットをランナーコンテナにマウントするだけで完了します。

本番環境から学んだ実践的なTips

DooD でのレイヤーキャッシュを効果的に使う

DooD の最大のメリットはホストとイメージキャッシュを共有できることです。ただし、パイプラインを数週間継続的に実行すると、このキャッシュが50〜80GBに達することがあります。実際にランナーのVPSがディスク不足になった経験があります。定期的なクリーンアップのcronjobを追加しましょう:

# Dockerキャッシュを毎週クリーンアップ — crontabに追加
0 3 * * 0 docker system prune -f --filter "until=168h"

ソケットの権限を正しく設定する

# dockerソケットのグループを確認
ls -la /var/run/docker.sock
# srw-rw---- 1 root docker ...

# ランナーユーザーはdockerグループに属する必要がある
usermod -aG docker gitlab-runner

# またはランナーイメージのDockerfileで修正
RUN groupmod -g 999 docker && usermod -aG docker runner-user

実際の結果

30台以上のコンテナを稼働する本番クラスターでDinDからDooD に移行したところ、リソース使用量が約40%削減されました。主な理由は、各ランナーコンテナでデーモンを起動する必要がなくなったことです。さらに、ジョブ間でイメージキャッシュを共有できるため、DinDのように毎回pullし直す手間もなくなります。

何を選ぶべき場面は?

  • DooD:高パフォーマンスが必要なVM/VPS上のセルフホストランナー、ホストを管理できる小規模チーム向け
  • DinD:パイプライン間の隔離が必要な場合、共有環境、または複雑なDockerネットワークのテスト向け
  • Kaniko:Kubernetesクラスター、セキュリティを重視する環境、--privilegedを使いたくない場合向け

私は専用VPS上のGitLabランナーにはDooD を使っています。速くてシンプルで、デーモンオーバーヘッドを気にする必要がありません。他のチームと共有するKubernetesクラスターではKanikoの方が安全です。結局のところ、「最良のツール」を選ぶのではなく、各環境の信頼レベルと隔離要件に合ったツールを選ぶことが大切です。

Share: