Contents
see List왜 Docker 빌드 캐시를 다시 설계해야 하는가
컨테이너 이미지를 매번 새로 빌드하는 CI 환경에서는 같은 의존성을 반복해서 내려받고, 같은 컴파일 결과를 다시 만드는 시간이 누적됩니다. 개발자 노트북에서는 Docker 레이어 캐시가 자연스럽게 남지만, GitHub Actions, GitLab CI, Jenkins 임시 에이전트처럼 실행 환경이 매번 새로 만들어지는 곳에서는 캐시가 사라지는 경우가 많습니다. 이때 단순히 Dockerfile 명령 순서를 정리하는 것만으로는 한계가 있습니다. BuildKit의 캐시 마운트와 원격 캐시를 함께 쓰면 패키지 매니저 캐시, 빌드 산출물, 이미지 레이어를 분리해서 재사용할 수 있고, 빌드 시간이 긴 Node.js, Java, Python 프로젝트에서 효과가 큽니다.
기본 원칙: 변경이 잦은 파일을 뒤로 미루기
Docker 빌드는 각 명령을 레이어로 저장합니다. 앞 레이어가 바뀌면 뒤 레이어는 모두 다시 계산되므로, 의존성 설치에 필요한 파일과 애플리케이션 소스 전체를 같은 단계에서 복사하면 작은 코드 수정에도 패키지 설치가 반복됩니다. 먼저 lock 파일만 복사해서 의존성을 설치하고, 그 다음 소스 코드를 복사하는 구조로 나누어야 합니다. 이 원칙은 npm, pnpm, Maven, Gradle, pip 모두에 적용됩니다.
- 의존성 설치 단계에는 package-lock.json, pnpm-lock.yaml, pom.xml, build.gradle 같은 잠금 파일만 넣습니다.
- 소스 코드는 의존성 설치가 끝난 뒤 복사합니다.
- 테스트, 빌드, 런타임 이미지를 단계별로 나누어 최종 이미지에는 필요한 파일만 남깁니다.
- .dockerignore를 작성해 node_modules, build, target, .git, 로컬 환경 파일이 빌드 컨텍스트에 들어가지 않도록 합니다.
BuildKit 캐시 마운트 적용 예시
BuildKit의 RUN --mount=type=cache는 특정 디렉터리를 빌드 캐시 영역으로 분리합니다. 이미지 레이어 안에 패키지 캐시를 남기지 않으면서도 다음 빌드에서 다시 사용할 수 있습니다. 아래 예시는 Node.js 프로젝트에서 npm 캐시를 재사용하고, 빌드 결과만 최종 이미지에 복사하는 구성입니다.
# syntax=docker/dockerfile:1.7
FROM node:22-bookworm AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci --prefer-offline --no-audit
FROM node:22-bookworm AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:22-bookworm-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
CMD ["node", "dist/server.js"]
여기서 중요한 부분은 npm 캐시와 node_modules의 역할을 구분하는 것입니다. /root/.npm 캐시는 다운로드한 패키지 tarball을 보관하고, node_modules는 실제 설치 결과입니다. 캐시 마운트는 레이어 크기를 키우지 않으면서 네트워크 다운로드를 줄입니다. 단, lock 파일이 바뀌면 설치 결과는 다시 계산되어야 하므로 재현성을 해치지 않습니다.
CI에서 원격 캐시까지 연결하기
임시 CI 러너는 로컬 BuildKit 캐시가 오래 유지되지 않을 수 있습니다. 이 경우 registry나 GitHub Actions cache backend 같은 원격 캐시를 붙여야 합니다. 가장 단순한 형태는 buildx로 이미지를 빌드하면서 이전 캐시를 가져오고, 새 캐시를 다시 내보내는 방식입니다. 캐시는 운영 이미지와 별도 태그로 관리하면 롤백이나 배포 태그 정책과 충돌하지 않습니다.
docker buildx create --use --name ci-builder
docker buildx build --file Dockerfile --tag registry.example.com/myapp:2026-05-27 --cache-from type=registry,ref=registry.example.com/myapp:buildcache --cache-to type=registry,ref=registry.example.com/myapp:buildcache,mode=max --push .
mode=max는 중간 단계 캐시까지 최대한 보관하므로 빌드 시간이 많이 줄어들 수 있습니다. 대신 캐시 저장소 크기가 커질 수 있으므로 보관 정책이 필요합니다. 사내 레지스트리를 사용한다면 buildcache 태그의 용량과 갱신 주기를 모니터링하고, 오래된 캐시 manifest를 정리하는 작업을 주기적으로 실행하는 것이 좋습니다.
.dockerignore와 비밀값 처리
캐시 최적화에서 자주 놓치는 부분은 빌드 컨텍스트입니다. Docker CLI는 현재 디렉터리의 파일을 빌더로 전송한 뒤 빌드를 시작합니다. 로그, 테스트 산출물, 로컬 의존성 폴더가 포함되면 빌드 시작 전 전송 시간이 늘고, 불필요한 파일 변경 때문에 캐시가 깨질 수 있습니다. 다음처럼 보수적으로 제외 목록을 두고, 필요한 파일만 명시적으로 복사하는 습관이 좋습니다.
node_modules
.git
.env
.env.*
coverage
dist
build
target
.DS_Store
*.log
또 하나의 원칙은 비밀값을 이미지 레이어에 남기지 않는 것입니다. private npm registry 토큰, Maven repository 인증 정보, SSH 키를 Dockerfile의 ARG나 ENV로 직접 넣으면 이미지 히스토리나 캐시에 남을 수 있습니다. BuildKit secret mount를 사용해 빌드 중에만 읽고, 결과 레이어에는 복사되지 않도록 구성해야 합니다.
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc --mount=type=cache,target=/root/.npm npm ci --prefer-offline --no-audit
운영 적용 절차
이미 운영 중인 프로젝트에 캐시 전략을 바로 넣을 때는 한 번에 모든 것을 바꾸기보다 병목을 측정한 뒤 단계적으로 적용해야 합니다. 먼저 현재 CI 로그에서 빌드 전체 시간, 의존성 설치 시간, 이미지 push 시간을 분리합니다. 그 다음 Dockerfile 순서를 정리하고, 패키지 매니저 캐시 마운트를 넣고, 마지막으로 원격 캐시를 붙입니다. 각 단계마다 평균 빌드 시간과 실패율을 비교해야 실제 효과를 판단할 수 있습니다.
- 첫 번째 측정: 캐시 적용 전 5회 이상의 CI 빌드 시간을 기록합니다.
- 두 번째 적용: lock 파일 선복사와 .dockerignore 정리를 먼저 반영합니다.
- 세 번째 적용: npm, Maven, Gradle, pip 등 패키지 매니저 캐시 마운트를 추가합니다.
- 네 번째 적용: CI 러너가 임시 환경이면 registry 기반 원격 캐시를 연결합니다.
- 다섯 번째 점검: 캐시 저장소 용량, 오래된 태그 정리, 비밀값 노출 가능성을 확인합니다.
문제 발생 시 점검할 항목
캐시를 붙였는데도 빌드가 빨라지지 않는다면 대개 빌드 컨텍스트가 너무 자주 바뀌거나, lock 파일이 불필요하게 갱신되거나, 캐시 저장소 인증이 실패하는 경우입니다. buildx 로그에서 CACHED 표시가 어디까지 나오는지 확인하고, 의존성 설치 단계가 매번 실행되는지 살펴보면 원인을 좁힐 수 있습니다. 또한 멀티 아키텍처 이미지를 빌드할 때는 amd64와 arm64 캐시가 다르게 쌓일 수 있으므로 플랫폼 값을 고정하거나 캐시 태그를 분리해야 합니다.
마무리 체크리스트
- Dockerfile은 의존성 설치와 소스 복사를 분리했는가?
- .dockerignore가 빌드 컨텍스트 노이즈를 충분히 줄이고 있는가?
- 패키지 매니저 캐시는
RUN --mount=type=cache로 분리했는가? - CI 임시 러너에는
--cache-from,--cache-to원격 캐시를 연결했는가? - 토큰과 SSH 키는 secret mount로만 전달하고 이미지 레이어에 남기지 않았는가?
- 캐시 적용 전후의 평균 빌드 시간과 캐시 저장소 용량을 함께 측정하고 있는가?