이미지 빌드 · 레이어와 캐시 · 이론
빌드 캐시는 왜 그 자리에서 깨지는가
한 줄 요약
각 레이어의 캐시 키는 부모 레이어의 다이제스트 + 자기 명령으로
계산됩니다. 그래서 위에서 하나가 깨지면 그 아래는 내용과 무관하게 전부
다시 실행됩니다.
왜 이게 필요했나
FROM node:22-bookworm-slim # 1WORKDIR /app # 2COPY . . # 3 <- 소스 한 글자만 바뀌어도 여기서 깨진다RUN npm ci # 4 <- 그래서 여기도RUN npm run build # 5 <- 여기도주석 한 줄을 고쳐도 의존성 설치가 처음부터 다시 돕니다. 도커는 명령의
의미를 모르고, 그 자리에 있는 레이어의 부모가 달라졌다는 사실만
압니다.
어떻게 동작하나
명령 종류에 따라 캐시 키 계산 방식이 다릅니다. 이 차이를 모르면 진단이
계속 빗나갑니다.
RUN의 캐시 키는 명령 문자열 그 자체입니다. 무엇을 하는지는 보지COPY/ADD의 캐시 키는 대상 파일들의 내용 해시입니다.
않습니다. 그래서 RUN apt-get update 한 줄은 몇 달이 지나도 영원히
캐시에 적중합니다. apt-get update 와 install 을 같은 RUN 에 붙여야
하는 진짜 이유는 크기가 아니라 캐시입니다.
여기서 오래된 오해를 하나 정정해야 합니다. "파일을 열어 저장(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 가 같은지 확인하고, 앞 레이어를
일부러 깨뜨려 아래가 전부 다시 도는 것을 보고, 삭제로는 이미지가 줄지
않는다는 것을 두 이미지의 크기 차이로 증명합니다.