Docker BuildKit 캐시와 비밀값, 빠른 빌드보다 먼저 나눠야 할 것
AI를 활용하여 생성한 이미지입니다.
Docker 빌드가 느리다고 해서 캐시를 무작정 크게 만들면 관리가 더 어려워져요. 소스가 바뀌어 다시 실행되는 레이어와, 패키지 다운로드를 재사용하는 캐시 마운트는 역할이 다릅니다. 여기에 API 토큰까지 ARG나 환경 변수로 넣으면 이미지나 빌드 기록에 비밀값이 남을 수 있어요. Docker는 BuildKit에서 캐시 최적화와 비밀값 전달을 서로 다른 기능으로 설명합니다.[1][2]
| 문제 | 맞는 도구 | 잘못된 선택 |
|---|---|---|
| 패키지를 매번 다시 다운로드 | RUN --mount=type=cache |
토큰을 캐시 디렉터리에 함께 저장 |
| CI 러너가 매번 새로 시작 | --cache-from·--cache-to |
로컬 빌더 캐시만 믿기 |
| 사설 저장소 인증 필요 | --secret 또는 SSH mount |
ARG TOKEN=... |
레이어 캐시와 캐시 마운트를 구분해요
일반 레이어 캐시는 Dockerfile 명령과 그 명령이 의존하는 파일이 그대로일 때 재사용됩니다. 따라서 자주 바뀌는 소스 파일을 너무 일찍 COPY하면 뒤의 의존성 설치 레이어까지 무효화될 수 있어요. Docker는 레이어 순서를 정리하고, 필요 없는 파일을 .dockerignore로 빌드 컨텍스트에서 제외하라고 안내합니다.[1]
패키지 관리자의 다운로드 폴더에는 캐시 마운트를 붙일 수 있어요.
# syntax=docker/dockerfile:1
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
COPY . .
이 방식은 설치 명령의 레이어가 다시 만들어져도 변경되지 않은 패키지를 다시 받지 않도록 돕습니다. npm, Go, Rust, apt 등은 캐시 위치와 동시 접근 방식이 다르므로 패키지 관리자의 요구를 확인해야 해요. Docker 문서도 apt에는 sharing=locked가 필요할 수 있다고 설명합니다.[1]
CI에서는 외부 캐시를 별도로 내보내요
기본 캐시는 현재 BuildKit 빌더 안에 있어요. CI 러너가 매번 새로 만들어지는 환경이라면 이 캐시는 다음 실행으로 이어지지 않습니다. 이때 registry, local, GitHub Actions 같은 외부 백엔드로 캐시를 내보내고 다음 빌드에서 다시 가져오는 구성을 사용합니다.[3]
docker buildx build --push -t registry.example/app:latest \
--cache-to type=registry,ref=registry.example/app:buildcache,mode=max \
--cache-from type=registry,ref=registry.example/app:buildcache .
mode=min은 최종 이미지에 포함되는 레이어 중심으로 저장하고, mode=max는 중간 단계까지 포함해 캐시 적중 가능성을 높입니다. 대신 캐시 저장량과 가져오는 시간이 커질 수 있어요. 브랜치별 캐시를 나누려면 같은 위치를 덮어쓰지 않도록 참조 이름을 분리하세요.[3]
토큰은 secret mount로만 전달해요
Docker는 빌드 인자와 환경 변수가 최종 이미지에 남을 수 있으므로 비밀값 전달용으로 쓰지 말라고 안내합니다. 대신 빌드 명령에서 --secret을 넘기고 Dockerfile의 RUN --mount=type=secret에서 잠시 읽는 구조를 사용해요.[2]
docker buildx build \
--secret id=private_token,env=PRIVATE_TOKEN \
-t example/app:dev .
RUN --mount=type=secret,id=private_token \
TOKEN=$(cat /run/secrets/private_token) && ./download-private-deps.sh
비밀값을 읽은 명령의 출력 로그에도 토큰을 출력하지 말고, 빌드 캐시에 인증 파일이 복사되지 않았는지 확인하세요. SSH로 사설 Git 저장소를 가져올 때는 SSH mount를 쓰는 편이 git clone 결과와 인증 재료를 이미지 레이어에서 분리하기 쉽습니다.[2]
빌드가 느릴 때 확인하는 순서
- 빌드 컨텍스트와
.dockerignore부터 줄입니다. - 자주 바뀌는
COPY가 의존성 설치보다 앞에 있는지 봅니다. - 패키지 관리자의 캐시 경로에 cache mount를 붙입니다.
- CI라면 외부 캐시의 import·export 경로와 브랜치 범위를 나눕니다.
- 토큰·SSH 키·클라우드 자격 증명이 ARG, ENV, COPY, 로그에 남지 않는지 확인합니다.
Docker Desktop의 WSL2 자원과 VHDX 용량까지 함께 조정해야 한다면 Docker Desktop WSL2 자원 제한을 나누어 조정하는 방법도 연결해서 볼 수 있어요.
캐시가 빨라도 보안 경계는 남겨 둬요
캐시를 외부 레지스트리에 저장하면 CI 실행기 사이에서 재사용할 수 있지만, 그 저장 위치에 인증 파일이나 빌드 산출물의 비밀값이 들어가지 않는지 별도로 확인해야 해요. 캐시 적중률을 높이려고 모든 중간 파일을 내보내는 설정을 쓰더라도, 토큰을 캐시 디렉터리에 쓰는 방식은 피해야 합니다. BuildKit의 secret mount는 실행 중 필요한 파일을 제공하는 기능이고, 캐시 백엔드는 빌드 결과를 재사용하는 기능이라서 두 경계를 섞으면 안 돼요.[2][3]
CI에서 외부 캐시를 쓸 때는 캐시 참조 이름도 설계 대상이에요. 여러 브랜치가 같은 참조를 동시에 갱신하면 한 빌드가 다른 빌드의 캐시를 덮어쓸 수 있습니다. 기본 브랜치와 기능 브랜치의 참조를 나누고, 병합 후에는 기본 브랜치 캐시를 다시 채우는 식으로 운용하면 어떤 빌드가 만든 캐시인지 추적하기 쉬워요. Docker는 캐시 백엔드마다 저장 방식과 제약이 다르다고 설명하므로, 사용하는 CI 제공자의 백엔드 지원 범위를 먼저 확인해야 합니다.[3]
실패 원인을 구분하는 진단 순서
- 로컬 빌더에서 같은 Dockerfile을 실행해 레이어 캐시 문제인지 확인합니다.
- 캐시를 끈 상태와 켠 상태를 비교해 패키지 다운로드 단계가 병목인지 봅니다.
- CI에서
cache-fromimport가 실제로 실행되는지 로그를 확인합니다. - 외부 캐시 권한·저장 위치·참조 이름을 확인하고, 실패하면 로컬 캐시만으로도 빌드가 끝나는지 검증합니다.
- secret mount를 쓰는 명령의 로그와 결과 레이어에 토큰·인증 파일이 남지 않는지 확인합니다.
캐시 import가 실패했다고 빌드 자체가 실패해야 하는 것은 아니에요. 캐시는 속도를 위한 보조 경로이므로, 캐시가 비어 있어도 정합한 새 빌드가 가능한 구조를 유지해야 합니다. 반대로 빌드가 성공했다는 이유만으로 비밀값 노출이 없었다고 판단해서도 안 돼요. Dockerfile의 ARG, ENV, COPY, 명령 출력, 캐시 디렉터리를 각각 확인해야 합니다.[1][2]
캐시 설정을 되돌리는 기준
새 캐시 백엔드가 저장 공간을 과도하게 쓰거나 권한 문제를 일으키면 먼저 cache-to export를 제거하고 기본 빌드가 끝나는지 확인하세요. 패키지 다운로드 캐시도 필요할 때만 유지하고, 자격 증명이 들어간 디렉터리는 cache mount 대상에서 빼야 합니다. 빠른 빌드보다 우선할 것은 재현 가능한 빌드와 비밀값의 분리예요.[1][2][3]