Overview

Dockerfile 작성 시 아무렇게나 작성하다보면 이미지 레이어가 불필요하게 늘어나고 이미지 크기도 증가합니다. 이런 식으로 Dockerfile 작성 시 몇가지 피해야 할 케이스들이 있습니다.

Docker 공식 문서를 보면 Best practices for writing Dockerfiles 라는 글이 있는데 Dockerfile 작성 시 주의 사항과 모범적인 예제를 가이드합니다.

저도 Dockerfile을 주기적으로 작성하지 않다보니 자꾸 잊어버리고 실수를 반복하는 경우가 많아서 위에서 언급한 공식 문서의 일부분을 정리해 놓고 작성 시 리뷰를 하고 있습니다.

Multi-stage 빌드 활용

Dockerfile을 작성할 때 Multi-stage 빌드를 활용하는 것이 좋습니다. Multi-stage 빌드를 사용하게 되면 이미지 크기를 효율적으로 줄일 수 있기 때문입니다.

경고

Multi-stage 빌드 기능을 사용하기 위해 "Docker 17.05" 이상 버전이 필요합니다.

Openshift 3.x 버전은 Docker 1.13.1 버전을 사용하지만 "imagebuilder"를 통해 이 기능을 지원한다고 합니다.
https://github.com/openshift/origin/issues/21627

Docker 버전 체계는 "~1.13.1" 버전으로 이후로 "17.x ~" 로 릴리스되고 있습니다.
- "~1.13.1" : https://docs.docker.com/release-notes/docker-engine/
- "17.x ~" : https://docs.docker.com/engine/release-notes/

TL;DR;

좀 더 자세한 내용을 설명하자면 내용이 길어지기 때문에 먼저 예제를 살펴보겠습니다. 자세한 내용은 뒤에 이어지는 “Multi-stage 빌드 사용 배경”을 읽어보시면 됩니다.

아래 multi-stage 빌드를 활용한 Dockerfile 예제가 있습니다.

  • Go build를 위한 “golang:latest” 이미지는 최종 이미지에 포함되지 않습니다.
  • 최종 이미지인 “alpine:latest” 에서는 빌드 결과물인 실행파일(app)만 복사해옵니다.
  • 최종 이미지에서는 불필요한 Go 패키지 및 소스 코드를 가지고 있지 않습니다.
  • 예제 코드
# 빌드 이미지 선언(최종 이미지에는 포함되지 않음)
FROM golang:latest AS builder
 
# Working directory 지정
WORKDIR /go/src/
 
# 소스 코드 복사
COPY src/app.go .
 
# 소스 코드 빌드
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
 
# Base 이미지 선언(최종 이미지)
FROM alpine:latest
 
# 인증서 추가
RUN apk --no-cache add ca-certificates
 
# Working directory 지정
WORKDIR /root/
 
# Build 결과 복사(빌드 이미지로 부터)
COPY --from=builder /go/src/app .
 
# 바이너리 실행
CMD ["./app"]

Multi-stage 빌드 사용 배경

일반적으로 Dockerfile은 소스코드를 빌드하고 결과물을 실행하는 방식으로 작성합니다.

단순하게 Dockerfile을 작성한다면 실행 환경으로 사용할 리눅스 이미지(예: ubuntu)에 빌드에 필요한 패키지, 라이브러리를 다운로드 받아서 이 이미지를 하나의 이미지로 관리할 것입니다. 예를 들어 go 언어로 작성된 소스코드를 빌드하고 ubutun 환경에서 실행한다면 이미지는 “golang:ubuntu”와 같이 됩니다.

하지만 앱을 실행할 때는 빌드 단계의 레이어는 사용하지 않지만 용량만 차지하는 불필요한 자원입니다. 그래서 빌드, 실행 단계를 분리할 수 있는 빌드 패턴이 나왔습니다. 빌드, 실행 단계의 Dockerfile을 각각 작성하고 이를 한번에 실행할 수 있는 스크립트를 만드는 방식인데 자세한 내용은 Before multi-stage builds 라는 글을 참고하세요.

