- Published on
GitLab CI Docker-in-Docker TLS 오류 해결 9가지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론
GitLab CI에서 Docker-in-Docker(DinD)를 쓰다 보면 어느 날 갑자기 Cannot connect to the Docker daemon at tcp://docker:2376 같은 메시지와 함께 파이프라인이 멈추는 경우가 있습니다. 특히 GitLab Runner가 제공하는 docker:dind 서비스는 기본적으로 TLS(2376)를 켜는 방향으로 동작해, 클라이언트(DOCKER_HOST/DOCKER_TLS_CERTDIR)·네트워크(서비스 DNS)·권한(privileged)·인증서 경로 중 하나만 어긋나도 TLS 오류로 표면화됩니다.
이 글에서는 “DinD TLS 오류”를 단순히 DOCKER_TLS_CERTDIR=""로 끄는 요령에 그치지 않고, 원인별로 정확히 분리해 9가지 해결책을 정리합니다. GitLab CI는 작은 설정 하나가 보안/성능/재현성에 직결되므로, 각 해결책의 트레이드오프도 함께 다룹니다.
> 참고로 Git 작업 흐름에서 권한/보호 규칙 때문에 CI가 막히는 케이스도 흔합니다. 필요하면 Git rebase 후 강제푸시 막힘 - 보호규칙 해제법도 같이 확인해 두면 좋습니다.
0) 먼저 확인할 대표 증상(로그 패턴)
TLS 관련 DinD 실패는 대체로 아래 패턴으로 나타납니다.
Cannot connect to the Docker daemon at tcp://docker:2376. Is the docker daemon running?error during connect: Get "https://docker:2376/_ping": x509: certificate signed by unknown authorityremote error: tls: bad certificateclient is newer than server(버전/기능 불일치가 TLS 오류처럼 보일 때도 있음)lookup docker on xxx: no such host(서비스 DNS 문제지만 결과적으로 TLS 핑 실패)
문제 분리를 위해, job 시작 직후 아래 디버그를 한 번 넣어두면 원인 파악이 빨라집니다.
set -euxo pipefail
env | sort | grep -E 'DOCKER|CI_|GITLAB|TLS' || true
# docker CLI가 어떤 엔드포인트를 보고 있는지
docker version || true
# 네트워크/DNS 확인
getent hosts docker || true
nc -vz docker 2376 || true
nc -vz docker 2375 || true
1) (가장 흔함) TLS를 끄고 2375로 붙이기
docker:dind는 기본적으로 TLS를 활성화하며(최근 이미지 기준), 이때 클라이언트는 인증서를 맞춰 가져와야 합니다. 인증서 공유/마운트가 어긋나면 TLS 오류가 납니다. 가장 단순한 해결은 TLS를 끄고 2375로 붙는 것입니다.
적용 예시(.gitlab-ci.yml)
image: docker:27
services:
- name: docker:27-dind
command: ["--host=tcp://0.0.0.0:2375", "--tls=false"]
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
build:
stage: build
script:
- docker info
- docker build -t myapp:ci .
주의점
- 보안: 동일 네트워크 내에서 2375는 평문입니다. 공유 러너/멀티테넌트 환경이면 권장하지 않습니다.
- 권장 사용처: 사설 러너(전용 VM) + 네트워크 격리가 명확한 경우.
2) TLS를 유지하되, DOCKER_TLS_CERTDIR/인증서 경로를 정확히 맞추기
TLS를 유지하려면 다음이 일관돼야 합니다.
- DinD 데몬이 인증서를 생성하는 경로:
DOCKER_TLS_CERTDIR(기본/certs) - 클라이언트가 인증서를 찾는 경로: 보통
/certs/client - job 컨테이너와 서비스 컨테이너 간에 해당 경로가 공유되어야 함
GitLab CI에서는 서비스(dind)가 생성한 인증서를 job 컨테이너가 볼 수 있어야 합니다. (환경/러너 설정에 따라 공유가 자연스럽게 되기도, 안 되기도 합니다.)
적용 예시(TLS 유지)
image: docker:27
services:
- name: docker:27-dind
variables:
DOCKER_HOST: tcp://docker:2376
DOCKER_TLS_CERTDIR: "/certs"
DOCKER_CERT_PATH: "/certs/client"
DOCKER_TLS_VERIFY: "1"
build:
script:
- ls -al /certs || true
- ls -al /certs/client || true
- docker version
- docker info
실패 시 체크
/certs/client가 비어있으면 공유가 안 된 것입니다(러너 executor/설정 이슈 가능).x509: certificate signed by unknown authority는 보통DOCKER_CERT_PATH가 틀리거나, 인증서가 아예 없을 때 발생합니다.
3) Runner가 privileged가 아니라서 DinD 데몬이 정상 기동하지 않는 문제
DinD는 내부적으로 컨테이너에서 Docker 데몬을 띄우므로, 대개 privileged 모드가 필요합니다. privileged가 아니면 데몬이 완전히 뜨지 못하고, 클라이언트는 “TLS 연결 실패”처럼 보이는 에러를 냅니다.
증상
- 서비스 컨테이너 로그에
mount: permission denied,iptables: Permission denied등이 보임 - job에서는
Cannot connect...또는 TLS 핑 실패
해결
- GitLab Runner 설정에서
privileged = true
config.toml 예시:
[[runners]]
executor = "docker"
[runners.docker]
privileged = true
보안상 privileged를 켜기 어렵다면, 아래 8번(대안: Kaniko/BuildKit rootless)을 검토하세요.
4) 서비스 DNS/alias 문제로 docker 호스트가 잘못 해석되는 케이스
DOCKER_HOST=tcp://docker:2376에서 docker는 GitLab CI 서비스 컨테이너의 호스트명(별칭)입니다. 특정 러너/네트워크 구성에서는 alias가 꼬이거나, 동일 이름이 충돌해 lookup docker ... no such host가 날 수 있습니다.
해결: 명시적으로 alias 지정
services:
- name: docker:27-dind
alias: docker
variables:
DOCKER_HOST: tcp://docker:2376
디버그
getent hosts docker
cat /etc/resolv.conf
DNS가 꼬이는 문제는 클라우드 네트워크 전반에서 빈번합니다. 비슷한 결로 사설 DNS 설정이 꼬여 NXDOMAIN이 나는 사례는 Azure Private Endpoint DNS 꼬임으로 NXDOMAIN 해결하기처럼 “원인이 TLS가 아니라 이름해석”일 수도 있다는 점을 기억해 두세요.
5) DinD 데몬이 뜨기 전에 job이 docker 명령을 실행하는 레이스 컨디션
서비스 컨테이너는 job과 동시에 시작되지만, Docker 데몬이 완전히 준비되기 전까지는 _ping가 실패합니다. 이때도 TLS 오류처럼 보일 수 있습니다.
해결: 헬스체크/대기 루프 추가
set -e
for i in $(seq 1 30); do
if docker info >/dev/null 2>&1; then
echo "Docker is ready"
break
fi
echo "Waiting for Docker... ($i)"
sleep 1
if [ "$i" -eq 30 ]; then
echo "Docker not ready" >&2
exit 1
fi
done
이 루프를 before_script에 넣으면 간헐적 실패가 크게 줄어듭니다.
6) docker CLI/daemon 버전 불일치로 TLS 핸드셰이크가 깨지는 경우
docker:latest와 docker:dind의 조합이 바뀌면서, CLI와 데몬 버전이 크게 벌어지면 예상치 못한 오류가 납니다. 표면적으로는 TLS 오류/연결 오류처럼 보이지만, 실제론 프로토콜/기능 호환 이슈일 수 있습니다.
해결: 같은 메이저 버전으로 고정
image: docker:27.3
services:
- name: docker:27.3-dind
체크
docker version
# Client: ...
# Server: ...
Client/Server 메이저를 맞추는 것이 가장 안전합니다.
7) 회사 프록시/SSL 검사(미들박스)로 인증서 검증이 뒤틀리는 케이스
기업망에서 HTTPS 트래픽을 가로채는 프록시/SSL inspection이 있으면, 컨테이너 내부 CA 번들이 실제 통신 경로와 맞지 않아 x509 오류가 발생할 수 있습니다. DinD는 컨테이너 간 통신이지만, 러너 네트워크/노드 정책에 따라 프록시 환경변수(HTTP_PROXY, HTTPS_PROXY)가 주입되어 예기치 않은 경로를 타기도 합니다.
해결 포인트
- DinD 통신은 내부망이므로
NO_PROXY에docker,127.0.0.1,localhost등을 추가 - 불필요한
HTTPS_PROXY가 job에 주입되는지 확인
variables:
NO_PROXY: "docker,localhost,127.0.0.1"
no_proxy: "docker,localhost,127.0.0.1"
추가로, 러너 레벨에서 프록시가 강제 주입된다면 runner 설정/노드 환경변수까지 점검해야 합니다.
8) privileged DinD를 포기하고 Kaniko/BuildKit로 전환(근본 회피)
DinD의 TLS/권한 문제는 구조적으로 반복됩니다. 특히 보안 정책상 privileged를 금지하는 조직이라면, 이미지 빌드를 DinD가 아닌 도구로 전환하는 것이 장기적으로 안정적입니다.
대안 A: Kaniko(컨테이너 내부에서 데몬 없이 빌드)
build:
image:
name: gcr.io/kaniko-project/executor:latest
entrypoint: [""]
script:
- /kaniko/executor
--context "$CI_PROJECT_DIR"
--dockerfile "$CI_PROJECT_DIR/Dockerfile"
--destination "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
대안 B: BuildKit(buildx) + rootless(환경에 따라)
러너/쿠버네티스 환경에서는 BuildKit 데몬을 별도로 두고 소켓 연결하는 패턴도 가능합니다.
이 방식은 TLS 자체를 없애거나 단순화해 “TLS 오류” 클래스를 통째로 제거합니다.
9) 쿠버네티스 executor에서 DinD 사용 시 보안 컨텍스트/스토리지 문제
GitLab Runner가 Kubernetes executor일 때 DinD를 쓰면, 다음 이슈가 TLS 오류처럼 나타날 수 있습니다.
- DinD가
/var/lib/docker를 쓸 수 없어 데몬이 죽음(결과: 연결/TLS 실패) - Pod security 정책으로 privileged/NET_ADMIN 등이 막힘
- ephemeral storage 부족으로 데몬이 반복 재시작
해결 체크리스트
securityContext.privileged: true또는 허용 가능한 대체 권한 부여/var/lib/docker에 emptyDir 또는 빠른 스토리지 마운트- 리소스 요청/제한을 현실적으로 설정
K8s에서 컨테이너가 반복 재시작하면 표면 증상은 네트워크/TLS 실패처럼 보이지만, 실제론 OOM/스토리지/권한이 원인인 경우가 많습니다. 이런 진단 흐름은 K8s CrashLoopBackOff에서 OOMKilled 원인 추적과 동일한 방식으로 접근하면 빠르게 정리됩니다.
실전용: “TLS 끔”과 “TLS 유지” 템플릿 2종
템플릿 A) 단순/안정(사설 러너에서 TLS 끔)
stages: [build]
image: docker:27.3
services:
- name: docker:27.3-dind
command: ["--host=tcp://0.0.0.0:2375", "--tls=false"]
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
build:
stage: build
before_script:
- |
set -e
for i in $(seq 1 30); do
docker info >/dev/null 2>&1 && break
sleep 1
done
script:
- docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" .
- docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
템플릿 B) TLS 유지(인증서 경로 명시)
stages: [build]
image: docker:27.3
services:
- name: docker:27.3-dind
alias: docker
variables:
DOCKER_HOST: tcp://docker:2376
DOCKER_TLS_CERTDIR: "/certs"
DOCKER_CERT_PATH: "/certs/client"
DOCKER_TLS_VERIFY: "1"
build:
stage: build
before_script:
- |
set -eux
for i in $(seq 1 30); do
docker info >/dev/null 2>&1 && break
echo "waiting docker..."; sleep 1
done
script:
- docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" .
- docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
결론: “TLS 오류”는 원인이 아니라 증상이다
GitLab CI에서 DinD TLS 오류는 대부분 아래 4개 중 하나로 환원됩니다.
- TLS 설정(2376)과 클라이언트 환경변수 불일치
- privileged/권한 문제로 데몬이 제대로 뜨지 않음
- 서비스 DNS/네트워크 문제로
docker호스트에 접근 불가 - 데몬 준비 전 실행/버전 불일치/프록시 등 주변 요인
가장 빠른 해결은 TLS를 끄는 것이지만, 조직 보안/공유 러너 환경에서는 TLS 유지 또는 Kaniko/BuildKit 전환이 더 안전합니다. 위 9가지를 체크리스트처럼 위에서 아래로 적용하면, “왜 TLS가 깨졌는지”를 재현 가능하게 설명하면서도 파이프라인을 안정화할 수 있습니다.