🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Dockerfile 최적화 기법 완벽 가이드 - 슬라이드 1/13
A

AI Generated

2025. 10. 30. · 24 Views

Dockerfile 최적화 기법 완벽 가이드

Docker 이미지 크기를 줄이고 빌드 속도를 높이는 실전 최적화 기법을 소개합니다. 멀티 스테이지 빌드, 레이어 캐싱, 베이스 이미지 선택 등 실무에서 바로 적용할 수 있는 9가지 핵심 기법을 다룹니다.


목차

  1. 멀티 스테이지 빌드 - 이미지 크기를 획기적으로 줄이는 핵심 기법
  2. 레이어 캐싱 최적화 - 빌드 속도를 10배 빠르게 만드는 비결
  3. 베이스 이미지 선택 전략 - 이미지 크기를 90% 줄이는 선택
  4. 빌드 아규먼트와 환경 변수 활용 - 하나의 Dockerfile로 다양한 환경 대응
  5. .dockerignore 활용 - 빌드 컨텍스트를 80% 줄이는 필수 파일
  6. RUN 명령 최적화 - 레이어 수를 줄여 이미지 효율 높이기
  7. ENTRYPOINT와 CMD 조합 - 유연한 컨테이너 실행 설계
  8. 헬스체크 설정 - 컨테이너 상태를 자동으로 모니터링하기
  9. 빌드 캐시 무효화 전략 - 외부 데이터를 최신으로 유지하기

1. 멀티 스테이지 빌드 - 이미지 크기를 획기적으로 줄이는 핵심 기법

시작하며

여러분이 Node.js 애플리케이션을 Docker 이미지로 빌드했는데 이미지 크기가 1GB가 넘어가는 상황을 겪어본 적 있나요? 프로덕션에는 실행 파일만 필요한데, 빌드 도구와 개발 의존성까지 모두 포함되어 있어서 배포 시간이 길어지고 스토리지 비용이 증가합니다.

이런 문제는 실제 개발 현장에서 매우 자주 발생합니다. 빌드 과정에서 필요한 컴파일러, 빌드 도구, 테스트 라이브러리 등이 최종 이미지에 그대로 남아있기 때문입니다.

이는 불필요한 보안 취약점을 노출시키고, 네트워크 대역폭을 낭비하며, 배포 속도를 저하시킵니다. 바로 이럴 때 필요한 것이 멀티 스테이지 빌드입니다.

빌드 단계와 실행 단계를 분리하여 최종 이미지에는 실행에 필요한 파일만 포함시킬 수 있습니다.

개요

간단히 말해서, 멀티 스테이지 빌드는 하나의 Dockerfile에서 여러 개의 FROM 문을 사용하여 빌드 단계를 나누는 기법입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, 애플리케이션을 빌드할 때는 컴파일러, 빌드 도구, 개발 의존성이 필요하지만 실행할 때는 컴파일된 바이너리나 빌드된 파일만 있으면 됩니다.

예를 들어, Go 애플리케이션을 빌드할 때는 Go 컴파일러가 필요하지만, 컴파일된 바이너리를 실행할 때는 Go 런타임조차 필요 없습니다. 기존에는 빌드용 Dockerfile과 실행용 Dockerfile을 따로 만들거나, 빌드 후 수동으로 파일을 복사하는 복잡한 스크립트를 작성했다면, 이제는 하나의 Dockerfile에서 단계별로 정의하고 필요한 파일만 선택적으로 복사할 수 있습니다.

멀티 스테이지 빌드의 핵심 특징은 첫째, 각 FROM 문이 새로운 빌드 단계를 시작한다는 점, 둘째, 이전 단계의 파일을 COPY --from으로 선택적으로 가져올 수 있다는 점, 셋째, 최종 이미지는 마지막 단계만 포함된다는 점입니다. 이러한 특징들이 이미지 크기를 80% 이상 줄이고 보안성을 높이는 이유입니다.

코드 예제

# 빌드 단계: 전체 개발 환경 포함
FROM node:18 AS builder
WORKDIR /app
# 의존성 설치
COPY package*.json ./
RUN npm ci
# 소스 코드 복사 및 빌드
COPY . .
RUN npm run build

# 실행 단계: 빌드된 파일만 포함
FROM node:18-alpine
WORKDIR /app
# 빌드 단계에서 필요한 파일만 복사
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
CMD ["node", "dist/index.js"]

설명

이것이 하는 일: 이 Dockerfile은 Node.js 애플리케이션을 빌드하고 실행하는 과정을 두 단계로 나누어, 최종 이미지에는 빌드된 결과물만 포함시킵니다. 첫 번째로, builder라는 이름의 빌드 단계에서는 node:18 이미지를 사용하여 전체 개발 환경을 구성합니다.

npm ci로 의존성을 설치하고 npm run build로 TypeScript를 JavaScript로 컴파일합니다. 이 단계에서는 개발 의존성, 소스 코드, 빌드 도구 등이 모두 포함되어 이미지 크기가 큽니다.

하지만 이 단계는 최종 이미지에 포함되지 않으므로 문제가 되지 않습니다. 그 다음으로, 두 번째 FROM 문에서 node:18-alpine이라는 더 작은 베이스 이미지를 사용하여 새로운 단계를 시작합니다.

COPY --from=builder 명령으로 이전 단계에서 빌드된 dist 폴더와 프로덕션 의존성만 선택적으로 가져옵니다. 이 단계에서는 소스 코드나 개발 도구가 포함되지 않습니다.

마지막으로, CMD 명령으로 컴파일된 JavaScript 파일을 실행하도록 설정하여 최종적으로 실행에 필요한 파일만 담긴 최적화된 이미지를 만들어냅니다. 원본 이미지가 1.2GB였다면, 최종 이미지는 150MB 정도로 줄어듭니다.

여러분이 이 코드를 사용하면 배포 시간이 5분에서 1분으로 단축되고, 네트워크 비용이 절감되며, 컨테이너 시작 속도가 빨라지는 효과를 얻을 수 있습니다. 또한 불필요한 빌드 도구가 포함되지 않아 보안 취약점이 줄어들고, 이미지 스캔 시간도 단축됩니다.

실전 팁

💡 AS 키워드로 각 단계에 의미있는 이름을 붙이면 나중에 COPY --from으로 참조하기 쉽고 Dockerfile의 가독성이 높아집니다

💡 프로덕션 의존성만 설치하려면 npm ci --only=production을 사용하여 devDependencies를 제외하세요. 많은 개발자들이 전체 node_modules를 복사하는 실수를 합니다

💡 중간 단계를 디버깅하려면 docker build --target=builder -t myapp:debug . 명령으로 특정 단계까지만 빌드할 수 있습니다

💡 여러 단계에서 공통으로 사용하는 의존성이 있다면 별도의 단계로 분리하여 캐싱 효율을 높이세요

💡 최종 단계의 베이스 이미지는 가능한 한 alpine이나 distroless 이미지를 사용하여 크기를 최소화하세요


