Dockerfile 작성 및 이미지 최적화
Dockerfile은 도커 이미지를 만들기 위한 자동화된 스크립트- 단순히 앱을 실행하는 것을 넘어, 운영 환경에서 효율적으로 동작하는 이미지를 만들기 위해서는 작성 요령과 최적화 기법을 깊이 있게 이해해야 함.
1. Dockerfile의 주요 명령어와 역할
- Dockerfile은 아래에서 위로 쌓이는 레이어(Layer) 구조를 형성함
FROM: 모든 Dockerfile의 시작. 기반이 될 베이스 이미지를 지정함WORKDIR: 컨테이너 내부에서 명령어가 실행될 작업 디렉토리를 설정 (없으면 자동 생성)COPY/ADD: 호스트 머신의 파일을 컨테이너 내부로 복사 (ADD는 URL 다운로드나 압축 해제 기능이 추가로 포함됨)RUN: 이미지를 빌드하는 동안 실행할 명령어 (패키지 설치, 파일 권한 변경 등)CMD/ENTRYPOINT: 컨테이너가 시작될 때 실행할 명령
CMD: 인자값이 변경될 수 있는 기본 명령ENTRYPOINT: 컨테이너의 주 목적으로 고정된 실행 명령EXPOSE: 컨테이너가 사용할 포트 명시 (실제 포트 포워딩은docker run -p에서 수행)ENV: 환경 변수 설정
2. 이미지 최적화의 필요성
- 이미지가 무거우면
- 빌드 속도가 느려지고,
- 네트워크 전송 시 대역폭을 낭비하며,
- 무엇보다 배포 시 컨테이너 기동 속도에 악영향을 줌
- 또한, 불필요한 도구가 포함되면 보안 취약점(Attack Surface)이 늘어남
Dockerfile작성의 성패는 “얼마나 캐시를 잘 활용하는가”와 “최종 이미지를 얼마나 가볍게 만드는가”에 달려 있음
3. 핵심 최적화 전략
- 최적화 과정을 거치면 수 GB에 달하던 이미지가 수십 MB 수준으로 줄어드는 경험을 할 수 있음
- 효율적인 빌드를 위한 Dockerfile 최적화 전략
- 도커 이미지를 빌드할 때 효율성을 극대화하기 위한 두 가지 핵심 기법
- 레이어 캐싱(Layer Caching) 최적화
- 멀티 스테이지 빌드(Multi-stage Build)
- 실무에서는 빌드 속도와 이미지 용량이 배포 효율성을 결정짓는 핵심 지표가 됨
- 경량 베이스 이미지 선택 (Alpine & Slim)
- 일반적인 OS 이미지(Ubuntu 등)는 수백 MB를 차지 🡲 최소화된 이미지로 교체
- 예시
- Alpine
- 5MB 내외의 초경량 리눅스 배포판
- 보안이 강력하고 가볍지만, 일부 C 라이브러리 호환성(musl vs glibc)을 체크해야 함
- Slim
- 필요한 런타임(Python, Java 등)만 포함하고 불필요한 패키지를 제거한 버전
- Alpine
- 레이어 수 최소화 (Layer Flattening)
- 도커는
RUN,COPY,ADD명령마다 새로운 레이어를 생성함 🡲 비슷한 작업은&&로 묶어 하나의 레이어로 합치는 것이 좋음-
나쁜 예(레이어 3개 생성)
RUN apt-get update RUN apt-get install -y python3 RUN rm -rf /var/lib/apt/lists/* -
좋은 예 (레이어 1개 생성)
RUN apt-get update && apt-get install -y \ python3 \ && rm -rf /var/lib/apt/lists/*
-
- 도커는
- 멀티 스테이지 빌드 (Multi-stage Build) 🡲 가장 중요
- 빌드 시에만 필요한 도구(컴파일러, SDK 등)와 실행 시에 필요한 파일(바이너리, 라이브러리)을 분리하는 기법
- 단일
Dockerfile내에서FROM구문을 여러 번 사용하여 “빌드 단계(Build Stage)”와 “실행 단계(Run Stage)”를 완전히 분리함- 애플리케이션을 빌드할 때는 컴파일러, SDK, 빌드 툴 등 많은 도구가 필요하지만,
- 실제 서비스를 실행할 때는 소스 코드가 컴파일된 결과물(바이너리나 배포용 파일)만 있으면 됨
- 단일
- 최종 이미지의 용량을 수 GB에서 수십 MB 단위로 줄일 수 있음
-
불필요한 도구가 제거되어 보안성 극대화
- 멀티 스테이지 빌드 적용 예시
-
Node.JS 기준
# Stage 1: Build (무거운 이미지를 사용하여 컴파일) FROM node:18 AS builder WORKDIR /app COPY . . RUN npm install && npm run build # Stage 2: Production (가벼운 이미지를 사용하여 결과물만 복사) FROM nginx:alpine COPY --from=builder /app/dist /usr/share/nginx/html -
Python 기준
# --------------------------------------------------------- # Stage 1: Build Stage (의존성 패키지 빌드 및 컴파일을 위한 환경) # --------------------------------------------------------- # C 확장 모듈이나 무거운 라이브러리 설치를 위해 빌드 도구(gcc 등)가 포함된 베이스 사용 FROM python:3.9 AS builder WORKDIR /app # 캐시 효율을 위해 의존성 정의 파일 먼저 복사 COPY requirements.txt . # --user 옵션을 사용하여 패키지를 로컬 사용자 디렉토리(.local)에 설치 # 빌드 도구가 필요한 패키지들이 여기서 컴파일 및 빌드됩니다. RUN pip install --no-cache-dir --user -r requirements.txt # 소스 코드 복사 (컴파일 언어가 아니므로 이 단계에서는 소스만 준비) COPY . . # --------------------------------------------------------- # Stage 2: Run Stage (실제 서비스를 구동하기 위한 초경량 환경) # --------------------------------------------------------- # 실행 시에는 컴파일러가 필요 없으므로 매우 가벼운 -slim 이미지를 사용 FROM python:3.9-slim WORKDIR /app # 중요: Stage 1(builder)의 결과물인 .local(설치된 패키지들) 폴더만 쏙 빼와서 복사함 COPY --from=builder /root/.local /root/.local # 실제 애플리케이션 소스 코드 복사 COPY --from=builder /app /app # 복사해온 패키지들을 파이썬 런타임이 인식할 수 있도록 환경 변수(PATH) 설정 ENV PATH=/root/.local/bin:$PATH EXPOSE 5000 # 최종 실행 명령 (최종 이미지에는 무거운 빌드 툴체인이 포함되지 않음) CMD ["python", "app.py"]
-
-
이렇게 하면 최종 이미지에는 소스 코드나 빌드 도구가 포함되지 않아 용량이 극적으로 줄어듦
-
멀티 스테이지 빌드 전후 비교 결과
빌드 방식 포함되는 구성 요소 최종 이미지 용량 보안성 (공격 표면) 단일 스테이지 빌드 Go SDK + 소스 코드 + 빌드 툴 + 실행 파일 약 800 MB 위험 (컴파일 도구 노출) 멀티 스테이지 빌드 초경량 Linux(Alpine) + 실행 파일 약 20 MB 안전 (필수 파일만 존재)
- 빌드 시에만 필요한 도구(컴파일러, SDK 등)와 실행 시에 필요한 파일(바이너리, 라이브러리)을 분리하는 기법
-
레이어 캐싱(Layer Caching) 효율 극대화
- 도커는 이미지 빌드 시,
Dockerfile의 명령어가 변경되지 않았다면 기존에 빌드된 레이어를 재사용(Caching) 함- 단 하나의 레이어라도 변경되면, 그 이후에 오는 모든 레이어는 캐시가 깨져(Cache Busting) 처음부터 다시 빌드 됨
- 따라서 명령어의 순서를 “변하지 않는 파일 🡲 자주 변하는 파일” 순으로 배치해야 빌드 속도를 획기적으로 줄일 수 있음
-
변경이 잦은 파일(소스 코드)은 Dockerfile의 뒷부분에, 변경이 적은 파일(종속성 설정)은 앞부분에 배치할 것
COPY package.json . # 라이브러리 목록을 먼저 복사 RUN npm install # 변경이 없는 한 캐시된 레이어 사용 COPY . . # 소스 코드는 자주 변하므로 마지막에 복사-
나쁜 예시 (캐시 효율 저하)
FROM node:18-alpine WORKDIR /app # 소스 코드와 설정 파일을 한 번에 복사 COPY . . # 소스 코드가 1줄만 바뀌어도 아래의 무거운 패키지 설치과정이 매번 새로 실행됨 RUN npm install CMD ["npm", "start"] -
좋은 예시 (캐시 최적화)
FROM node:18-alpine WORKDIR /app # 1. 자주 변하지 않는 의존성 정의 파일만 먼저 복사 COPY package.json package-lock.json ./ # 2. 패키지 설치 (package.json이 바뀌지 않았다면 이 무거운 작업은 캐시 처리됨) RUN npm install # 3. 자주 변하는 소스 코드는 가장 마지막에 복사 COPY . . CMD ["npm", "start"]
-
- 도커는 이미지 빌드 시,
.dockerignore파일 활용- Git의
.gitignore처럼 이미지 빌드에 불필요한 파일(.git,node_modules, 로그 파일 등)이 컨테이너 내부로 복사되지 않도록 제외할 것
- Git의
- 요약
- 캐시 최적화: 소스 코드(
COPY . .)보다 패키지 설치(npm install,pip install등) 명령을 무조건 앞서 배치- 용량 최적화: 컴파일이 필요한 언어(Go, Java, C++)나 고도화된 프론트엔드 빌드(React, Vue) 환경에서는 멀티 스테이지 빌드가 필수
- 효율적인 Dockerfile 체크리스트
- 가장 가벼운 베이스 이미지를 썼는가?
- 멀티 스테이지 빌드를 활용했는가?
- 캐시를 고려하여 명령어 순서를 배치했는가?
- 불필요한 임시 파일(캐시 삭제 등)을
RUN명령 끝에 지웠는가?- .dockerignore로 불필요한 파일을 걸러냈는가?