하지만 이마저도 Dockerfile을 두개로 관리해야하고 스크립트도 작성해야되다 보니 번거로운 작업이라 느껴졌고 그래서 “Alex Ellis” 라는 엔지니어가 이를 해결할 수 있는 “multi-stage build” 기능을 PR로 올려 Merge가 됐습니다. 이와 관련된 Builder pattern vs. Multi-stage builds in Docker 블로그가 있으니 더 궁금하시면 참고하세요.

여튼 그래서 어쨋거나.. 우리는 multi-stage 빌드 기능을 사용해서 이미지의 크기를 효율적으로 줄일 수 있다라는 점만 기억하면 됩니다.

이미지 레이어 최소화

Docker 1.10 이상 버전에서 “RUN”, “COPY”, “ADD” 명령어는 레이어를 생성합니다.(=이미지 사이즈 증가)

따라서 명령어는 가능한 아래와 같이 한줄로 작성하세요.

명령을 이어서 실행할때는 “;” 보다는 “&&” 을 사용하세요.

# Do not
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y nginx
 
COPY a.txt /tmp
COPY b.txt /tmp
 
# Do
RUN apt-get update && apt-get install -y \
    curl \
    nginx \
    && apt-get clean all
 
COPY a.txt b.txt /tmp

Multi-line argument 정렬

Argument를 여러 줄로 작성하는 경우 알파벳 순으로 정렬하세요.

# Do not
RUN apt-get update && apt-get install -y curl wget git net-tools dnsutils telnet vim \
    && apt-get clean all
 
# Do
RUN apt-get update && apt-get install -y \
    curl \
    dnsutils \
    git \
    net-tools \
    telnet \
    wget \
    && apt-get clean all

Hard code 제거

변경 될 수 있는 속성은 ENV(Environment Variable)로 정의해서 사용하세요.

ENV로 정의한 변수는 컨테이너 실행 시 주입할 수 있습니다.

# Do not
RUN apt-get update && apt-get install -y curl \
&& curl -OL https://github.com/example/example.tar.gz --proxy http://example-proxy.com
 
# Do
ENV http_proxy http://example-proxy.com
RUN apt-get update && apt-get install -y curl \
&& curl -OL https://github.com/example/example.tar.gz --proxy $http_proxy
 
$ docker run --env http_proxy="http://another-proxy.com"

파일 복사

파일을 다운로드 한 후에 다음 layer에서 삭제하면 중간 layer에는 파일이 그대로 남게됩니다.

따라서 다운로드 받아서 실행하고 삭제하는 것까지 한줄로 실행하세요.

# Do not
COPY openshift-origin-client-tools-v3.11.0-0cbc58b-linux-64bit.tar.gz .
RUN tar zxvf openshift-origin-client-tools-v3.11.0-0cbc58b-linux-64bit.tar.gz
RUN rm -rf openshift-origin-client-tools-v3.11.0-0cbc58b-linux-64bit.tar.gz
 
# Do not
ADD https://github.com/openshift/origin/releases/download/v3.11.0/openshift-origin-client-tools-v3.11.0-0cbc58b-linux-64bit.tar.gz .
RUN rm -rf openshift-origin-client-tools-v3.11.0-0cbc58b-linux-64bit.tar.gz
  
# Do
RUN curl -OL https://github.com/openshift/origin/releases/download/v3.11.0/openshift-origin-client-tools-v3.11.0-0cbc58b-linux-64bit.tar.gz \
    && tar zxvf openshift-origin-client-tools-v3.11.0-0cbc58b-linux-64bit.tar.gz \
    && rm -rf openshift-origin-client-tools-v3.11.0-0cbc58b-linux-64bit.tar.gz

ADD or COPY

ADD와 COPY 명령은 파일을 복사하는 기능을 공통적으로 가지고 있습니다.

하지만 파일을 복사하기 위한 용도라면 COPY 명령을 사용하세요.

# Do not
ADD nginx.conf /etc/nginx/nginx.conf
  
# Do
COPY nginx.conf /etc/nginx/nginx.conf

참고

ADD 명령어로 압축(tar) 파일을 복사하는 경우 추출(extraction)도 함께 수행됩니다.
따라서 단순히 복사만을 하는 경우는 COPY 명령을 사용하는 것이 좋습니다.