2. 레이어 캐싱 최적화 - 빌드 속도를 10배 빠르게 만드는 비결

시작하며

여러분이 코드 한 줄만 수정했는데도 Docker 빌드가 처음부터 다시 시작되어 10분씩 기다려야 하는 경험을 해보셨나요? 특히 npm install이나 pip install 같은 의존성 설치 단계가 매번 반복되면서 개발 속도가 현저히 느려집니다.

이런 문제는 Dockerfile의 명령어 순서를 잘못 배치했을 때 발생합니다. Docker는 레이어 단위로 캐싱을 하는데, 한 레이어가 변경되면 그 이후의 모든 레이어가 무효화됩니다.

소스 코드가 변경될 때마다 의존성 설치부터 다시 하게 되는 것입니다. 바로 이럴 때 필요한 것이 레이어 캐싱 최적화입니다.

자주 변경되지 않는 명령을 앞에 배치하여 캐시 적중률을 최대화할 수 있습니다.

개요

간단히 말해서, 레이어 캐싱 최적화는 Dockerfile의 명령어를 변경 빈도에 따라 배치하여 Docker의 빌드 캐시를 최대한 활용하는 기법입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, Docker는 각 명령어를 실행할 때마다 레이어를 생성하고 해시값으로 캐싱합니다.

명령어와 파일 내용이 동일하면 캐시를 재사용하지만, 하나라도 변경되면 그 시점부터 모든 캐시가 무효화됩니다. 예를 들어, package.json이 변경되지 않았다면 npm install을 다시 실행할 필요가 없지만, COPY .

. 다음에 npm install을 하면 소스 코드 변경 시마다 의존성을 재설치하게 됩니다.

기존에는 모든 파일을 한 번에 복사한 후 빌드를 진행했다면, 이제는 의존성 파일만 먼저 복사하여 설치하고, 그 다음에 소스 코드를 복사하는 순서로 진행할 수 있습니다. 레이어 캐싱 최적화의 핵심 특징은 첫째, 변경이 적은 파일을 먼저 COPY한다는 점, 둘째, 의존성 설치와 소스 코드 빌드를 분리한다는 점, 셋째, .dockerignore로 불필요한 파일을 제외한다는 점입니다.

이러한 특징들이 빌드 시간을 10분에서 1분으로 단축시키는 핵심입니다.

코드 예제

FROM python:3.11-slim
WORKDIR /app

# 1단계: 의존성 파일만 먼저 복사 (변경 빈도 낮음)
COPY requirements.txt .
# 캐시 가능: requirements.txt가 변경되지 않으면 재사용
RUN pip install --no-cache-dir -r requirements.txt

# 2단계: 소스 코드는 나중에 복사 (변경 빈도 높음)
COPY src/ ./src/
COPY config/ ./config/

# 3단계: 애플리케이션 설정 파일 복사
COPY app.py .

# 실행 명령
CMD ["python", "app.py"]

설명

이것이 하는 일: 이 Dockerfile은 파일을 변경 빈도에 따라 단계적으로 복사하여 Docker의 레이어 캐싱을 효율적으로 활용합니다. 첫 번째로, requirements.txt만 먼저 복사하고 pip install을 실행합니다.

이 레이어는 requirements.txt가 변경되지 않는 한 캐시에서 재사용됩니다. 실무에서 의존성은 한 달에 한두 번 정도만 변경되지만, 소스 코드는 하루에도 수십 번 변경되므로 이 분리가 매우 중요합니다.

만약 모든 파일을 한 번에 복사했다면 소스 코드 변경 시마다 의존성을 재설치해야 합니다. 그 다음으로, 소스 코드 디렉토리를 복사합니다.

src/와 config/ 디렉토리를 개별적으로 복사하여, 특정 디렉토리만 변경되었을 때 다른 디렉토리의 캐시는 유지될 수 있도록 합니다. 예를 들어 config/ 파일만 수정했다면 src/ 복사 레이어는 캐시에서 가져올 수 있습니다.

마지막으로, 메인 애플리케이션 파일을 복사하여 최종적으로 실행 가능한 이미지를 완성합니다. 이 순서대로 배치하면 대부분의 경우 의존성 설치 단계가 캐시되어 빌드 시간이 10분에서 30초로 단축됩니다.

여러분이 이 코드를 사용하면 개발 중 빌드 대기 시간이 획기적으로 줄어들고, CI/CD 파이프라인의 실행 시간이 단축되며, 개발자의 생산성이 크게 향상되는 효과를 얻을 수 있습니다. 특히 팀 환경에서는 하루에 수백 번의 빌드가 발생하므로 누적 시간 절감 효과가 엄청납니다.

실전 팁

💡 .dockerignore 파일을 반드시 작성하여 node_modules, .git, pycache 같은 불필요한 파일을 제외하세요. 이런 파일들이 포함되면 캐시가 자주 무효화됩니다

💡 package.json과 package-lock.json을 함께 복사하세요. lock 파일이 있어야 의존성 버전이 고정되어 캐시가 안정적으로 작동합니다

💡 COPY 명령은 가능한 한 구체적으로 작성하세요. COPY . . 대신 COPY src/ ./src/ 처럼 디렉토리별로 나누면 부분 캐싱이 가능합니다

💡 RUN 명령을 여러 개로 나누면 각각 캐시되지만, 너무 많이 나누면 레이어 수가 증가하여 이미지 크기가 커질 수 있으므로 균형을 맞추세요

💡 docker build --progress=plain 옵션으로 어느 레이어가 캐시되고 어느 레이어가 다시 빌드되는지 확인하여 최적화 포인트를 찾으세요


3. 베이스 이미지 선택 전략 - 이미지 크기를 90% 줄이는 선택

시작하며

여러분이 간단한 Python Flask 앱을 Docker로 배포했는데 이미지 크기가 900MB가 넘어가는 상황을 보셨나요? 단순히 FROM python:3.11을 사용했을 뿐인데, 불필요한 시스템 라이브러리와 개발 도구들이 모두 포함되어 있습니다.

이런 문제는 베이스 이미지 선택을 신중하게 하지 않았을 때 발생합니다. 공식 이미지의 기본 태그는 대부분 full 버전으로, 개발과 빌드에 필요한 모든 도구를 포함하고 있습니다.

프로덕션 환경에서는 이런 도구들이 불필요한데도 말이죠. 바로 이럴 때 필요한 것이 베이스 이미지 선택 전략입니다.

alpine, slim, distroless 같은 경량화된 이미지를 선택하여 크기와 보안을 동시에 최적화할 수 있습니다.

개요

간단히 말해서, 베이스 이미지 선택 전략은 애플리케이션의 요구사항에 맞는 가장 작은 베이스 이미지를 선택하여 최종 이미지 크기를 최소화하는 기법입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, Docker 이미지 크기는 배포 속도, 네트워크 비용, 스토리지 비용, 보안 취약점에 직접적인 영향을 미칩니다.

