GitHub Actions Docker 이미지 빌드 캐시 레이어 재사용 안 될 때 원인과 해결법

CI/CD 파이프라인에서 Docker 빌드가 10분씩 걸린다면, 높은 확률로 캐시가 제대로 작동하지 않는 거예요. 설정은 다 해놨는데 매번 처음부터 빌드되는 그 답답함, 맞죠?
핵심 요약
- GitHub Actions의
cache-from/cache-to설정을 해도 캐시가 무효화되는 주된 원인은 Dockerfile 레이어 순서 문제와cache-key불일치 두 가지다.actions/cache와docker/build-push-action의 내장 캐시 방식은 동작 원리가 다르기 때문에 혼용하면 캐시 히트율이 급격히 떨어진다.- Docker BuildKit의
--cache-from type=gha방식은 GitHub Actions 네이티브 캐시 스토리지를 쓰기 때문에 별도 레지스트리 없이도 레이어 재사용이 가능하다.- 캐시 레이어 재사용률을 측정하려면
docker buildx build --progress=plain출력에서CACHED라인 수를 확인하는 게 가장 빠른 방법이다.
설정은 했는데 왜 매번 새로 빌드될까?
GitHub Actions에서 Docker 이미지를 빌드할 때 캐시 레이어가 재사용되지 않는 문제는 생각보다 흔해요. 단순히 “캐시 설정이 없어서"가 아니라, 설정은 했는데도 캐시가 무효화되는 케이스가 대부분이거든요.
핵심 원인은 세 가지로 좁혀져요.
첫 번째: Dockerfile 레이어 순서.
Docker는 위에서 아래로 순차적으로 레이어를 빌드하고, 특정 레이어가 바뀌면 그 아래 레이어는 전부 무효화돼요. COPY . . 명령어를 의존성 설치보다 앞에 두면, 소스 파일이 1줄만 바뀌어도 npm install이나 pip install을 처음부터 다시 실행하는 셈이에요.
두 번째: cache-key 해시 불일치.
actions/cache를 쓸 때 key에 ${{ github.sha }}를 넣으면 커밋마다 키가 달라지니까 캐시가 절대 히트하지 않아요. 캐시 키에는 package-lock.json이나 requirements.txt의 해시를 써야 해요. 의존성 파일이 안 바뀌면 캐시도 유지되니까요.
세 번째: cache-from 타입 혼용.
docker/build-push-action v2 이후 내장 캐시(type=gha)와 외부 레지스트리 캐시(type=registry)를 동시에 쓰거나, 구버전 액션의 캐시 방식과 섞어 쓰면 캐시 스토리지를 제대로 찾지 못해요.
참고로, GitHub Actions 자체 캐시 스토리지 한도는 리포지토리당 10GB예요. 이 한도를 넘으면 오래된 캐시부터 자동으로 지워지기 때문에 캐시가 있다가 없어지는 현상이 발생하기도 해요. GitHub 공식 문서에 따르면 캐시 항목은 7일 동안 접근이 없으면 자동 삭제돼요.
레이어 캐시가 실제로 어떻게 작동하는지
Docker 빌드 캐시는 레이어 단위로 동작해요. 각 Dockerfile 명령어(RUN, COPY, ADD)가 하나의 레이어를 만들고, Docker는 이 레이어의 체크섬을 비교해서 변경 여부를 판단하죠.
GitHub Actions에서 이 레이어를 다음 빌드에서 재사용하려면 캐시를 어딘가에 저장해야 해요. 방법은 크게 두 가지예요.
type=gha (GitHub Actions 네이티브 캐시)
BuildKit이 GitHub의 캐시 스토리지 API를 직접 호출해요. 별도 레지스트리가 필요 없고, 설정도 제일 간단해요. 단, GitHub 캐시 API 속도에 의존하기 때문에 대형 이미지에서는 느릴 수 있어요.
type=registry (컨테이너 레지스트리 캐시)
Docker Hub, AWS ECR, GitHub Container Registry 같은 레지스트리에 캐시 매니페스트를 별도로 저장해요. 속도는 빠르지만 레지스트리 권한 설정과 비용이 추가로 필요해요.
캐시 방식 비교
| 항목 | type=gha | type=registry | actions/cache (구방식) |
|---|---|---|---|
| 설정 난이도 | 낮음 | 중간 | 낮음 |
| 추가 인프라 | 불필요 | 레지스트리 필요 | 불필요 |
| 캐시 히트 속도 | 중간 | 빠름 | 중간 |
| 대용량 이미지 | 한계 있음 (10GB 총량) | 레지스트리 용량에 따라 | 압축 오버헤드 있음 |
| 브랜치 간 공유 | 기본 브랜치만 공유 | 가능 | 제한적 |
| 권장 사용 | 중소 규모 프로젝트 | 대형 이미지, 팀 공유 | 레거시 / 비권장 |
캐시 방식 선택은 이미지 크기와 팀 규모에 달려 있어요. 1GB 미만 이미지라면 type=gha로 충분하고, 그 이상이라면 레지스트리 캐시가 더 안정적이에요.
Dockerfile 레이어 순서 최적화 — 실제로 어떻게 바꾸나
잘못된 패턴부터 볼게요.
# ❌ 이렇게 하면 안 돼요
COPY . .
RUN npm install
소스 파일이 조금이라도 바뀌면 npm install을 처음부터 실행해요. 10분 빌드의 주범이에요.
# ✅ 이렇게 바꾸면 돼요
COPY package.json package-lock.json ./
RUN npm install
COPY . .
package-lock.json이 안 바뀌면 npm install 레이어는 캐시에서 그대로 가져와요. 이것만 바꿔도 빌드 시간이 10분에서 1-2분으로 줄어드는 케이스가 많아요.
workflow 파일 올바른 설정 예시
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ env.IMAGE_TAG }}
cache-from: type=gha
cache-to: type=gha,mode=max
mode=max를 빠뜨리면 최종 레이어만 캐시되고 중간 레이어는 버려져요. 멀티 스테이지 빌드를 쓴다면 반드시 mode=max를 붙여야 모든 스테이지의 레이어가 저장돼요.
캐시가 안 되는 상황별 진단법
증상 1: 매 빌드마다 CACHED 라인이 전혀 안 보임
→ cache-from 설정이 없거나, 캐시 키가 매번 달라지는 경우예요. --progress=plain 옵션으로 빌드 로그를 자세히 보면 어느 레이어부터 캐시 미스가 나는지 바로 확인할 수 있어요.
증상 2: 처음 빌드는 캐시가 되는데 다른 브랜치에서는 안 됨
→ GitHub Actions 캐시는 기본적으로 기본 브랜치(main)의 캐시만 모든 브랜치가 읽을 수 있어요. 피처 브랜치에서 생성된 캐시는 해당 브랜치 내에서만 유효해요. restore-keys를 써서 폴백 키를 설정하면 이 문제를 우회할 수 있어요.
증상 3: 로컬에서는 캐시가 잘 되는데 CI에서만 안 됨
→ 로컬과 CI의 BuildKit 버전 차이, 또는 DOCKER_BUILDKIT=1 환경변수가 CI에서 설정 안 된 경우예요. docker/setup-buildx-action을 워크플로우 초반에 실행하면 BuildKit이 확실히 활성화돼요.
실제 파이프라인에서 어떻게 접근할까
소규모 팀 또는 사이드 프로젝트:
type=gha + Dockerfile 레이어 순서 정리만 해도 충분해요. 설정 두 줄에 빌드 시간이 절반 이하로 줄어드는 경우가 많아요.
프로덕션 레벨 팀:
멀티 스테이지 빌드와 type=registry를 같이 쓰세요. 빌드 스테이지(builder)와 런타임 스테이지를 분리하면 캐시할 레이어가 늘어나고, 최종 이미지는 작아져요. AWS ECR이나 GitHub Container Registry에 -cache 태그로 캐시 매니페스트를 따로 관리하는 게 팀 간 캐시 공유에도 유리해요.
지켜볼 신호 세 가지:
- GitHub이 캐시 API 속도 개선을 예고한 상태라,
type=gha의 성능 한계가 완화될 수 있어요. - BuildKit의
--cache-export/--cache-import명세는 계속 확장 중이에요. OCI 표준 캐시 포맷이 확정되면 레지스트리 간 이식성이 훨씬 좋아질 거예요. docker/build-push-actionv6 계열에서 캐시 키 자동 생성 로직이 바뀌는 경우가 있으니 버전을 올릴 때는 캐시 히트율을 다시 확인하는 게 좋아요.
요약: 캐시 문제, 어디서부터 손대야 할까
핵심을 세 줄로 정리하면 이래요.
- Dockerfile 레이어 순서부터 고치세요. 변경 빈도가 낮은 것(의존성)이 먼저, 자주 바뀌는 것(소스 코드)이 나중이에요.
cache-from type=gha, mode=max를 반드시 붙이세요. 이 한 줄이 없으면 중간 레이어가 전부 버려져요.- 캐시 키에
github.sha쓰지 마세요. 의존성 파일의 해시를 키로 써야 캐시가 살아남아요.
캐시 히트율이 올라가면 빌드 시간만 줄어드는 게 아니에요. GitHub Actions 과금도 분 단위라, 한 달 빌드 횟수가 수백 번이면 비용 차이가 꽤 나요.
그럼 지금 당장 --progress=plain으로 빌드 로그를 열어서 CACHED가 몇 줄이나 뜨는지 세어보세요. 그 숫자가 현재 캐시 상태를 가장 직접적으로 보여줘요.
참고자료
- GitHub Actions에서 Docker Build 캐싱 적용하기
- Github Actions Docker 빌드 속도를 최적화 해보자.
- Docker 이미지 빌드 빠르게 하는 방법 (10분 걸리던게 1분에 완료!)


