Published on

GitLab CI DinD TLS 실패, 원인별 해결법

Authors

GitLab CI에서 Docker-in-Docker(DinD)를 쓰다 보면 어느 날 갑자기 docker login, docker build, docker push가 TLS 관련 에러로 무너집니다. 특히 러너가 업데이트되거나 docker:dind 이미지 태그가 바뀌는 순간, 이전에는 우연히 통과하던 설정이 더 이상 동작하지 않는 일이 흔합니다.

이 글은 “DinD에서 TLS가 왜 깨지는지”를 증상별로 분류하고, 가장 많이 쓰이는 해결 루트를 재현 가능한 형태의 .gitlab-ci.yml 예제로 제공합니다. 동일한 증상이라도 원인이 다를 수 있으니, 로그 메시지 패턴을 기준으로 빠르게 분기해보세요.

참고로 CI에서 인증/네트워크 이슈를 점검하는 접근은 API 인증 오류를 추적할 때와 유사합니다. 체크리스트 방식이 필요하다면 OpenAI Responses API 401 403 인증오류 점검 가이드도 함께 보면 도움이 됩니다.

DinD의 TLS 구조를 먼저 이해하기

GitLab CI에서 DinD를 쓸 때 일반적인 구성은 아래와 같습니다.

  • Job 컨테이너: docker:24 같은 Docker CLI 이미지
  • Service 컨테이너: docker:24-dind (Docker daemon)
  • Job 컨테이너의 Docker CLI가 Service 컨테이너의 Docker daemon에 TCP로 접속

여기서 TLS는 주로 docker:dind가 제공하는 자동 인증서 생성 메커니즘(DOCKER_TLS_CERTDIR)과 결합됩니다.

  • DOCKER_TLS_CERTDIR가 설정되면 DinD는 서버 인증서를 만들고, 클라이언트는 해당 CA를 신뢰해야 합니다.
  • GitLab CI에서는 보통 /certs 볼륨을 공유해 Job 컨테이너가 클라이언트 인증서를 읽도록 구성합니다.

문제는 다음 중 하나라도 어긋나면 TLS가 바로 깨진다는 점입니다.

  • Job이 바라보는 DOCKER_HOST가 틀림(서비스 alias, 포트)
  • TLS 켠 상태인데 클라이언트 인증서가 없음(볼륨 공유 실패)
  • TLS 끈 상태인데 클라이언트가 TLS로 접속함(포트, 환경변수 불일치)
  • Runner가 privileged를 허용하지 않아 DinD 데몬 자체가 비정상

에러 메시지 패턴별 원인과 해결

1) Cannot connect to the Docker daemon + tcp://docker:2376

대표 로그 예시(인라인 코드로 확인):

  • Cannot connect to the Docker daemon at tcp://docker:2376. Is the docker daemon running?
  • dial tcp: lookup docker: no such host

원인

  • 서비스 alias가 docker가 아닌데 DOCKER_HOSTtcp://docker:2376로 고정
  • services가 누락됐거나, docker:dind가 기동 실패
  • 네임해석이 안 되는 러너 네트워크 설정 문제

해결

  • services에 alias를 명시하고 DOCKER_HOST를 그 alias로 맞춥니다.
  • DinD가 뜨는지 docker info 전 단계에서 확인합니다.
image: docker:24

services:
  - name: docker:24-dind
    alias: docker

variables:
  DOCKER_HOST: "tcp://docker:2376"
  DOCKER_TLS_CERTDIR: "/certs"

before_script:
  - docker version
  - docker info

build:
  stage: build
  script:
    - docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" .

만약 lookup docker: no such host가 나오면 alias가 제대로 적용되지 않은 경우가 많습니다. alias: docker를 명시하고, DOCKER_HOST도 일치시키세요.

2) x509: certificate signed by unknown authority

대표 로그 예시:

  • x509: certificate signed by unknown authority
  • tls: failed to verify certificate: x509: ...

원인

  • TLS는 켜져 있는데, Job 컨테이너가 DinD가 만든 CA를 못 읽음
  • /certs 볼륨 공유가 안 됨(러너 executor, 볼륨 정책)
  • DOCKER_CERT_PATH가 틀림

해결 A: GitLab 권장 방식(TLS 유지)으로 정렬

TLS를 유지하려면 아래 3가지를 세트로 맞춰야 합니다.

  • DOCKER_TLS_CERTDIR/certs
  • DOCKER_HOSTtcp://docker:2376
  • Job이 DOCKER_CERT_PATH/certs/client로 바라봄
image: docker:24

services:
  - name: docker:24-dind
    alias: docker

variables:
  DOCKER_HOST: "tcp://docker:2376"
  DOCKER_TLS_CERTDIR: "/certs"
  DOCKER_CERT_PATH: "/certs/client"
  DOCKER_TLS_VERIFY: "1"

before_script:
  - docker info

build:
  script:
    - docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" .

여기서 핵심은 DOCKER_TLS_VERIFY를 명시해 “나는 TLS로 붙는다”를 분명히 하는 것입니다. 환경변수가 애매하면 CLI가 다른 경로로 동작하면서 인증서 검증이 꼬일 수 있습니다.

해결 B: TLS를 끄고 2375로 단순화(내부망 전제)

보안적으로는 덜 권장되지만, 내부 러너 네트워크에서만 쓰고 빠르게 안정화가 필요하면 TLS를 끄는 것이 가장 단순합니다.

  • DOCKER_TLS_CERTDIR를 빈 문자열로
  • DOCKER_HOSTtcp://docker:2375
image: docker:24

services:
  - name: docker:24-dind
    alias: docker

variables:
  DOCKER_HOST: "tcp://docker:2375"
  DOCKER_TLS_CERTDIR: ""

before_script:
  - docker info

build:
  script:
    - docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" .

운영 정책상 TLS를 꺼도 되는지(공유 러너, 멀티테넌트 여부)를 먼저 확인하세요.

3) client sent an HTTP request to an HTTPS server

대표 로그 예시:

  • client sent an HTTP request to an HTTPS server

원인

  • DinD는 TLS(HTTPS, 2376)로 떠 있는데, Job이 HTTP(2375)로 접속
  • 즉, DOCKER_HOSTDOCKER_TLS_CERTDIR 조합이 서로 충돌

해결

  • TLS를 쓸 거면 DOCKER_HOSTtcp://docker:2376로, TLS를 끌 거면 DOCKER_TLS_CERTDIR""로 맞춥니다.

잘 되는 조합은 아래 둘 중 하나뿐이라고 생각하면 됩니다.

  • TLS ON: DOCKER_HOST=tcp://docker:2376 + DOCKER_TLS_CERTDIR=/certs
  • TLS OFF: DOCKER_HOST=tcp://docker:2375 + DOCKER_TLS_CERTDIR=""

4) tls: first record does not look like a TLS handshake

대표 로그 예시:

  • tls: first record does not look like a TLS handshake

원인

  • 위와 반대 케이스가 대부분입니다.
  • DinD는 HTTP(2375)인데, Job이 TLS로 접속(2376 또는 DOCKER_TLS_VERIFY=1)

해결

  • TLS OFF 구성이라면 DOCKER_TLS_VERIFY를 제거하고, DOCKER_HOSTtcp://docker:2375로 고정하세요.

5) DinD 데몬이 안 뜨는 케이스(privileged 누락)

표면적으로는 TLS 에러처럼 보이지만, 실제로는 DinD가 정상 기동을 못 해서 접속이 실패하는 경우가 있습니다.

의심 신호:

  • docker info가 타임아웃
  • 서비스 로그에 mount: permission denied, failed to start daemon류 메시지

해결

GitLab Runner가 Docker executor를 쓴다면 DinD는 보통 privileged가 필요합니다.

  • 러너 설정에서 privileged = true 활성화
  • 또는 프로젝트/그룹 러너 정책에서 허용

.gitlab-ci.yml만으로 해결이 안 되고 러너 레벨 설정이 필요한 전형적인 케이스입니다.

실전: GitLab Container Registry 로그인까지 포함한 안정 템플릿

아래는 TLS ON(권장) 기준으로, 레지스트리 로그인부터 빌드/푸시까지 한 번에 정리한 예제입니다.

stages: [build]

image: docker:24

services:
  - name: docker:24-dind
    alias: docker

variables:
  DOCKER_HOST: "tcp://docker:2376"
  DOCKER_TLS_CERTDIR: "/certs"
  DOCKER_CERT_PATH: "/certs/client"
  DOCKER_TLS_VERIFY: "1"
  DOCKER_DRIVER: overlay2

before_script:
  - docker version
  - docker info
  - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"

build-and-push:
  stage: build
  script:
    - docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" .
    - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"

체크포인트

  • docker info가 성공해야 이후 TLS/로그인 문제를 분리해서 볼 수 있습니다.
  • DOCKER_CERT_PATH는 반드시 "/certs/client"여야 합니다. "/certs"로 잡으면 클라이언트 키/인증서 위치가 어긋납니다.

자주 놓치는 디테일 6가지

1) docker:dind 이미지 태그를 고정하지 않음

docker:latestdocker:dind를 그대로 쓰면 어느 날 TLS 기본값이나 엔트리포인트 동작이 바뀌어 깨질 수 있습니다. 최소한 메이저 버전은 고정하세요.

  • 예: docker:24docker:24-dind

2) 서비스 alias 미지정

GitLab CI의 서비스 호스트네임은 이미지 이름이 아니라 alias로 안정화하는 편이 좋습니다.

  • 권장: alias: docker

3) DOCKER_HOST를 하드코딩해놓고 TLS 설정만 바꿈

TLS를 끄면서 DOCKER_TLS_CERTDIR""로 바꾸고 DOCKER_HOST2376에 둔 채로 두면 100% 실패합니다.

4) 회사 프록시/미들박스가 TLS를 간섭

사내 네트워크에서 프록시가 TLS를 가로채면 x509가 이상한 형태로 발생할 수 있습니다. 이 경우는 DinD 인증서 문제가 아니라 네트워크 경로 문제일 수 있으니, 동일 러너에서 외부 API 인증이 깨지는지도 같이 확인해보세요(점검 접근은 OpenAI Responses API 401 403 인증오류 점검 가이드 흐름과 유사합니다).

5) Kubernetes executor에서 DinD를 무리하게 사용

Kubernetes executor에서는 DinD가 보안/성능/네트워크 면에서 까다롭습니다. 가능하면 Kaniko, BuildKit(rootless), Docker socket 방식 등 대안을 검토하세요. (대용량 요청/프록시/ingress 이슈를 겪고 있다면 Kubernetes API 413 Request Entity Too Large 해결 같은 네트워크 레이어 점검 글도 같이 보면 좋습니다.)

6) 문제를 “TLS”로만 단정하고 로그를 안 나눔

docker info 성공 여부를 기준으로 계층을 분리하세요.

  • docker info 실패: DinD 기동/네트워크/privileged 문제
  • docker info 성공 + docker login 실패: 레지스트리 인증/CA/프록시 문제
  • docker build만 실패: 빌드 컨텍스트, 네트워크, 레이어 캐시, 디스크 문제

문제 해결을 빠르게 만드는 10분 진단 루틴

아래 순서대로 실행하면, TLS 문제를 “정확히 어디에서 깨지는지” 10분 내로 분리할 수 있습니다.

  1. servicesdocker:dind가 있는지 확인
  2. alias를 docker로 고정했는지 확인
  3. DOCKER_HOST2375인지 2376인지 확인
  4. TLS ON이면 DOCKER_TLS_CERTDIR=/certs, DOCKER_CERT_PATH=/certs/client, DOCKER_TLS_VERIFY=1 확인
  5. TLS OFF이면 DOCKER_TLS_CERTDIR="" 확인
  6. before_scriptdocker info 추가
  7. 실패 시 러너에서 privileged 허용 여부 확인
  8. 프록시 환경변수(HTTP_PROXY, HTTPS_PROXY, NO_PROXY)가 있다면 NO_PROXYdocker alias를 포함

NO_PROXY 예시는 다음처럼 인라인 코드로 관리하세요: NO_PROXY=localhost,127.0.0.1,docker

마무리

GitLab CI의 DinD TLS 실패는 “인증서가 나빠서”라기보다, 대부분 TLS ON/OFF 조합 불일치, 서비스 alias/포트 불일치, certs 볼륨 공유 실패, privileged 누락 같은 구성 문제에서 시작합니다.

가장 안정적인 운영 전략은 다음 중 하나를 선택해 일관되게 고정하는 것입니다.

  • 보안 우선: TLS ON 조합을 표준 템플릿으로 고정
  • 단순 우선(내부망): TLS OFF로 단순화하되 러너 격리/권한을 강화

CI 파이프라인에서 변경이 잦다면, 템플릿을 중앙화하고 태그를 고정해 “업데이트로 인한 우발적 TLS 붕괴”를 줄이는 것이 장기적으로 가장 큰 비용 절감 포인트입니다.