본문 바로가기
Development

Dockerfile Tips(Best Practice) - 이미지 크기 최소화

by 토마스.dev 2017. 1. 27.
반응형


Docker가 업계에서 매우 빠르게 퍼지면서 이제는 분야 관계없이 개발자들은 기본적으로 Docker개념을 알고 있어야 한다.

프론트엔드 개발자도, 백엔드 개발자도 자신의 어플리케이션을 배포하려면 Dockerfile을 직접 작성해야 한다는 것이다.

자신이 만든 어플리케이션의 환경구성이나 서비스 탐색(Service Discovery)은 자신이 직접 해야지 다른 누군가가 해주지 않는다.(서비스를 찾는 탐색기능 자체는 물론 PaaS에서 지원해줘야 하는 거겠지만)


요즘은 docker-compose도 나와서 docker image생성 뿐만 아니라, 테스트/배포 자동화가 더 편리해졌다. 

Kubernetes같은 PaaS의 장점까지도 흡수하며 발전하는 Docker를 보면 무섭기까지 하다.


처음 Dockerfile을 작성하는 데는 어렵지 않다. 

에러가 나더라도 시행착오를 몇번 거치면 어떻게든 이미지를 만들 수는 있다. 

하지만 그러고 나서 이미지의 크기를 보면 '내 어플리케이션이 이정도까지 큰가?' 라는 생각에 충격을 먹게 된다.


특히나 컨테이너를 배포할 때 배포시간, 네트웍 소모비용, 스토리지 용량 등을 고려해야 한다.


Dockerfile작성을 위한 여러가지 팁은 기본적으로 Docker 공식 홈페이지와 구글링을 통해 쉽게 접할 수 있다. 

여기서는 이러한 사이트에서 제시하는 여러가지 팁을 정리하여 공유하고자 한다.


Dockerfile을 생성하는데 있어 고려해야하는 사항들은 다음과 같다.


  • 이미지 크기
  • 이미지 빌드 속도
  • 보안
  • 유지보수성, 유연성


관련한 모든 팁을 한번에 쓰고자 하였으나 쓰다보니 분량이 많고, 시간도 충분하지 않아 몇번에 걸쳐 나눠 쓰려고 한다.

처음은 최소화와 관련된 팁만을 공유하고자 한다.


이미지 크기 최소화


가장 중요한 부분이 바로 이미지 크기를 최소화 하는 것이다. Docker는 이미지를 빌드할때 레이어라는 개념을 사용하는데, 컨테이너에 파일이 추가되거나 삭제 등 변화가 발생했을때 레이어가 생성되며 이 레이어의 최소화를 통해 용량을 최소화할 수 있다.


몇개의 레이어가 있으며 각 레이어가 얼마를 차지하고 있는지를 확인하는 명령어는 history이다. 아래는 history명령어를 이용해 특정 이미지의 레이어 정보를 보여주고 있다.


1
2
3
4
5
6
7
8
9
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
2e560760b2db        6 months ago        /bin/sh -#(nop) ENTRYPOINT &{["/run.sh"]}     0 B                 
<missing>           6 months ago        /bin/sh -#(nop) COPY file:6d7449b1aeffb25aa   1.602 kB            
<missing>           6 months ago        /bin/sh -#(nop) EXPOSE 3000/tcp               0 B                 
<missing>           6 months ago        /bin/sh -#(nop) VOLUME [/var/lib/grafana /v   0 B                 
<missing>           6 months ago        |1 GRAFANA_VERSION=3.1.1-1470047149 /bin/sh -   137.7 MB            
<missing>           6 months ago        /bin/sh -#(nop) ARG GRAFANA_VERSION           0 B                 
<missing>           11 months ago       /bin/sh -#(nop) CMD ["/bin/bash"]             0 B                 
<missing>           11 months ago       /bin/sh -#(nop) ADD file:b5391cb13172fb513d   125.1 MB
cs


패키지 인스톨과 파일 삭제를 동시에 하기

apt update나 curl를 통한 파일 다운로드시 필요한 파일을 설치한 후 불필요한 파일을 삭제해줘야 하는데, 중요한 점은 파일의 삭제를 설치명령과 동시에 Docker 명령어 내에서 수행해야 한다는 점이다.


예를 들어, apt update를 하면 /var/lib/apt/lists에 불필요한 리스트파일들이 생성되기 때문에 이를 삭제해야 하며, curl 등을 통해 zip 파일을 받았을 때 압축 해제후 이 zip파일을 삭제해야 된다.


헌데 이러한 삭제명령을 설치명령과 동시에 하지 않는다면 docker image에 이 정보들이 레이어로 쌓이게 된다.


1
2
3
RUN curl http://xx.xxx.com/file.zip 
RUN tar xvzf file.zip
RUN rm file.zip
cs


이렇게 만들어 두면, 각 명령어마다 레이어가 생기게 되어 file.zip이 포함되게 된다. 즉, 파일크기가 50mb라면 내가 원하는 최종 이미지에는 이 50mb가 포함되지 않기를 바라지만 레이어로 분리되어 포함이 된다는 것이다.


이는 다음과 같이 수정을 해야 한다.


1
2
3
RUN curl http://xx.xxx.com/file.zip \
&& tar xvzf file.zip \
&& rm file.zip
cs


알다시피 &&는 앞의 명령어가 성공하게 되면 다음 명령어를 수행하라는 연산자이다. 일명 이 방법을 체이닝(chaining)이라 한다.


이렇게 되면, 압축파일을 받아 설치하고 삭제하는 것을 한번에 하게 되고 결과적으로 하나의 레이어만 생기게 되며, file.zip이 포함되지 않게 된다.


