- Published on
Jenkins 빌드가 갑자기 느릴 때 Docker 레이어 캐시 복구
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론
Jenkins 파이프라인이 평소엔 35분이면 끝나던 Docker 빌드가 어느 날 갑자기 2030분씩 걸리기 시작하면, 가장 먼저 의심해야 할 것은 Docker 레이어 캐시(layer cache) 입니다. 로그를 보면 늘 보이던 CACHED가 사라지고 매번 RUN apt-get ..., npm ci, pip install 같은 무거운 단계가 처음부터 다시 실행됩니다.
문제는 “왜 느려졌는지”보다 “왜 캐시가 더 이상 적용되지 않는지”를 정확히 찔러야 재발을 막을 수 있다는 점입니다. 이 글에서는 Jenkins 환경에서 캐시가 깨지는 전형적인 패턴을 빠르게 진단하고, BuildKit + registry cache 또는 로컬 캐시 유지 전략으로 레이어 캐시를 복구하는 방법을 다룹니다.
> 참고: 빌드가 느려진 원인이 네트워크/권한 문제(예: S3, STS)로 인한 타임아웃일 수도 있습니다. 그런 경우는 로그에 403/timeout이 같이 보이므로 별도 점검이 필요합니다. 예: AWS S3 AccessDenied 403 - 정책·KMS·VPCE 점검
1) “캐시가 안 먹는다”의 증상 체크리스트
아래 중 2개 이상이면 레이어 캐시 문제일 확률이 높습니다.
- Jenkins 에이전트가 매 빌드마다 새로 뜨는 ephemeral 컨테이너/노드(Kubernetes plugin, EC2 spot 등)로 바뀜
docker build로그에서Using cache(legacy) 또는CACHED(BuildKit)가 거의 보이지 않음- Dockerfile 상단의
COPY . .이후 단계가 매번 전부 재실행 --no-cache옵션이 파이프라인 어딘가에 들어가 있음(의외로 흔함)- 베이스 이미지 태그가
latest같은 가변 태그이고, pull 정책 때문에 매번 바뀜 - 빌드 컨텍스트가 커졌거나(불필요 파일 포함),
.dockerignore가 비어 있음
2) 캐시가 깨지는 대표 원인 7가지
2.1 Jenkins 에이전트가 매번 새 머신(새 Docker 데몬)
Docker 레이어 캐시는 기본적으로 같은 Docker 데몬의 로컬 스토리지에 쌓입니다. Jenkins가 Kubernetes에서 pod로 에이전트를 띄우고, 빌드가 끝나면 pod가 삭제되는 구조라면 캐시는 매번 초기화됩니다.
- 해결 방향: 원격 캐시(Registry cache)로 이동하거나, 빌드 노드를 고정/볼륨 마운트해서 로컬 캐시를 유지합니다.
2.2 Dockerfile의 레이어 순서가 캐시에 불리함
예를 들어 Node 프로젝트에서 다음처럼 작성하면 소스 변경 때마다 의존성 설치가 다시 돕니다.
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
COPY . .가 먼저라서 파일 하나만 바뀌어도 npm ci 레이어가 무효화됩니다.
- 해결: lock 파일만 먼저 복사 → 의존성 설치 → 소스 복사 순으로 재배치합니다.
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
2.3 빌드 컨텍스트가 커짐 + .dockerignore 부재
테스트 리포트, .git, 로컬 캐시(node_modules, .venv, dist) 등이 컨텍스트에 포함되면, 작은 변경에도 컨텍스트 해시가 바뀌어 캐시 적중률이 떨어집니다.
.dockerignore 최소 예시:
.git
node_modules
.dist
build
coverage
*.log
.DS_Store
2.4 latest/가변 태그로 베이스 이미지가 흔들림
FROM ubuntu:latest는 pull 타이밍에 따라 이미지 digest가 바뀌고, 그 순간부터 아래 레이어가 전부 재빌드될 수 있습니다.
- 해결: 가능하면 digest pinning
FROM ubuntu@sha256:...
2.5 멀티스테이지에서 불필요한 파일 복사
멀티스테이지 빌드에서 COPY --from=builder /app /app처럼 크게 복사하면, 빌더 단계 산출물 변경이 자주 발생해 캐시 무효화가 잦습니다. 산출물만 좁혀 복사하세요.
COPY /app/dist ./dist
2.6 BuildKit 비활성화 / builder 인스턴스 변경
Jenkins 노드마다 BuildKit 설정이 제각각이거나, docker buildx builder가 매번 새로 생성되면 캐시 전략이 흔들립니다.
2.7 캐시 purge/GC가 과격함
운영 중 디스크 부족으로 docker system prune -af 같은 작업이 주기적으로 돌면 캐시가 통째로 날아갑니다.
3) 가장 확실한 해법: BuildKit + Registry Cache
에이전트가 ephemeral이라도 캐시를 유지하려면 레지스트리에 캐시를 내보내고(import/export) 다시 가져오는 방식이 가장 재현성이 좋습니다.
핵심은 아래 3가지입니다.
docker buildx build사용--cache-to type=registry로 캐시 푸시- 다음 빌드에서
--cache-from type=registry로 캐시 풀
3.1 Jenkinsfile 예시 (Buildx + registry cache)
아래 예시는 ECR/사설 registry 어디든 적용 가능합니다(인증은 별도).
pipeline {
agent any
environment {
IMAGE = "registry.example.com/myteam/myapp"
CACHE = "registry.example.com/myteam/myapp:buildcache"
}
stages {
stage('Prepare buildx') {
steps {
sh '''
set -e
docker version
docker buildx version
# 고정된 builder를 사용(없으면 생성)
if ! docker buildx inspect jenkins-builder >/dev/null 2>&1; then
docker buildx create --name jenkins-builder --use
else
docker buildx use jenkins-builder
fi
docker buildx inspect --bootstrap
'''
}
}
stage('Build & Push (with cache)') {
steps {
sh '''
set -e
GIT_SHA=$(git rev-parse --short HEAD)
docker buildx build \
--progress=plain \
--tag ${IMAGE}:${GIT_SHA} \
--tag ${IMAGE}:latest \
--cache-from type=registry,ref=${CACHE} \
--cache-to type=registry,ref=${CACHE},mode=max \
--push \
.
'''
}
}
}
}
포인트
mode=max는 가능한 많은 중간 레이어를 캐시에 저장해 적중률을 올립니다.--progress=plain은 Jenkins 로그에서 캐시 적중 여부를 확인하기 좋습니다.--push를 켜야 registry cache 전략이 매끄럽습니다(로컬에만 남기지 않음).
4) Kubernetes Jenkins 에이전트에서 캐시를 유지하는 2가지 대안
레지스트리 캐시가 가장 범용적이지만, 환경에 따라 로컬 캐시를 살리는 편이 더 빠를 때도 있습니다.
4.1 Docker-in-Docker(dind) + PVC로 /var/lib/docker 유지
Kubernetes Jenkins agent pod에 dind를 띄우고 /var/lib/docker를 PVC에 붙이면 레이어 캐시가 유지됩니다.
주의점:
- 보안/권한(Privileged) 요구
- 노드/스토리지 성능에 따라 오히려 느려질 수 있음
4.2 Kaniko/BuildKit daemonless + 캐시 볼륨
클러스터 정책상 privileged가 불가능하면 Kaniko의 --cache=true + cache repo를 쓰거나, BuildKit(rootless) 구성을 고려합니다.
다만 Jenkins에서 운영 난이도는 registry cache 방식이 가장 단순합니다.
5) Dockerfile 캐시 적중률을 올리는 실전 패턴
5.1 Node/React/Next.js
package-lock.json/pnpm-lock.yaml/yarn.lock를 먼저 복사npm ci는 lock 기반이라 캐시 친화적
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:20-alpine AS build
WORKDIR /app
COPY /app/node_modules ./node_modules
COPY . .
RUN npm run build
> Next.js 계열은 빌드 캐시/서버액션 캐시가 별도로 꼬일 때도 있습니다. 애플리케이션 레벨 캐시 문제라면 Next.js 14 서버액션 500·CSRF·캐시 꼬임 해결도 함께 점검하세요.
5.2 Python
requirements.txt/poetry.lock먼저 복사- wheel 캐시를 BuildKit cache mount로 가속
# syntax=docker/dockerfile:1.6
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt ./
RUN \
pip install -r requirements.txt
COPY . .
CMD ["python", "main.py"]
6) “캐시가 적용되는지” 로그로 검증하는 방법
6.1 BuildKit 로그에서 CACHED 확인
--progress=plain을 켠 상태에서 다음을 확인합니다.
#x [stage ...] CACHEDimporting cache manifest from ...buildcache
캐시가 전혀 import되지 않는다면:
- registry 인증 실패
- cache ref 잘못됨
- buildx builder가 다른 드라이버를 사용
6.2 레이어가 계속 깨지는 지점 찾기
캐시가 깨지는 “첫 번째 레이어”를 찾으면 원인이 거의 드러납니다.
COPY . .가 첫 번째라면.dockerignore/컨텍스트 문제RUN apt-get update부터 다시라면 베이스 이미지 변경 또는ARG변경RUN npm ci부터 다시라면 lock 파일 변경 또는 이전 레이어 무효화
7) 자주 하는 실수와 예방책
7.1 ARG를 너무 앞에 두기
ARG BUILD_NUMBER 같은 값은 매 빌드마다 바뀌므로, Dockerfile 상단에 있으면 이후 레이어가 전부 무효화됩니다.
- 해결: 정말 필요한 지점 직전에만
ARG사용
7.2 빌드 시간 최적화와 런타임 최적화를 혼동
캐시를 살리는 목적은 “빌드 시간”을 줄이는 것입니다. 런타임 이미지 크기 최적화(멀티스테이지, slim)는 별개로 하되, 빌드 단계에서 의존성 설치 레이어가 잘 캐시되도록 설계해야 합니다.
7.3 디스크 부족으로 인한 캐시 손실
Jenkins 노드에서 디스크가 부족하면 Docker가 GC를 하거나 운영자가 prune을 돌립니다. 빌드가 갑자기 느려진 날에 노드 이벤트/알람(디스크 80%↑)이 있었는지 확인하세요.
클러스터에서 노드 리소스 압박이 원인이라면, 파드 스케줄링/리소스 이슈도 함께 점검해야 합니다. 예: EKS Pod Pending(Insufficient cpu) 원인과 해결
8) 결론: “에이전트가 바뀌어도 캐시가 남는 구조”로 고정하라
Jenkins 빌드가 갑자기 느려졌을 때 Docker 레이어 캐시를 복구하는 가장 확실한 접근은 다음 순서입니다.
- 로그에서 캐시가 사라진 지점(첫 무효 레이어)을 찾는다.
- Dockerfile을 캐시 친화적으로 재구성한다(
COPY순서,.dockerignore, 가변 태그 제거). - Jenkins 에이전트가 ephemeral이면 BuildKit + registry cache로 캐시를 외부화한다.
- 디스크/GC/prune 같은 운영 요인으로 캐시가 날아가지 않도록 관측/정책을 정한다.
이렇게 구성하면 “갑자기 느려지는” 상황 자체가 줄어들고, 느려지더라도 원인 파악이 빨라져 CI의 예측 가능성이 크게 올라갑니다.