python:3.11은 920MB인 반면, python:3.11-slim은 125MB, python:3.11-alpine은 50MB에 불과합니다. 예를 들어, 마이크로서비스 아키텍처에서 10개의 서비스를 배포한다면 이미지 크기 차이가 누적되어 8GB 대 500MB의 차이가 발생합니다.

기존에는 공식 이미지의 기본 태그를 그대로 사용했다면, 이제는 slim, alpine, distroless 같은 최적화된 베이스 이미지를 선택할 수 있습니다. 베이스 이미지 선택의 핵심 특징은 첫째, slim 이미지는 개발 도구를 제외하고 런타임만 포함한다는 점, 둘째, alpine 이미지는 경량 Linux 배포판을 사용하여 크기를 최소화한다는 점, 셋째, distroless 이미지는 쉘조차 포함하지 않아 보안성이 극대화된다는 점입니다.

이러한 특징들이 이미지 크기를 90% 줄이고 공격 표면을 최소화하는 이유입니다.

코드 예제

# 좋지 않은 예: 920MB
# FROM python:3.11

# 더 나은 예: 125MB (slim - 런타임만 포함)
# FROM python:3.11-slim

# 최적 예: 50MB (alpine - 최소 경량 이미지)
FROM python:3.11-alpine

WORKDIR /app

# alpine에서 필요한 빌드 의존성 설치
RUN apk add --no-cache gcc musl-dev linux-headers

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 빌드 의존성 제거 (프로덕션에 불필요)
RUN apk del gcc musl-dev linux-headers

COPY . .
CMD ["python", "app.py"]

설명

이것이 하는 일: 이 Dockerfile은 alpine 베이스 이미지를 사용하여 최소한의 시스템 구성 요소만 포함하면서도 Python 애플리케이션을 정상적으로 실행합니다. 첫 번째로, python:3.11-alpine을 베이스 이미지로 선택합니다.

이 이미지는 Alpine Linux라는 경량 배포판을 사용하며, busybox 기반의 최소한의 유틸리티만 포함되어 있습니다. 일반 python:3.11이 920MB인 반면, alpine 버전은 50MB로 18배 작습니다.

이는 불필요한 패키지 관리자, 문서, 로케일 파일 등을 모두 제거했기 때문입니다. 그 다음으로, Python 패키지 중 C 확장이 필요한 경우를 대비해 gcc와 musl-dev 같은 컴파일 도구를 임시로 설치합니다.

pip install 실행 후에는 apk del 명령으로 이런 빌드 도구를 제거하여 최종 이미지에는 포함되지 않도록 합니다. 이 패턴은 alpine 이미지에서 매우 중요한데, 컴파일이 필요한 패키지를 설치할 때 일시적으로만 컴파일러를 유지하기 때문입니다.

마지막으로, 애플리케이션 코드를 복사하고 실행 명령을 설정하여 최종적으로 50MB 크기의 최적화된 이미지를 만들어냅니다. 이는 쿠버네티스 클러스터에서 Pod을 시작할 때 이미지 pull 시간을 5초 이내로 단축시킵니다.

여러분이 이 코드를 사용하면 Docker Hub나 ECR 같은 레지스트리의 스토리지 비용이 절감되고, 배포 속도가 빨라지며, 보안 스캔 대상이 줄어들어 CVE 취약점이 감소하는 효과를 얻을 수 있습니다. 또한 컨테이너 오케스트레이션 환경에서 노드 간 이미지 전송 시간이 단축됩니다.

실전 팁

💡 alpine 이미지는 glibc 대신 musl libc를 사용하므로 일부 바이너리 패키지가 호환되지 않을 수 있습니다. 문제가 생기면 slim 이미지로 돌아가세요

💡 --no-cache-dir 옵션을 pip install에 추가하면 pip 캐시가 이미지에 저장되지 않아 추가로 100MB 정도 절약됩니다

💡 Node.js 애플리케이션이라면 node:18-alpine 대신 node:18-alpine3.18처럼 alpine 버전을 명시하면 재현 가능한 빌드가 가능합니다

💡 distroless 이미지(gcr.io/distroless/python3)는 쉘이 없어 디버깅이 어려우므로 프로덕션 배포 직전에 전환하세요

💡 베이스 이미지의 보안 취약점을 정기적으로 확인하려면 trivy나 snyk 같은 스캐너를 CI/CD에 통합하세요


4. 빌드 아규먼트와 환경 변수 활용 - 하나의 Dockerfile로 다양한 환경 대응

시작하며

여러분이 개발, 스테이징, 프로덕션 환경마다 다른 Dockerfile을 관리하고 있나요? 각 환경마다 API 엔드포인트, 로그 레벨, 기능 플래그가 다른데, 코드 중복이 발생하고 관리가 복잡해집니다.

이런 문제는 환경별로 하드코딩된 값을 사용할 때 발생합니다. 설정이 변경될 때마다 Dockerfile을 수정하고 다시 빌드해야 하며, 실수로 프로덕션 이미지에 개발 설정이 포함될 위험도 있습니다.

바로 이럴 때 필요한 것이 빌드 아규먼트(ARG)와 환경 변수(ENV)입니다. 빌드 타임과 런타임에 값을 동적으로 주입하여 하나의 Dockerfile로 모든 환경을 관리할 수 있습니다.

개요

간단히 말해서, ARG는 빌드 시점에 사용되는 변수이고, ENV는 컨테이너 실행 시점에 사용되는 변수입니다. 이 둘을 조합하면 유연한 이미지를 만들 수 있습니다.

왜 이 개념이 필요한지 실무 관점에서 설명하면, 빌드 타임에는 베이스 이미지 버전, 빌드 최적화 옵션, 기능 토글을 설정하고, 런타임에는 데이터베이스 URL, API 키, 로그 레벨을 설정해야 합니다. 예를 들어, 같은 이미지를 개발 환경에서는 DEBUG=true로 실행하고 프로덕션에서는 DEBUG=false로 실행할 수 있습니다.

기존에는 환경별로 다른 Dockerfile을 만들거나 빌드 후 설정 파일을 수동으로 교체했다면, 이제는 ARG와 ENV를 사용하여 빌드와 실행 시점에 값을 동적으로 주입할 수 있습니다. ARG와 ENV의 핵심 특징은 첫째, ARG는 Dockerfile 내에서만 사용되고 최종 이미지에 저장되지 않는다는 점, 둘째, ENV는 이미지에 저장되어 컨테이너에서 환경 변수로 접근할 수 있다는 점, 셋째, ARG를 ENV로 변환하여 빌드 값을 런타임에 전달할 수 있다는 점입니다.

이러한 특징들이 하나의 Dockerfile로 다양한 환경을 지원하는 핵심입니다.

코드 예제

# 빌드 아규먼트: 빌드 시 주입 (기본값 포함)
ARG NODE_VERSION=18
ARG BUILD_ENV=production

# 베이스 이미지에 ARG 사용
FROM node:${NODE_VERSION}-alpine

WORKDIR /app

# 빌드 환경에 따라 다른 의존성 설치
COPY package*.json ./
RUN if [ "$BUILD_ENV" = "development" ]; then \
    npm ci; \
    else \
    npm ci --only=production; \
    fi

# 환경 변수: 런타임에 사용 (ARG를 ENV로 변환)
ENV NODE_ENV=${BUILD_ENV}
ENV PORT=3000
ENV LOG_LEVEL=info

COPY . .
EXPOSE ${PORT}
CMD ["node", "server.js"]

설명

이것이 하는 일: 이 Dockerfile은 ARG와 ENV를 조합하여 빌드 시점과 실행 시점에 각각 다른 설정을 적용할 수 있는 유연한 이미지를 만듭니다. 첫 번째로, ARG로 NODE_VERSION과 BUILD_ENV를 정의합니다.

이 변수들은 docker build --build-arg BUILD_ENV=development 같은 명령으로 외부에서 주입할 수 있습니다. 기본값을 설정해두면 인자를 생략했을 때 프로덕션 설정으로 빌드됩니다.

ARG는 이미지 레이어에 저장되지 않으므로 민감한 정보(빌드 토큰 등)를 전달하는 데 적합합니다. 그 다음으로, BUILD_ENV 값에 따라 조건부로 명령을 실행합니다.

development 환경이면 devDependencies를 포함한 전체 의존성을 설치하고, production 환경이면 프로덕션 의존성만 설치합니다. 쉘의 if 문을 RUN 안에서 사용하여 하나의 레이어로 처리함으로써 레이어 수를 줄입니다.

마지막으로, ENV 명령으로 런타임 환경 변수를 설정하고, ARG 값을 ENV로 변환하여 애플리케이션 코드에서 접근할 수 있도록 합니다. 최종적으로 docker run -e LOG_LEVEL=debug myapp 같은 명령으로 실행 시점에 환경 변수를 오버라이드할 수 있습니다.

여러분이 이 코드를 사용하면 환경별 Dockerfile 관리 부담이 사라지고, CI/CD 파이프라인에서 동일한 빌드 스크립트로 모든 환경을 처리할 수 있으며, 설정 변경 시 코드 수정 없이 인자만 바꾸면 되는 유연성을 얻을 수 있습니다. 또한 docker-compose.yml에서 environment 섹션으로 환경별 설정을 중앙화할 수 있습니다.

실전 팁

💡 민감한 정보는 ARG나 ENV 대신 Docker secrets나 Kubernetes secrets를 사용하세요. 이미지 히스토리에 비밀번호가 노출될 수 있습니다

💡 ARG는 FROM 문 전에도 선언할 수 있으며, 이 경우 FROM에서만 사용 가능합니다. 이후 단계에서 사용하려면 FROM 이후에 다시 선언해야 합니다

💡 .env 파일과 docker-compose.yml의 env_file을 사용하면 여러 환경 변수를 한 번에 주입할 수 있어 명령이 간결해집니다

💡 ENV 변수는 docker inspect로 확인할 수 있으므로 보안에 민감한 값은 런타임에 마운트하거나 외부 시크릿 관리 시스템을 사용하세요

💡 docker build --build-arg HTTP_PROXY=http://proxy.example.com 같은 패턴으로 기업 환경의 프록시 설정을 빌드 시 주입할 수 있습니다


5. .dockerignore 활용 - 빌드 컨텍스트를 80% 줄이는 필수 파일

시작하며

여러분이 Docker 빌드를 시작했는데 "Sending build context to Docker daemon: 2.5GB"라는 메시지를 보며 몇 분씩 기다려본 적 있나요? node_modules, .git, 로그 파일, IDE 설정 같은 불필요한 파일들이 모두 Docker 데몬으로 전송되고 있습니다.

이런 문제는 .dockerignore 파일을 작성하지 않았을 때 발생합니다. Docker는 Dockerfile이 있는 디렉토리의 모든 파일을 빌드 컨텍스트로 간주하여 Docker 데몬으로 전송합니다.

이는 네트워크 오버헤드를 증가시키고 빌드 시작 시간을 지연시키며, COPY . .

같은 명령 실행 시 캐시 무효화를 자주 일으킵니다. 바로 이럴 때 필요한 것이 .dockerignore 파일입니다.

.gitignore와 비슷한 문법으로 빌드 컨텍스트에서 제외할 파일을 지정하여 빌드 성능을 크게 향상시킬 수 있습니다.

개요

간단히 말해서, .dockerignore는 Docker 빌드 시 빌드 컨텍스트에서 제외할 파일과 디렉토리를 지정하는 설정 파일입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, 프로젝트 디렉토리에는 소스 코드 외에도 의존성 패키지, 빌드 결과물, Git 히스토리, 테스트 커버리지 리포트, 로그 파일 등 수많은 파일이 있습니다.

이런 파일들은 Docker 이미지에 포함될 필요가 없을 뿐만 아니라, 빌드 컨텍스트에 포함되면 전송 시간이 늘어나고 캐시가 자주 무효화됩니다. 예를 들어, node_modules가 500MB라면 매 빌드마다 이 디렉토리를 Docker 데몬으로 전송하게 되어 로컬 빌드에서도 수십 초가 소요됩니다.

기존에는 COPY 명령에서 제외할 파일을 일일이 지정하거나, 빌드 전에 수동으로 파일을 삭제했다면, 이제는 .dockerignore에 패턴을 한 번만 작성하면 자동으로 제외됩니다. .dockerignore의 핵심 특징은 첫째, 빌드 컨텍스트 크기를 줄여 전송 시간을 단축한다는 점, 둘째, 불필요한 파일 변경이 캐시에 영향을 주지 않는다는 점, 셋째, 민감한 정보가 실수로 이미지에 포함되는 것을 방지한다는 점입니다.

이러한 특징들이 빌드 속도를 높이고 보안을 강화하는 핵심입니다.

코드 예제

# .dockerignore 파일 내용

# 의존성 디렉토리 (컨테이너 내부에서 다시 설치)
node_modules/
venv/
__pycache__/
*.pyc

# Git 관련 (히스토리는 불필요)
.git/
.gitignore
.gitattributes

# 빌드 결과물 (컨테이너 내부에서 다시 빌드)
dist/
build/
*.egg-info/

# 개발 도구 설정
.vscode/
.idea/
*.swp
*.swo
.DS_Store

# 테스트 및 문서
tests/
*.md
docs/
coverage/

# 환경 변수 파일 (보안)
.env
.env.local
*.key
*.pem

# 로그 및 임시 파일
logs/
*.log
tmp/
temp/

# Docker 파일 자체는 제외하지 않음 (필요한 경우)
!Dockerfile
!docker-compose.yml

설명

이것이 하는 일: 이 .dockerignore 파일은 Docker 빌드 시 불필요한 파일들을 빌드 컨텍스트에서 체계적으로 제외하여 성능과 보안을 동시에 개선합니다. 첫 번째로, node_modules, venv, pycache 같은 의존성 디렉토리를 제외합니다.

이런 디렉토리는 로컬 개발 환경에서 설치된 것으로, 운영 체제나 아키텍처가 다를 수 있어 Docker 이미지에서는 다시 설치해야 합니다. 또한 이런 디렉토리는 용량이 크고 파일 수가 많아 빌드 컨텍스트 전송 시간의 대부분을 차지합니다.

node_modules를 제외하면 빌드 컨텍스트가 2GB에서 50MB로 줄어드는 경우가 흔합니다. 그 다음으로, .git, .vscode, .idea 같은 개발 도구 관련 파일과 디렉토리를 제외합니다.

Git 히스토리는 프로덕션 이미지에 불필요할 뿐만 아니라, 전체 커밋 히스토리가 포함되어 수백 MB를 차지할 수 있습니다. IDE 설정 파일도 개인 개발 환경에만 필요하며 이미지에 포함될 이유가 없습니다.

마지막으로, .env, *.key, *.pem 같은 민감한 정보를 포함한 파일을 제외합니다. 이는 보안상 매우 중요한데, 이런 파일이 실수로 이미지에 포함되면 Docker Hub 같은 레지스트리에 푸시했을 때 비밀 정보가 노출될 수 있습니다.

최종적으로 빌드 컨텍스트 크기가 80% 줄어들고, 빌드 시작 시간이 수 분에서 수 초로 단축됩니다. 여러분이 이 코드를 사용하면 로컬 개발 환경에서 빌드 대기 시간이 크게 줄어들고, CI/CD 파이프라인의 빌드 단계가 빨라지며, 민감한 정보가 이미지에 포함되는 보안 사고를 예방할 수 있습니다.

또한 COPY . .

명령 실행 시 관련 없는 파일 변경으로 인한 캐시 무효화가 줄어듭니다.

실전 팁

💡 .dockerignore는 Dockerfile과 같은 디렉토리에 있어야 하며, 빌드 컨텍스트 루트를 기준으로 패턴을 작성하세요

💡 !를 사용하여 제외 규칙의 예외를 지정할 수 있습니다. 예를 들어 docs/를 제외하되 docs/README.md는 포함하려면 !docs/README.md를 추가하세요

💡 **/ 패턴으로 모든 하위 디렉토리에서 특정 파일을 제외할 수 있습니다. **/*.log는 모든 로그 파일을 제외합니다

💡 docker build 전에 docker build --no-cache -t test . 2>&1 | grep "Sending build context" 명령으로 빌드 컨텍스트 크기를 확인하세요

💡 프로젝트 타입별로 .dockerignore 템플릿을 만들어두면 새 프로젝트에서 빠르게 적용할 수 있습니다


6. RUN 명령 최적화 - 레이어 수를 줄여 이미지 효율 높이기

시작하며

여러분이 Dockerfile에서 패키지를 설치할 때마다 RUN 명령을 개별적으로 작성하여 이미지 레이어가 수십 개가 되고 이미지 크기가 불필요하게 커진 경험이 있나요? 각 RUN 명령은 새로운 레이어를 생성하고, 중간 파일들이 최종 이미지에 남아있게 됩니다.

이런 문제는 Docker의 레이어 아키텍처를 이해하지 못했을 때 발생합니다. 각 명령은 이전 레이어 위에 새로운 파일시스템 레이어를 추가하므로, 한 레이어에서 파일을 생성하고 다음 레이어에서 삭제해도 최종 이미지 크기는 줄어들지 않습니다.

삭제는 단지 "삭제되었다는 표시"를 추가할 뿐입니다. 바로 이럴 때 필요한 것이 RUN 명령 최적화입니다.

관련된 명령을 &&로 연결하여 하나의 레이어로 만들고, 임시 파일을 같은 레이어에서 정리하여 이미지 크기를 줄일 수 있습니다.

개요

간단히 말해서, RUN 명령 최적화는 여러 명령을 하나의 RUN 문으로 결합하고 임시 파일을 즉시 정리하여 레이어 수와 이미지 크기를 최소화하는 기법입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, Docker는 Union File System을 사용하여 레이어를 쌓는 방식으로 동작합니다.

각 레이어는 변경 사항만 저장하지만, 삭제는 "파일이 삭제되었다"는 메타데이터를 추가하는 것이므로 원본 파일은 하위 레이어에 여전히 존재합니다. 예를 들어, 첫 번째 RUN에서 1GB 파일을 다운로드하고 두 번째 RUN에서 삭제해도 이미지 크기는 1GB 이상입니다.

기존에는 가독성을 위해 각 작업을 별도의 RUN 명령으로 작성했다면, 이제는 관련된 작업을 하나의 RUN 명령으로 결합하고 백슬래시와 &&로 연결할 수 있습니다. RUN 명령 최적화의 핵심 특징은 첫째, &&로 명령을 체이닝하여 하나의 레이어로 만든다는 점, 둘째, 임시 파일과 캐시를 같은 RUN 문에서 정리한다는 점, 셋째, 멀티라인 포맷팅으로 가독성을 유지한다는 점입니다.

이러한 특징들이 레이어 수를 줄이고 이미지 크기를 최소화하는 핵심입니다.

코드 예제

FROM ubuntu:22.04

# 비효율적인 방식 (여러 레이어 생성)
# RUN apt-get update
# RUN apt-get install -y curl
# RUN apt-get install -y vim
# RUN rm -rf /var/lib/apt/lists/*

# 효율적인 방식 (하나의 레이어, 즉시 정리)
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        curl=7.81.0-1ubuntu1.10 \
        vim=2:8.2.3995-1ubuntu2.7 \
        git=1:2.34.1-1ubuntu1.9 && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

# Python 패키지 설치 예시
RUN pip install --no-cache-dir \
        flask==2.3.0 \
        redis==4.5.0 && \
    find /usr/local -type f -name '*.pyc' -delete && \
    find /usr/local -type d -name '__pycache__' -delete

설명

이것이 하는 일: 이 Dockerfile은 관련된 명령들을 하나의 RUN 문으로 결합하고 임시 파일을 같은 레이어에서 정리하여 최적화된 이미지를 생성합니다. 첫 번째로, apt-get update와 install을 &&로 연결하여 하나의 레이어로 만듭니다.

이렇게 하면 4개의 레이어 대신 1개의 레이어만 생성됩니다. --no-install-recommends 옵션은 추천 패키지를 제외하여 불필요한 설치를 막고, 각 패키지에 버전을 명시하여 재현 가능한 빌드를 보장합니다.

백슬래시()를 사용하여 여러 줄로 나누면 가독성이 유지됩니다. 그 다음으로, apt-get clean과 rm -rf /var/lib/apt/lists/*를 같은 RUN 문 안에서 실행하여 패키지 관리자의 캐시를 즉시 제거합니다.

만약 이를 별도의 RUN 명령으로 실행하면 이전 레이어에 캐시가 남아있어 이미지 크기가 줄어들지 않습니다. /var/lib/apt/lists/는 apt 패키지 인덱스를 저장하는데, 설치 후에는 불필요하므로 200MB 정도를 절약할 수 있습니다.

마지막으로, Python 패키지 설치 시에도 --no-cache-dir 옵션으로 pip 캐시를 비활성화하고, 설치 후 .pyc 파일과 pycache 디렉토리를 삭제하여 최종적으로 100MB 이상을 추가로 절약합니다. 이 모든 작업이 하나의 레이어에서 완료되어 중간 파일이 이미지에 남지 않습니다.

여러분이 이 코드를 사용하면 이미지 레이어 수가 20개에서 5개로 줄어들고, 이미지 크기가 500MB에서 300MB로 감소하며, 레이어 수가 적어 Docker의 레이어 관리 오버헤드도 줄어드는 효과를 얻을 수 있습니다. 또한 pull과 push 시 전송해야 할 레이어가 적어져 네트워크 효율이 높아집니다.

실전 팁

💡 명령이 실패하면 전체 RUN 문이 실패하므로 set -e를 추가하거나 각 명령의 성공 여부를 신중히 고려하세요

💡 패키지 버전을 명시하면 재현 가능한 빌드가 가능하지만, 보안 패치를 위해 정기적으로 버전을 업데이트하세요

💡 apt-get update와 install을 분리하지 마세요. 오래된 패키지 인덱스가 캐시되어 최신 버전을 설치할 수 없는 문제가 발생합니다

💡 && 대신 ||를 사용하면 앞 명령이 실패해도 계속 진행되므로 주의하세요. 일반적으로 &&가 안전합니다

💡 명령이 너무 길어지면 스크립트 파일로 분리하고 COPY로 가져온 후 RUN bash setup.sh && rm setup.sh 패턴을 사용하세요


7. ENTRYPOINT와 CMD 조합 - 유연한 컨테이너 실행 설계

시작하며

여러분이 Docker 컨테이너를 실행할 때 기본 동작은 유지하면서 인자만 바꾸고 싶은데, CMD를 사용하면 전체 명령을 오버라이드해야 해서 불편했던 경험이 있나요? 예를 들어 python app.py를 실행하는데, docker run myapp --debug 같은 식으로 플래그만 추가하고 싶은 상황입니다.

이런 문제는 ENTRYPOINT와 CMD의 차이를 이해하지 못했을 때 발생합니다. CMD만 사용하면 docker run의 인자가 전체 명령을 대체하므로 매번 python app.py --debug처럼 전체를 입력해야 합니다.

반복적인 부분이 많아 실수하기 쉽고 사용성이 떨어집니다. 바로 이럴 때 필요한 것이 ENTRYPOINT와 CMD의 조합입니다.

ENTRYPOINT로 고정된 실행 파일을 지정하고 CMD로 기본 인자를 제공하면, 사용자는 필요한 인자만 오버라이드할 수 있습니다.

개요

간단히 말해서, ENTRYPOINT는 컨테이너가 실행 파일처럼 동작하도록 만드는 명령이고, CMD는 ENTRYPOINT에 전달되는 기본 인자를 정의합니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, 컨테이너를 실행 파일처럼 사용하거나 기본 동작을 유지하면서 옵션만 변경하고 싶은 경우가 많습니다.

ENTRYPOINT는 오버라이드하기 어렵게 설계되어 있어 핵심 실행 명령을 강제할 수 있고, CMD는 쉽게 오버라이드되어 유연한 인자 전달이 가능합니다. 예를 들어, 데이터베이스 마이그레이션 컨테이너는 항상 migrate 명령을 실행하되, 대상 환경은 인자로 받을 수 있습니다.

기존에는 CMD에 전체 명령을 넣고 docker run에서 필요할 때마다 전체를 다시 작성했다면, 이제는 ENTRYPOINT로 실행 파일을 고정하고 CMD로 기본 인자를 제공할 수 있습니다. ENTRYPOINT와 CMD 조합의 핵심 특징은 첫째, ENTRYPOINT는 컨테이너의 주요 명령을 정의하고 쉽게 변경되지 않는다는 점, 둘째, CMD는 ENTRYPOINT에 전달되는 기본 인자이며 docker run 인자로 오버라이드된다는 점, 셋째, JSON 배열 형식(exec form)으로 작성하면 쉘을 거치지 않아 시그널 처리가 올바르게 동작한다는 점입니다.

이러한 특징들이 유연하고 안전한 컨테이너 실행을 가능하게 합니다.

코드 예제

FROM python:3.11-slim
WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# ENTRYPOINT: 고정된 실행 명령 (쉽게 변경되지 않음)
# exec form 사용 (JSON 배열) - 시그널 처리 정상 작동
ENTRYPOINT ["python", "app.py"]

# CMD: 기본 인자 (docker run 인자로 오버라이드 가능)
CMD ["--host", "0.0.0.0", "--port", "8000"]

# 실행 예시:
# docker run myapp
#   → python app.py --host 0.0.0.0 --port 8000
#
# docker run myapp --debug
#   → python app.py --debug (CMD가 완전히 대체됨)
#
# docker run myapp --host 127.0.0.1 --port 9000
#   → python app.py --host 127.0.0.1 --port 9000

설명

이것이 하는 일: 이 Dockerfile은 ENTRYPOINT와 CMD를 조합하여 python app.py는 항상 실행되되, 옵션은 유연하게 변경할 수 있는 컨테이너를 만듭니다. 첫 번째로, ENTRYPOINT를 JSON 배열 형식으로 정의하여 python app.py가 항상 실행되도록 고정합니다.

exec form(JSON 배열)을 사용하면 /bin/sh -c를 거치지 않고 직접 실행되므로, Ctrl+C 같은 시그널이 프로세스에 올바르게 전달되어 graceful shutdown이 가능합니다. shell form(문자열)을 사용하면 PID 1이 쉘 프로세스가 되어 시그널 처리에 문제가 생길 수 있습니다.

그 다음으로, CMD로 기본 인자를 제공합니다. docker run myapp처럼 인자 없이 실행하면 --host 0.0.0.0 --port 8000이 ENTRYPOINT 뒤에 추가되어 python app.py --host 0.0.0.0 --port 8000이 실행됩니다.

이는 가장 일반적인 사용 사례를 위한 합리적인 기본값입니다. 마지막으로, 사용자가 docker run myapp --debug처럼 인자를 제공하면 CMD가 완전히 대체되어 python app.py --debug가 실행됩니다.

ENTRYPOINT는 유지되므로 python app.py는 항상 실행되고, 사용자는 필요한 플래그만 추가하면 됩니다. 최종적으로 컨테이너가 CLI 도구처럼 직관적으로 동작합니다.

여러분이 이 코드를 사용하면 컨테이너를 실행 파일처럼 사용할 수 있어 사용성이 높아지고, 기본 설정을 제공하면서도 유연성을 유지하며, 시그널 처리가 올바르게 동작하여 컨테이너가 안전하게 종료되는 효과를 얻을 수 있습니다. 특히 쿠버네티스 환경에서 SIGTERM을 받았을 때 graceful shutdown이 가능합니다.

실전 팁

💡 shell form(ENTRYPOINT python app.py)은 환경 변수 치환이 가능하지만 시그널 처리에 문제가 있으므로 exec form을 사용하세요

💡 ENTRYPOINT를 오버라이드하려면 docker run --entrypoint bash myapp를 사용하여 디버깅 쉘로 들어갈 수 있습니다

💡 헬스체크나 초기화 작업이 필요하면 ENTRYPOINT에 쉘 스크립트를 지정하고, 스크립트 마지막에 exec "$@"로 CMD를 실행하세요

💡 docker-compose.yml에서 command 필드는 CMD를 오버라이드하고 entrypoint 필드는 ENTRYPOINT를 오버라이드합니다

💡 여러 서비스를 하나의 이미지에서 실행하려면 ENTRYPOINT를 스크립트로 만들고 첫 번째 인자($1)로 서비스를 선택하는 패턴을 사용하세요


8. 헬스체크 설정 - 컨테이너 상태를 자동으로 모니터링하기

시작하며

여러분이 컨테이너가 실행 중인데 실제로는 애플리케이션이 응답하지 않아서 사용자가 에러를 겪는 상황을 경험해본 적 있나요? docker ps로 보면 Up 상태인데 웹 서버가 크래시되었거나 데드락에 걸려 있는 경우입니다.

이런 문제는 컨테이너의 프로세스가 실행 중이라는 것과 애플리케이션이 정상적으로 서비스하고 있다는 것이 다르기 때문에 발생합니다. Docker는 기본적으로 PID 1 프로세스가 살아있는지만 확인하므로, 애플리케이션이 내부적으로 고장났어도 감지하지 못합니다.

바로 이럴 때 필요한 것이 HEALTHCHECK 명령입니다. 주기적으로 애플리케이션의 실제 상태를 확인하여 컨테이너가 healthy인지 unhealthy인지 판단할 수 있습니다.

개요

간단히 말해서, HEALTHCHECK는 컨테이너 내부에서 주기적으로 실행되는 명령으로, 애플리케이션의 실제 동작 상태를 확인하여 컨테이너 오케스트레이터에게 알려줍니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, 프로세스가 실행 중이어도 네트워크 요청에 응답하지 못하거나 데이터베이스 연결이 끊어진 상태일 수 있습니다.

Docker Swarm이나 Kubernetes 같은 오케스트레이터는 헬스체크 결과를 보고 unhealthy 컨테이너를 재시작하거나 트래픽을 우회시킵니다. 예를 들어, 웹 서버의 메모리 누수로 응답 시간이 느려지면 헬스체크가 실패하고 자동으로 재시작됩니다.

기존에는 외부 모니터링 도구로 주기적으로 핑을 보내거나, 수동으로 상태를 확인했다면, 이제는 HEALTHCHECK를 Dockerfile에 정의하여 Docker가 자동으로 모니터링하도록 할 수 있습니다. HEALTHCHECK의 핵심 특징은 첫째, 주기적으로 명령을 실행하여 애플리케이션 상태를 확인한다는 점, 둘째, 연속 실패 횟수에 따라 unhealthy로 마킹한다는 점, 셋째, 컨테이너 오케스트레이터가 이 정보를 사용하여 자동 복구를 수행한다는 점입니다.

이러한 특징들이 고가용성과 자동 복구를 가능하게 합니다.

코드 예제

FROM node:18-alpine
WORKDIR /app

# curl 설치 (헬스체크에 필요)
RUN apk add --no-cache curl

COPY package*.json ./
RUN npm ci --only=production

COPY . .

# 헬스체크 설정
HEALTHCHECK --interval=30s \
            --timeout=10s \
            --start-period=40s \
            --retries=3 \
            CMD curl -f http://localhost:3000/health || exit 1

EXPOSE 3000
CMD ["node", "server.js"]

# 옵션 설명:
# --interval: 헬스체크 실행 주기 (기본 30초)
# --timeout: 각 체크의 타임아웃 (기본 30초)
# --start-period: 초기 시작 유예 기간 (실패해도 카운트 안함)
# --retries: 연속 실패 횟수 (이 횟수만큼 실패하면 unhealthy)
# exit 0: healthy, exit 1: unhealthy

설명

이것이 하는 일: 이 Dockerfile은 30초마다 /health 엔드포인트를 호출하여 애플리케이션이 정상 동작하는지 확인하고, 3번 연속 실패하면 컨테이너를 unhealthy로 마킹합니다. 첫 번째로, curl을 설치하여 HTTP 헬스체크를 수행할 수 있도록 합니다.

alpine 이미지는 curl이 기본 포함되지 않으므로 apk add로 설치합니다. 웹 애플리케이션의 경우 HTTP 엔드포인트를 호출하는 것이 가장 확실한 헬스체크 방법입니다.

curl -f 옵션은 HTTP 에러 코드(4xx, 5xx)를 받으면 exit 1을 반환하여 실패로 처리합니다. 그 다음으로, HEALTHCHECK 옵션을 설정합니다.

--start-period=40s는 컨테이너 시작 후 40초 동안은 헬스체크가 실패해도 실패 카운트에 포함하지 않습니다. 애플리케이션 초기화 시간이 필요하기 때문입니다.

--interval=30s로 30초마다 체크하고, --timeout=10s로 각 체크는 10초 안에 완료되어야 하며, --retries=3으로 3번 연속 실패하면 unhealthy 상태가 됩니다. 마지막으로, CMD에서 curl -f http://localhost:3000/health를 실행하여 실제 HTTP 요청을 보냅니다.

/health 엔드포인트는 데이터베이스 연결, 외부 API 연결, 메모리 사용량 등을 확인하여 200 OK 또는 503 Service Unavailable을 반환하도록 구현해야 합니다. 최종적으로 docker ps에서 status 열에 healthy 또는 unhealthy가 표시되고, Docker Swarm이나 Kubernetes는 이 정보를 보고 트래픽 라우팅과 재시작을 결정합니다.

여러분이 이 코드를 사용하면 컨테이너가 멈춰있는데 Up 상태로 남아있는 문제를 방지하고, 오케스트레이터가 자동으로 문제를 감지하여 복구하며, 모니터링 대시보드에서 각 컨테이너의 실제 상태를 실시간으로 확인할 수 있는 효과를 얻을 수 있습니다. 특히 블루-그린 배포나 카나리 배포 시 새 버전이 정상인지 자동으로 확인하는 데 유용합니다.

실전 팁

💡 헬스체크 엔드포인트는 무거운 작업을 하지 말고 단순히 상태만 확인하세요. 30초마다 실행되므로 부하가 될 수 있습니다

💡 데이터베이스 헬스체크는 SELECT 1 같은 가벼운 쿼리를 사용하고, 실제 데이터 조회는 피하세요

💡 start-period는 애플리케이션의 평균 시작 시간보다 충분히 길게 설정하세요. 초기화 중에 unhealthy로 마킹되면 재시작 루프에 빠질 수 있습니다

💡 Kubernetes에서는 Dockerfile의 HEALTHCHECK 대신 livenessProbe와 readinessProbe를 사용하는 것이 더 유연합니다

💡 docker inspect mycontainer | jq '.[0].State.Health' 명령으로 헬스체크 히스토리를 확인하여 실패 패턴을 분석할 수 있습니다


9. 빌드 캐시 무효화 전략 - 외부 데이터를 최신으로 유지하기

시작하며

여러분이 Dockerfile에서 git clone이나 curl로 외부 소스를 가져오는데, 소스가 업데이트되어도 Docker가 캐시된 레이어를 재사용하여 최신 버전을 받아오지 못하는 경험을 해보셨나요? ADD 명령은 변경되지 않았으므로 Docker는 이전 레이어를 그대로 사용합니다.

이런 문제는 Docker가 파일 내용이 아닌 명령어 문자열만 비교하여 캐시를 판단하기 때문에 발생합니다. git clone https://github.com/repo.git이라는 명령은 매번 동일하므로 원격 저장소가 업데이트되어도 캐시가 재사용됩니다.

결과적으로 오래된 코드로 빌드하게 됩니다. 바로 이럴 때 필요한 것이 빌드 캐시 무효화 전략입니다.

ARG를 사용하여 캐시 버스팅 키를 주입하거나, ADD로 원격 URL을 가져오는 등의 방법으로 특정 레이어부터 캐시를 무효화할 수 있습니다.

개요

간단히 말해서, 빌드 캐시 무효화 전략은 외부 데이터가 변경되었을 때 Docker가 캐시를 재사용하지 않도록 강제로 특정 레이어를 무효화하는 기법입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, 일반적으로 캐시는 빌드 속도를 높이지만, 외부 소스를 가져오는 경우에는 오히려 최신 버전을 받지 못하는 문제가 됩니다.

git clone, curl, wget, pip install git+https:// 같은 명령은 원격 소스의 변경을 감지하지 못합니다. 예를 들어, 공통 라이브러리 저장소에서 코드를 가져오는데 업데이트를 반영하지 못하면 버그 수정이나 기능 추가가 누락됩니다.

기존에는 docker build --no-cache로 전체 캐시를 무효화하거나, 레이어 순서를 바꿔서 캐시를 깨뜨렸다면, 이제는 ARG로 캐시 버스팅 키를 주입하여 특정 레이어만 선택적으로 무효화할 수 있습니다. 빌드 캐시 무효화 전략의 핵심 특징은 첫째, ARG에 타임스탬프나 커밋 해시를 전달하여 값이 변경될 때마다 캐시를 무효화한다는 점, 둘째, 무효화 지점을 정확히 제어하여 이전 레이어는 캐시를 유지한다는 점, 셋째, CI/CD에서 자동으로 최신 버전을 빌드할 수 있다는 점입니다.

이러한 특징들이 캐시 효율과 최신성을 동시에 확보하는 핵심입니다.

코드 예제

FROM python:3.11-slim
WORKDIR /app

# 1단계: 의존성 설치 (캐시 유지하고 싶은 부분)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 2단계: 캐시 무효화 포인트
# 빌드 시마다 변경되는 값을 ARG로 받음
ARG CACHE_BUST=unknown
RUN echo "Cache bust: $CACHE_BUST"

# 3단계: 외부 소스 가져오기 (항상 최신 버전)
RUN git clone https://github.com/username/shared-lib.git /app/lib

# 또는 특정 커밋 사용
ARG LIB_VERSION=main
RUN git clone --branch $LIB_VERSION --depth 1 \
    https://github.com/username/shared-lib.git /app/lib

# 4단계: 애플리케이션 코드 복사
COPY . .

CMD ["python", "app.py"]

# 빌드 예시:
# docker build --build-arg CACHE_BUST=$(date +%s) -t myapp .
# docker build --build-arg LIB_VERSION=v2.3.0 -t myapp .

설명

이것이 하는 일: 이 Dockerfile은 의존성 설치는 캐시를 활용하되, 외부 저장소에서 코드를 가져오는 부분은 매 빌드마다 최신 버전을 받도록 설계되었습니다. 첫 번째로, requirements.txt 기반 의존성 설치를 먼저 수행합니다.

이 부분은 자주 변경되지 않으므로 캐시를 최대한 활용하고 싶은 영역입니다. 만약 캐시 버스팅을 이 레이어 위에 두면 의존성 설치까지 매번 다시 하게 되어 비효율적입니다.

그 다음으로, ARG CACHE_BUST를 선언하고 echo 명령으로 사용합니다. 이 ARG에 docker build --build-arg CACHE_BUST=$(date +%s)처럼 현재 타임스탬프를 전달하면, 매 빌드마다 값이 달라져 이 레이어부터 캐시가 무효화됩니다.

이 기법을 "cache busting"이라고 부르며, 정확히 원하는 지점에서만 캐시를 깨뜨릴 수 있습니다. 마지막으로, git clone 명령을 실행하여 외부 저장소의 최신 코드를 가져옵니다.

CACHE_BUST 레이어가 무효화되었으므로 이 레이어도 다시 실행되어 최신 커밋을 받습니다. --depth 1 옵션으로 히스토리 없이 최신 스냅샷만 가져와 시간과 용량을 절약할 수 있습니다.

LIB_VERSION 같은 ARG를 추가하면 특정 태그나 브랜치를 선택할 수도 있습니다. 여러분이 이 코드를 사용하면 CI/CD 파이프라인에서 항상 최신 의존성 코드로 빌드할 수 있고, 의존성 설치 같은 무거운 작업은 여전히 캐시를 활용하며, 빌드 시점에 버전을 명시적으로 제어할 수 있는 효과를 얻을 수 있습니다.

특히 모노레포에서 공유 라이브러리를 여러 서비스에서 사용할 때 유용합니다.

실전 팁

💡 CI/CD에서 $(git rev-parse HEAD)를 CACHE_BUST에 전달하면 커밋이 바뀔 때만 캐시가 무효화되어 더 효율적입니다

💡 외부 저장소의 특정 버전을 사용하려면 git clone 후 git checkout <commit-hash>로 고정하여 재현 가능한 빌드를 만드세요

💡 ADD https://example.com/file.tar.gz /app/ 같은 URL ADD는 파일이 변경되면 자동으로 캐시가 무효화되지만, HTTP 헤더에 따라 달라지므로 신뢰성이 낮습니다

💡 --no-cache 옵션은 전체 캐시를 무효화하므로 개발 중에만 사용하고, 프로덕션 빌드에서는 선택적 무효화를 사용하세요

💡 외부 소스를 자주 업데이트해야 한다면 멀티 스테이지 빌드에서 외부 소스 다운로드를 별도 단계로 분리하여 병렬로 실행할 수 있습니다


#Docker#MultiStageBuild#LayerCaching#ImageOptimization#BuildOptimization#JavaScript

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.