Docker-in-Docker (DinD) và Docker-out-of-Docker (DooD): Cấu hình CI/CD Runner thực chiến

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

Nếu bạn đang setup CI/CD pipeline mà cần build Docker image bên trong một container runner, chắc chắn bạn sẽ phải đối mặt với câu hỏi: dùng DinD hay DooD? Mình đã mất khá nhiều thời gian tìm hiểu trước khi hiểu rõ sự khác biệt — bài này tổng hợp lại để bạn không phải mò mẫm như mình.

Làm ngay trong 5 phút — DooD trên GitLab Runner

Nếu bạn cần kết quả nhanh, đây là cách setup DooD (Docker-out-of-Docker) cho GitLab Runner — cách này mình đang dùng trên production và ổn định nhất:

Bước 1: Cài GitLab Runner với Docker executor

# Cài 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

# Register runner — chọn 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

Bước 2: File .gitlab-ci.yml để build image

build-image:
  image: docker:24
  services: []   # KHÔNG cần dind service khi dùng DooD
  variables:
    DOCKER_HOST: unix:///var/run/docker.sock
  script:
    - docker build -t myapp:$CI_COMMIT_SHORT_SHA .
    - docker push myapp:$CI_COMMIT_SHORT_SHA

Vậy là xong. Runner giờ có thể build Docker image bằng cách giao tiếp trực tiếp với Docker daemon của host. Đơn giản hơn bạn nghĩ.

DinD và DooD — Thực ra khác nhau chỗ nào?

Hai cái tên trông giống nhau nhưng cơ chế hoạt động hoàn toàn khác. Hiểu đúng thì chọn đúng — và khi pipeline bỗng dưng lỗi, bạn cũng biết debug ở đâu.

Docker-in-Docker (DinD)

DinD nghĩa là chạy một Docker daemon bên trong container. Container runner sẽ khởi động daemon riêng của nó, hoàn toàn độc lập với host.

# .gitlab-ci.yml dùng DinD
build-dind:
  image: docker:24
  services:
    - docker:24-dind          # Daemon riêng biệt chạy trong service container
  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 cần chạy container với quyền --privileged. Flag này cho phép container truy cập các kernel subsystem — namespaces, cgroups, overlay filesystem — để spin up một Docker daemon khác bên trong.

Docker-out-of-Docker (DooD)

DooD không tạo daemon mới. Thay vào đó, container runner mount socket của Docker daemon trên host (/var/run/docker.sock) vào trong container. Mọi lệnh docker bên trong container thực ra đang giao tiếp với daemon ngoài host.

# Chạy container với DooD — mount socket host
docker run -v /var/run/docker.sock:/var/run/docker.sock \
  -it docker:24 sh

# Bên trong container này, lệnh sau chạy container TRÊN HOST
docker run hello-world

So sánh nhanh

  • DinD: Daemon riêng biệt, isolation tốt hơn, cần --privileged, chậm hơn (phải pull image lại từ đầu mỗi lần)
  • DooD: Dùng daemon host, nhanh hơn (cache image chung), đơn giản hơn, nhưng container có quyền ngang bằng host trên Docker

Giải thích chi tiết — Tại sao DinD cần –privileged?

Docker daemon cần truy cập các kernel subsystems như namespaces, cgroups, và overlay filesystem. Khi chạy trong container, những quyền này bị giới hạn theo mặc định. Flag --privileged gỡ bỏ hầu hết giới hạn đó.

Đây cũng là điểm yếu lớn nhất của DinD. Container chạy với --privileged có thể escape ra ngoài host nếu bị exploit — trong nhiều CVE container escape đã được công bố, không ít cái dính tới flag này. Chính vì vậy team security ở nhiều công ty block hẳn --privileged trên production cluster.

Cấu hình DinD an toàn hơn với TLS

# docker-compose.yml cho DinD runner tự host
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:

Với cấu hình này, DinD daemon giao tiếp qua TLS — không để port 2375 (unencrypted) mở.

Nâng cao — Kaniko: Build image không cần Docker daemon

Kaniko là giải pháp mình tìm ra sau khi security review từ chối approve --privileged trên Kubernetes cluster. Thay vì tranh luận, chuyển sang Kaniko — và nó giải quyết vấn đề ngay. Kaniko build Docker image từ Dockerfile mà không cần daemon, chạy hoàn toàn trong user space, không cần quyền đặc biệt gì cả.

# .gitlab-ci.yml với Kaniko
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

Build time của Kaniko chậm hơn DooD khoảng 20-30% vì không có layer cache daemon sẵn. Đánh đổi đó thường chấp nhận được khi security là ưu tiên cao hơn tốc độ.

GitHub Actions — DooD built-in

GitHub-hosted runners đã cài sẵn Docker, bạn chỉ cần dùng:

# .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 }}

Self-hosted GitHub Actions runner thì setup tương tự GitLab DooD — mount socket vào runner container là xong.

Tips thực tế từ kinh nghiệm production

Cache layer hiệu quả với DooD

Ưu điểm lớn nhất của DooD là share image cache với host. Nhưng sau vài tuần chạy pipeline liên tục, cache này có thể lên tới 50-80 GB — mình từng thấy runner VPS hết disk vì cái này. Thêm cronjob cleanup định kỳ:

# Cleanup Docker cache mỗi tuần — thêm vào crontab
0 3 * * 0 docker system prune -f --filter "until=168h"

Phân quyền socket đúng cách

# Kiểm tra group của docker socket
ls -la /var/run/docker.sock
# srw-rw---- 1 root docker ...

# Runner user cần thuộc group docker
usermod -aG docker gitlab-runner

# Hoặc fix trong Dockerfile của runner image
RUN groupmod -g 999 docker && usermod -aG docker runner-user

Kết quả thực tế

Trên production cluster chạy 30+ container, chuyển từ DinD sang DooD giảm được ~40% resource usage. Lý do chính: không cần spin up thêm daemon trong mỗi runner container. Thêm nữa, image cache dùng chung giữa các job — thay vì mỗi job pull lại từ đầu như khi dùng DinD.

Khi nào chọn gì?

  • DooD: Self-hosted runner trên VM/VPS cần hiệu năng cao, team nhỏ kiểm soát được host
  • DinD: Cần isolation giữa các pipeline, môi trường shared, hoặc test Docker networking phức tạp
  • Kaniko: Kubernetes cluster, security-sensitive, không muốn dùng --privileged

Mình dùng DooD cho GitLab runner trên VPS riêng — nhanh, đơn giản, không phải deal với daemon overhead. Còn cluster Kubernetes shared với team khác thì Kaniko cho an toàn hơn. Rốt cuộc không phải chọn tool nào “tốt nhất” mà là chọn tool phù hợp với mức độ trust và yêu cầu isolation của từng môi trường.

Share: