Published on

Docker BuildKit 캐시 깨짐? GitLab CI 속도 3배

Authors

서로 다른 커밋인데도 매번 apt-get부터 다시 돌고, 파이썬 의존성 설치가 항상 풀로 재실행되고, GitLab CI 빌드가 10분 -> 30분으로 튀는 경험은 대부분 “BuildKit 캐시가 깨졌다”로 귀결됩니다. 문제는 캐시가 깨지는 이유가 생각보다 다양하고, GitLab Runner 환경에서는 로컬 캐시가 기본적으로 휘발성인 경우가 많다는 점입니다.

이 글에서는 GitLab CI에서 BuildKit 캐시가 왜 자주 미스 나는지(원인), 그리고 --cache-to/--cache-from 기반 registry 캐시를 중심으로 실제로 빌드 시간을 3배 수준으로 줄이는 구성(해결)을 단계별로 정리합니다.

참고로 멀티스테이지와 캐시 레이어 설계 자체가 핵심인 만큼, GitHub Actions 기준이지만 원리가 동일한 글도 함께 보면 도움이 됩니다: GitHub Actions로 Docker 멀티스테이지·캐시 튜닝

GitLab CI에서 BuildKit 캐시가 “자주” 깨지는 6가지 원인

1) Runner가 매번 새 VM/Pod로 뜨는 구조(로컬 캐시 휘발)

GitLab SaaS Shared Runner나 Kubernetes executor를 쓰면, Job 컨테이너가 매번 새로 생성됩니다. 이 경우 Docker daemon의 로컬 레이어 캐시가 유지되지 않으니, BuildKit을 켰더라도 캐시 히트가 거의 나지 않습니다.

해결 방향은 로컬 캐시에 기대지 말고 원격 캐시(Registry cache) 를 쓰는 것입니다.

2) docker build는 캐시를 “가져오지” 않는다

많이 놓치는 지점이 --cache-from입니다. 로컬에 이미지가 없으면 캐시 소스가 없고, remote registry에 같은 태그가 있더라도 자동으로 레이어 캐시를 가져오지 않습니다.

즉, 캐시를 쓰려면 다음이 필요합니다.

  • 캐시를 저장할 대상(--cache-to)
  • 캐시를 가져올 소스(--cache-from)

3) Dockerfile 레이어 설계가 캐시를 계속 무효화

대표적인 패턴:

  • COPY . .를 너무 일찍 해서, 소스 한 줄 바뀔 때마다 이후 레이어가 전부 무효화
  • RUN apt-get update만 단독으로 실행(다음 레이어에서 install 시점이 바뀌면 캐시 효율 저하)
  • 의존성 파일(package-lock.json, poetry.lock, requirements.txt)보다 소스를 먼저 복사

4) 빌드 컨텍스트가 너무 큼(불필요 파일이 해시를 바꿈)

BuildKit은 컨텍스트 파일들의 메타데이터/내용 변화에 민감합니다. node_modules, .venv, dist, __pycache__ 같은 디렉터리가 컨텍스트에 포함되면, 실제로는 이미지에 안 쓰더라도 캐시 키가 흔들릴 수 있습니다.

.dockerignore는 “옵션”이 아니라 캐시 안정성의 핵심입니다.

5) 비결정적 명령(시간/네트워크/미러 상태에 따라 결과가 바뀜)

예:

  • apt-get update가 매번 다른 인덱스를 받음
  • pip install이 최신 버전을 끌어오도록 열려 있음
  • curl https://.../latest 같은 동적 다운로드

이런 경우 캐시가 맞아도 결과가 달라질 수 있어, 재현성/캐시 효율이 동시에 무너집니다. 가능하면 버전 핀ning과 lockfile을 사용하세요.

6) BuildKit 기능을 켰지만 “캐시 내보내기”는 안 함

DOCKER_BUILDKIT=1만으로는 GitLab CI에서 속도가 극적으로 좋아지지 않는 경우가 많습니다. 핵심은 캐시를 외부로 export해서 다음 파이프라인이 가져오게 만드는 것입니다.

목표 아키텍처: Registry 캐시로 파이프라인 간 캐시 공유

GitLab CI에서 가장 재현 가능하고 효과가 큰 패턴은 아래입니다.

  • BuildKit + buildx 사용
  • 캐시를 GitLab Container Registry에 저장
  • 다음 빌드에서 해당 캐시를 --cache-from으로 로드

캐시 저장소는 보통 두 가지를 둡니다.

  • 이미지 태그: :$CI_COMMIT_SHA (정확한 산출물)
  • 캐시 태그: :buildcache (누적 캐시)

실전: GitLab CI 설정 예시(빌드 3배 가속의 기본형)

아래 예시는 Docker-in-Docker 기반입니다. Runner에 따라 docker:24 버전은 조정하세요.

stages:
  - build

variables:
  DOCKER_BUILDKIT: "1"
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: ""  # dind에서 TLS 비활성(환경에 따라 정책 확인)

build-image:
  stage: build
  image: docker:24
  services:
    - name: docker:24-dind
      command: ["--mtu=1460"]
  before_script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
    - docker buildx version
    - docker buildx create --use --name ci-builder || docker buildx use ci-builder
  script:
    - |
      IMAGE="$CI_REGISTRY_IMAGE/app"
      docker buildx build \
        --platform linux/amd64 \
        --tag "$IMAGE:$CI_COMMIT_SHA" \
        --tag "$IMAGE:latest" \
        --cache-from type=registry,ref="$IMAGE:buildcache" \
        --cache-to type=registry,ref="$IMAGE:buildcache",mode=max \
        --push \
        .
  rules:
    - if: '$CI_COMMIT_BRANCH'

포인트 해설

  • --cache-to ... mode=max
    • 가능한 많은 중간 레이어 정보를 캐시로 내보냅니다. 캐시 크기는 커질 수 있지만 CI 속도는 가장 잘 나옵니다.
  • --push
    • GitLab CI에서 build 결과를 다음 Job이 쓰거나 배포가 바로 이어진다면 필수입니다.
  • --platform
    • 멀티 플랫폼이 필요 없다면 단일로 고정하는 게 캐시 안정성에 유리합니다.

Dockerfile 레이어를 “캐시 친화적”으로 재구성하기

캐시는 CI 설정만으로는 한계가 있고, Dockerfile의 레이어 분리가 결정적입니다.

나쁜 예: 소스 복사가 너무 이른 경우

FROM python:3.12-slim
WORKDIR /app

COPY . .
RUN pip install -r requirements.txt

CMD ["python", "main.py"]

소스가 조금만 바뀌어도 pip install 레이어가 매번 무효화됩니다.

좋은 예: 의존성 파일을 먼저 복사

FROM python:3.12-slim
WORKDIR /app

# 시스템 패키지는 한 레이어에서 끝내고 캐시 효율을 높임
RUN apt-get update \
  && apt-get install -y --no-install-recommends build-essential \
  && rm -rf /var/lib/apt/lists/*

# 의존성 정의 파일만 먼저 복사
COPY requirements.txt ./

# pip 캐시를 BuildKit 캐시 마운트로 유지(빌드 간 다운로드 비용 절감)
RUN --mount=type=cache,target=/root/.cache/pip \
  pip install --no-cache-dir -r requirements.txt

# 마지막에 소스 복사
COPY . .

CMD ["python", "main.py"]

여기서 RUN --mount=type=cache는 BuildKit 전용 기능입니다. GitLab CI에서 registry 캐시와 결합하면 “레이어 캐시 + 다운로드 캐시”가 같이 살아서 체감 속도가 크게 좋아집니다.

.dockerignore로 캐시 키 흔들림 줄이기

컨텍스트를 줄이면 전송 시간뿐 아니라 “불필요한 변경으로 인한 캐시 미스”도 줄어듭니다.

# .dockerignore
.git
.gitlab
node_modules
.venv
__pycache__
*.pyc
dist
build
coverage
.DS_Store
.env

특히 Python/Node 프로젝트에서 node_modules, .venv가 들어가면 캐시가 사실상 매번 새로 잡히는 수준으로 흔들릴 수 있습니다.

캐시가 또 깨질 때 체크리스트(원인 빠르게 찾기)

1) BuildKit 로그에서 캐시 히트 확인

--progress=plain을 켜면 어떤 스텝이 캐시를 탔는지 확인이 쉽습니다.

docker buildx build --progress=plain .

출력에서 CACHED가 줄줄 나오지 않으면, 캐시 소스 로드가 안 됐거나(대부분 --cache-from 문제), 컨텍스트/레이어가 계속 바뀌는 겁니다.

2) 캐시 태그가 실제로 registry에 푸시됐는지

:$CI_COMMIT_SHA는 잘 올라가는데 :buildcache가 없으면, --cache-to가 실패했거나 권한 문제가 있을 수 있습니다.

3) GitLab Registry 권한/프로젝트 설정

프로젝트가 private이면 Job token 권한이 제한될 수 있습니다. CI_REGISTRY_USER/CI_REGISTRY_PASSWORD 로그인 경로가 맞는지부터 확인하세요.

4) 멀티스테이지에서 “캐시가 안 먹는 스테이지”가 있는지

멀티스테이지는 좋지만, 스테이지 간 COPY --from=...가 많고 앞단 스테이지가 자주 바뀌면 뒤가 줄줄이 무효화됩니다. 스테이지별로 변경 빈도를 기준으로 재배치하세요.

멀티스테이지 최적화 관점은 다음 글의 접근을 그대로 GitLab CI에 적용할 수 있습니다: GitHub Actions로 Docker 멀티스테이지·캐시 튜닝

추가 최적화: 파이프라인 시간을 더 줄이는 팁 5가지

1) 베이스 이미지 태그를 고정

python:3.12-slim 같은 floating tag는 내부적으로 업데이트가 발생해 캐시가 무효화될 수 있습니다. 가능하면 digest 고정도 고려하세요(보안/재현성 이점).

2) 의존성 설치를 “변경이 적은 레이어”로 최대한 이동

  • Node: package.json/package-lock.json 먼저 복사 후 npm ci
  • Python: requirements.txt/poetry.lock 먼저 복사

3) 테스트/린트를 이미지 빌드와 분리

빌드 이미지와 테스트 이미지가 섞이면 캐시 키가 흔들립니다. 테스트는 별도 Job에서 소스 체크아웃 기반으로 돌리고, 이미지 빌드는 “패키징”에 집중시키는 편이 빠릅니다.

4) 네트워크 병목이면 --mount=type=cache를 적극 사용

pip, npm, apt 모두 캐시 마운트가 가능합니다. 단, 캐시 마운트는 “레이어 캐시”가 아니라 “다운로드 캐시”에 가깝기 때문에 registry 캐시와 같이 써야 효과가 큽니다.

5) 실패 시 원인 추적을 위해 셸 옵션 주의

CI 스크립트에서 set -euo pipefail을 쓰는 경우, 캐시 관련 커맨드가 실패해도 로그가 빈약하게 남을 수 있습니다. 안전한 예외 처리 패턴은 이 글이 도움이 됩니다: bash set -euo pipefail 함정과 안전한 예외처리

정리: “캐시가 깨짐”을 구조적으로 없애는 방법

GitLab CI에서 BuildKit 캐시 문제를 근본적으로 줄이려면 우선순위가 명확합니다.

  1. 로컬 캐시에 기대지 말고 registry 캐시를 표준으로 만들기(--cache-to/--cache-from)
  2. Dockerfile 레이어를 변경 빈도 기준으로 재배치하기(의존성 파일 먼저, 소스는 나중)
  3. .dockerignore컨텍스트 흔들림 제거
  4. --progress=plain로 캐시 히트/미스를 눈으로 확인하며 튜닝

이 4가지만 제대로 적용해도, “매번 풀 빌드”에서 “대부분 캐시 빌드”로 바뀌면서 GitLab CI 빌드 시간이 3배 이상 개선되는 케이스가 흔합니다.