apt update도 마찬가지이다. 많은 trusted dockerfile 등을 보면 기본적으로 다음과 같은 패턴으로 사용하고 있다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
RUN apt-get update && apt-get install -y \
    aufs-tools \
    automake \
    build-essential \
    curl \
    dpkg-sig \
    libcap-dev \
    libsqlite3-dev \
    mercurial \
    reprepro \
    ruby1.9.1 \
    ruby1.9.1-dev \
    s3cmd=1.1.* \
 && rm -rf /var/lib/apt/lists/*
cs


&& 연산자로 마지막에 불필요한 리스트 파일들을 삭제하고 있다.


이와 같이, 레이어를 최소화 함으로써 이미지 크기를 최소화 할 수 있다.


작은  Base Image 사용하기

FROM 에 명시하는 Base Image를 최대한 작은 것으로 하는 것이다. 처음 작성을 하게 되면 내가 개발하고 있는 환경과 가장 비슷해야 하기 때문에 아무생각없이 ubuntu:14.04 등을 Base Image로 하게 된다. 하지만 해당 이미지를 pull 해서 받아보면 용량이 작지 않음을 알 수 있다.


1
2
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              14.04               b969ab9f929b        9 days ago          188 MB
cs


이런 ubuntu버전을 사용하게 되면 실제 어플리케이션을 실행하는데 상관없는 불필요한 파일들이 있기 때문이다. 예를들어 ifconfig, ping등 net-tools 패키지는 필요가 없음에도 설치가 되어 있는 것이다. 


어플리케이션에 필요한 환경만 구성하기 위해 같은 debian 계열로서 jessie, alpine등을 사용하면 더 작은 이미지를 기반으로 이미지를 생성할 수 있다.


 jessie  의 경우 123mb정도이나 slim버전은 80mb 정도이며, alpine은 무려 4mb가 밖에 안된다.

하지만 개발환경과 동일하게 맞추기 위해 자신이 생각하지 못한 파일들을 설치해야할 수 있기 때문에 해당 버전등에 대한 특징을 잘 파악해 둘 필요가 있다.


상황에 맞게 ADD 명령어 사용하기

ADD와 COPY을 기능을 포함하면서도 http 로 파일을 다운로드 하거나 tar/zip 파일을 풀어 설치하는 기능을 가지고 있다. 하지만 http를 통해 압축파일을 다운로드 받을때는 ADD보다는 curl 또는 wget을 사용하는 것이 좋다.


왜냐하면 ADD를 통해 압축파일을 다운로드 받고, 다시 이 압축파일을 지워야 하기 때문에 첫번째로 언급한 파일 삭제를 동시에 진행할 수 없기 때문이다.


1
2
3
ADD http://xx.xxx.com/file.tar.gz
RUN tar zvxf file.tar.gz
RUN rm file.tar.gz
cs


위처럼 ADD와  RUN명령어로 분리시켜 처리하는 것보다


1
2
3
RUN wget http://xx.xxx.com/file.zip \
&& tar xvzf filr.tar.gz \
&& rm file.tar.gz
cs


RUN 명령어로 한번에 처리하는게 낫다는 것이다.


ADD를 사용하기에 가장 적합한 케이스는 로컬 압축파일을 압축을 해제해 추가하고 싶을 때다


1
ADD file.tar.gz ./
cs


을 COPY와 RUN을 이용하게 되면 불필요한 레이어를 생성하게 된다.


1
2
COPY file.tar.gz ./
RUN tar xvzf file.tar.gz
cs

개발용 의존성 패키지 배제하기

테스트 등의 개발용을 위해 설치하는 의존성 패키지가 설치되지 않도록 한다. 물론 배포철학에 따라 전부 포함해야 한다는(테스트용과 prod의 일관성을 유지하기 위해) 의견도 있지만, 개발용으로 너무 많은 패키지를 설치해야 한다면 실제 prod으로 배포할때는 배제하는 것이 효율적이라 생각한다.


예를 들어 nodejs기반의 어플리케이션을 개발한다 치면 npm install은 기본 의존성 모듈과 더불어 개발용 모듈까지도 설치한다. npm install시 개발용 모듈은 설치하지 않도록 하는것이 바람직하다 할 수 있겠다.


헌데 요즘은 gulp와 같은 빌더를 이용하기 때문에 개발용 모듈을 배제하기가 쉽지 않다(gulp는 개발용 모듈로 포함되어 있다)

한가지 해결책은 프로젝트 파일들을 바로 복사한뒤에 npm install을 하지 말고, gulp 등을 빌드를 거치고 난 다음에 프로젝트 파일들을 컨테이너로 복사하는 것이다.

즉, 컨테이너에 옮겨서 빌드/테스트 하지 말고 빌드/테스트를 gulp로 끝낸 후에 컨테이너로 복사하라는 것이다.(물론 jenkins등의 연동을 통해서 test 자동화가 필요할 때는 docker-compose 등을 통해서 테스트용 dockerfile을 별도로 분리해 처리하는 것이 한 방법이겠다)


한 예로, kubernetes dashbaord github을 보면 매우 작은?! 크기의 도커이미지를 생성한다고(100mb이지만 하는거에 비하면 작다고 할 수 있다) 'Awesome' 외치고 있는걸 볼 수 있는데, Dockerfile을 보면 scratch 부터 시작해서 프로젝트 파일들을 복사하기만 하고 끝내는 것을 볼 수 있다. 소스코드를 직접 빌드 해보면, gulp로 빌드 및 테스트를 모두 끝내기 때문에 가능한 것임을 알 수 있다.


.dockerignore 활용하기

.dockerignore파일은 .gitignore 파일과 동일하게 COPY명령어 등을 통해 프로젝트 파일을 컨테이너로 복사할때 기입된 리스트(폴더, 파일등)를 배제하는 역할을 한다.

실제 프로젝트를 실행하는데 불필요한 폴더나 파일들을 필터링 할 수 있다.


export/import 사용하기

이미지에 너무 많은 레이어가 있어서 크기가 큰 경우 강제적으로 모든 레이어를 삭제해서 줄여주는 기능이 있다. 바로 export 명령어인데, 이것을 사용하면 불필요한 크기를 줄일 수 있다. 

사실 이 방법은 권장되지 않는다. 왜냐하면 말그대로 모든 레이어를 삭제해버리기 때문에 히스토리를 전부 상실함을 의미한다.


1
2
3
4
$ docker export 7423d238b | docker import - sample:flat 3995a1f00b91efb016250ca6acc31aaf5d621c6adaf84664a66b7a4594f695eb
$ docker history sample:flat
IMAGE        CREATED        CREATED BY SIZE
3995a1f00b91 12 seconds ago 85.18 MB

cs


위 예제를 보면 7423d238b 컨테이너를 export하고 이를 다시 sample:flat 이미지로 import 하는 것이다. 원래 1gb인 파일 크기가 85mb정도로 줄어들었다.


반응형