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.
