Docker 레이어 캐싱 최적화 — Dockerfile 작성 순서가 빌드 속도에 미치는 영향에 대해 설명해주세요
문제 상황 요약
최근 CI/CD에서 Docker 이미지 빌드가 지나치게 오래 걸리는 문제가 있었다.
처음에는 단순히 GitHub Actions 러너 성능 문제이거나, 프로젝트 자체가 무거워서 그런 줄 알았다.
그런데 확인해보니 핵심 원인은 Dockerfile 작성 순서에 있었다.
나는 평소처럼 Dockerfile에 소스 전체를 먼저 복사한 뒤, 그 다음에 의존성 설치와 빌드를 수행하도록 작성해두었는데, 이 방식은 코드가 조금만 바뀌어도 Docker가 이전 레이어 캐시를 거의 활용하지 못하게 만든다.
즉, 실제로 자주 바뀌는 것은 소스 코드 일부인데도, 그 위에 있는 npm install, pnpm install, gradle build 같은 무거운 작업이 매번 새로 실행되면서 CI/CD 시간이 불필요하게 길어지고 있었다.
결국 이 문제는 “도커 빌드는 원래 느리다”가 아니라, Dockerfile을 어떻게 썼느냐에 따라 캐시 활용률이 크게 달라진다는 문제였다.
왜 Dockerfile 순서가 중요한가
Docker 이미지는 명령어 단위로 레이어(layer) 가 쌓이는 구조다.
예를 들어 아래처럼 작성하면,
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build각 줄이 하나의 레이어가 된다.
문제는 Docker가 캐시를 사용할 때,
이전 레이어가 하나라도 바뀌면 그 아래 레이어 캐시를 재사용하지 못한다는 점이다.
즉, COPY . . 가 먼저 오면 소스 코드의 아주 사소한 변경만 있어도 그 다음 레이어인 RUN npm install 과 RUN npm run build 가 전부 다시 실행될 수 있다.
특히 프론트엔드 프로젝트처럼 파일 수가 많고 변경이 잦은 환경에서는 이 한 줄 때문에 캐시가 거의 무력화된다.
내가 겪은 실제 문제와 연결해보면
내 경우 CI/CD에서 Docker 이미지를 빌드할 때, 조금만 수정해도 매번 처음부터 다시 빌드되는 느낌이 강했다.
예를 들어 실제 변경은 다음 정도였다.
UI 문구 수정
컴포넌트 스타일 수정
API endpoint 문자열 수정
환경변수 참조 부분 소폭 수정
이 정도면 사실 의존성 자체는 거의 바뀌지 않는다.package.json, pnpm-lock.yaml, build.gradle, settings.gradle 같은 파일은 그대로인 경우가 많다.
그런데 Dockerfile에서 소스 전체를 먼저 복사해버리면, 이런 작은 수정조차도 Docker 입장에서는 “작업 디렉토리 전체가 바뀐 것”처럼 보이기 때문에 뒤에 있는 의존성 설치 레이어가 모두 무효화된다.
결국 CI/CD에서는 이런 일이 반복된다.
소스 코드 한 줄 수정
COPY . . 레이어 변경
그 아래 npm install 또는 gradle build 재실행
이미지 빌드 시간 증가
배포 전체가 느려짐
즉, 느린 원인은 “빌드 서버가 느려서”가 아니라 캐시가 깨지는 구조의 Dockerfile을 써서 무거운 작업을 매번 다시 돌리고 있었기 때문이었다.
잘못된 예시
Node / Next.js 계열에서 흔한 비효율적인 Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["npm", "start"]이 방식의 문제는 너무 명확하다.
소스 전체를 먼저 복사한다
코드 한 줄만 바뀌어도 COPY . . 의 해시가 달라진다
그 아래 RUN npm install 캐시가 깨진다
의존성 설치를 다시 한다
CI/CD 시간이 크게 늘어난다
실제로 자주 바뀌는 것은 src/, components/, pages/, app/ 같은 소스 파일인데, 이 때문에 상대적으로 덜 바뀌는 의존성 설치 단계까지 매번 다시 돌게 되는 것이다.
개선 방법
핵심은 단순하다.
변경 빈도가 낮은 파일부터 먼저 복사하고, 변경 빈도가 높은 파일은 나중에 복사해야 한다.
즉, 의존성 설치에 필요한 파일만 먼저 복사한 뒤 설치를 수행하고, 그 다음에 나머지 소스 전체를 복사하는 방식으로 바꿔야 한다.
개선된 예시
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/package.json /app/package-lock.json ./
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/node_modules ./node_modules
CMD ["npm", "start"]이 구조에서는 다음과 같은 장점이 있다.
package.json, package-lock.json 이 바뀌지 않으면 npm ci 레이어 캐시 재사용 가능 평소처럼 UI나 소스 코드만 수정한 경우에는 의존성 설치를 다시 하지 않아도 됨
결국 빌드 시간이 눈에 띄게 단축됨
내 실제 상황으로 풀어쓴 예시
내가 운영하는 프로젝트에서는 배포할 때 코드 수정이 잦다.
특히 프론트는 문구, UI, 컴포넌트, 스타일, 페이지 구조 등 자잘한 변경이 자주 발생한다.
그런데 만약 Dockerfile이 아래 흐름으로 되어 있으면,
COPY . .
RUN pnpm install
RUN pnpm build프론트에서 버튼 문구 한 줄 바꾸는 정도의 수정만 해도 pnpm install 이 다시 실행될 가능성이 커진다.
하지만 Dockerfile을 아래처럼 바꾸면,
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build이제는 의존성 파일이 바뀌지 않는 한 pnpm install 단계는 캐시를 재사용하게 된다.
즉, 내 실제 CI/CD 문제를 기준으로 보면 문제의 본질은 코드 변경이 아니라, 코드 변경이 의존성 설치 단계까지 다시 실행되게 만든 Dockerfile 구조였다.
왜 COPY . . 를 함부로 위에 두면 안 되는가
COPY . . 는 편해서 자주 쓰게 되지만, 사실상 “프로젝트 전체를 한 번에 캐시에 묶는” 행동이다.
여기에는 다음이 다 포함될 수 있다.
src
public
app
설정 파일
문서 파일
테스트 파일
.env 관련 파일
빌드 결과물
불필요한 로컬 파일
즉, 조금만 파일이 달라져도 해당 레이어가 달라지고, 그 뒤 레이어도 모두 무효화될 수 있다.
그래서 Dockerfile 최적화에서 가장 먼저 보는 포인트가 바로 COPY . . 가 어디에 있느냐다.
.dockerignore 도 같이 중요하다
이 문제는 Dockerfile 순서만의 문제가 아니라
빌드 컨텍스트 자체가 불필요하게 커져도 더 악화된다.
예를 들어 아래 파일들이 전송되면 쓸데없이 캐시가 자주 깨질 수 있다.
node_modules
.next
dist
.git
로그 파일
로컬 개발용 임시 파일
그래서 .dockerignore 를 함께 설정해야 한다.
예시:
node_modules
.next
dist
.git
*.log
.env이렇게 하면 Docker 데몬으로 보내는 컨텍스트도 줄고, 불필요한 파일 때문에 캐시가 깨지는 것도 막을 수 있다.
정리하면서 느낀 점
이번에 느낀 건, Docker 빌드 속도 문제는 단순히 “서버 성능”이나 “프로젝트 규모” 때문이 아니라 레이어 캐시가 살아남을 수 있는 구조로 Dockerfile을 짰는지가 훨씬 중요하다는 점이었다.
예전에는 Dockerfile을 그냥 실행 순서대로만 생각했다.
“복사하고, 설치하고, 빌드하면 되지” 정도로 봤다.
그런데 실제 CI/CD에서 계속 느려지는 걸 겪고 보니 Dockerfile은 단순한 실행 스크립트가 아니라, 캐시 전략까지 포함한 설계 문서에 가깝다는 걸 체감했다.
특히 내 프로젝트처럼 배포가 자주 일어나고, 코드 수정은 잦지만 의존성 변경은 드문 환경에서는 의존성 설치 레이어를 최대한 위에서 안정적으로 캐싱시키는 구조가 정말 중요했다.
결국 핵심은 이것이다.
자주 안 바뀌는 것부터 먼저 복사한다
자주 바뀌는 것은 나중에 복사한다
무거운 작업일수록 캐시가 잘 살아남게 배치한다
이 원리 하나만 제대로 적용해도 CI/CD 체감 속도는 꽤 많이 달라질 수 있다.
댓글
댓글이 없습니다.
