LabHub

이미지 빌드 · 레이어와 캐시 · 이론

빌드 캐시는 왜 그 자리에서 깨지는가

LabHub 에서 이어서 보기

한 줄 요약

각 레이어의 캐시 키는 부모 레이어의 다이제스트 + 자기 명령으로
계산됩니다. 그래서 위에서 하나가 깨지면 그 아래는 내용과 무관하게 전부
다시 실행됩니다.

왜 이게 필요했나

FROM node:22-bookworm-slim   # 1WORKDIR /app                 # 2COPY . .                     # 3  <- 소스 한 글자만 바뀌어도 여기서 깨진다RUN npm ci                   # 4  <- 그래서 여기도RUN npm run build            # 5  <- 여기도

주석 한 줄을 고쳐도 의존성 설치가 처음부터 다시 돕니다. 도커는 명령의
의미를 모르고, 그 자리에 있는 레이어의 부모가 달라졌다는 사실만
압니다.

어떻게 동작하나

명령 종류에 따라 캐시 키 계산 방식이 다릅니다. 이 차이를 모르면 진단이
계속 빗나갑니다.

여기서 오래된 오해를 하나 정정해야 합니다. "파일을 열어 저장(touch)만
해도 캐시가 깨진다"는 말은 구 빌더 시절 이야기입니다. 클래식 빌더는
파일 메타데이터에 수정 시각을 포함했지만, BuildKit 은 내용 해시와 모드,
소유권만 봅니다. CI 에서 git clone 을 새로 해 모든 파일의 mtime 이
현재 시각이 되어도 캐시에 영향이 없습니다. **여전히 캐시가 깨진다면
원인은 mtime 이 아니라 다른 곳에 있습니다.**

그래서 해법은 하나뿐입니다. 자주 바뀌는 것을 아래로 내리는 것.

COPY package.json package-lock.json ./RUN npm ciCOPY . .RUN npm run build

현장에서 만나는 모습

CI 에서만 유독 느린 경우가 있습니다. 로컬에서 두 번째 빌드가 4초인데
CI 는 90초에서 8분으로 늘었고, 로그를 보면 RUN npm ci 하나가 287.4초를
씁니다. 이유는 단순합니다 — **캐시는 빌더의 로컬 스토리지에 있는데 CI
러너는 매 실행마다 새 머신**입니다. 그래서 레지스트리에 캐시를 내보내는
설정이 따로 필요하고, 이때 mode=max 를 빼면 최종 스테이지만 캐시되어
정작 비싼 빌더 스테이지가 매번 다시 돕니다.

.dockerignore 도 오해가 많습니다. **이것은 이미지 크기 최적화 도구가
아니라 전송량 최적화 도구**입니다. 실측을 보면 컨텍스트 전송이
612.44MB / 18.3초에서 3.71MB / 0.4초로 줄었습니다. 다만 COPY . .
쓴다면 이미지 크기에도 영향이 가고, .env 를 제외하는 이유는 크기가
아니라 보안입니다.

다음 실습에서 할 것

같은 소스로 두 번 빌드해 이미지 ID 가 같은지 확인하고, 앞 레이어를
일부러 깨뜨려 아래가 전부 다시 도는 것을 보고, 삭제로는 이미지가 줄지
않는다는 것을 두 이미지의 크기 차이로 증명합니다.