avatar
Published on

Dockerfile 작성 가이드

Author
  • avatar
    Name
    yceffort

여기 에서 이어집니다.

하단 권장사항은 효율적이고 유지관리가 용이한 Dockerfile을 만드는데 도움이 되도록 제공되었다.

Table of Contents

FROM

https://docs.docker.com/engine/reference/builder/#from

가능하면, 현재 제공되고 있는 공식 이미지를 사용하는 것이 좋다. 알파인 이미지는 리눅스 배포 판 중에서 크키가 매우작고 (6mb) 엄격하게 관리되고 있기 때문에 사용을 추천한다.

LABEL

https://docs.docker.com/config/labels-custom-metadata/

이미지에 레이블을 추가하여 프로젝트별 이미지 구성, 라이센스 정보 기록, 자동화 정보 등 기타 여러가지 정보를 기록할 수 있다. 각 레이블은 LABEL로 시작하고, 하나 이상의 키-값 쌍으로 추가하면 된다.

공백이 있는 문자열은 따옴표로 묶거나 공백을 이스케이프 해야 한다. ('' 도 마찬가지다.)

# Set one or more individual labels
LABEL com.example.version="0.0.1-beta"
LABEL vendor1="ACME Incorporated"
LABEL vendor2=ZENITH\ Incorporated
LABEL com.example.release-date="2015-02-12"
LABEL com.example.version.is-production=""

모든 이미지는 레이블을 하나 이상 가지고 있을 수 있다. Docker 1.10 이전 버전에서는, 추가적인 레이어가 생성되지 않도록 여러 레이블을 하나의 LABEL로 묶는 것이 권장되었다. 이제 더이상 필요하진 않지만 여전히 여러개를 결합하는 방식은 가능하다.

# Set multiple labels on one line
LABEL com.example.version="0.0.1-beta" com.example.release-date="2015-02-12"
# Set multiple labels at once, using line-continuation characters to break long lines
LABEL vendor=ACME\ Incorporated \
      com.example.is-beta= \
      com.example.is-production="" \
      com.example.version="0.0.1-beta" \
      com.example.release-date="2015-02-12"

RUN

https://docs.docker.com/engine/reference/builder/#run

길거나 복잡한 RUN 구문은 백슬래시를 활용하여 여러줄로 분할하는 것이 Dockerfile 관리에 좋다.

apt-get

RUN에서 아마 가장 자주 사용되는 명령어는 apt-get일 것이다. RUN apt-get은 패키지를 설치하는 명령어이기 때문에 몇가지를 고려 해야 한다.

RUN apt-get updateapt-get install은 항상 같은 RUN구문 안에 있어야 한다.

RUN apt-get update && apt-get install -y \
    package-bar \
    package-baz \
    package-foo  \
    && rm -rf /var/lib/apt/lists/*

apt-get updateRUN구문에서 단독으로 쓰면 캐시 문제가 있을 수 있고, 이어지는 apt-get install가 실패할 가능성도 있다. 예를 들면

# syntax=docker/dockerfile:1
FROM ubuntu:18.04
RUN apt-get update
RUN apt-get install -y curl

이미지가 빌드된 이후에, 모든 레이어가 도커 캐시안에 들어가게 된다. apt-get install 뒤에 구문을 추가했다고 가정해보자.

# syntax=docker/dockerfile:1
FROM ubuntu:18.04
RUN apt-get update
RUN apt-get install -y curl nginx

도커는 이전 명령어와 수정된 명령어가 동일 할 때에만 이전단계의 캐시를 사용한다. 따라서 빌드가 캐신된 버전을 사용하기 때문에 apt-get update가 실행되지 않는다. 그러므로 이 빌드는 잠재적으로 오래된 버전의 curlnginx 패키지를 얻게되는 결과를 초래할 수 있다.

RUN apt-get update && apt-get install -y 를 수행하면, 도커파일이 더이상의 코딩이나 수동작업 없이 최신 패키지 버전을 설치할 수 있다. 이 기술을 cache busting이라고 한다. 패키지 버전을 지정하여 이 캐시버스팅을 수행할 수도 있다.

RUN apt-get update && apt-get install -y \
    package-bar \
    package-baz \
    package-foo=1.3.*

버전을 이런식으로 고정하면 캐시에 무엇이 있든 상관없이 특정 버전을 검색하도록 강제할 수 있다. 또한 이 기술을 사용하면 예기치 않은 필수 패키지의 버저닝으로 인한 장애를 줄일 수 있다.

아래는 모든 적절한 권장사항을 잘 수행한 예시다.

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/*

s3cmd1.1.* 버전을 사용하도록 했다. 이미지가 만약 이전 버전을 사용하고, 새로운 버전을 지정하면 apt-get update에 캐시 버스팅을 발생시키고 새로운 버전을 설치한다. 각 라인에 패키지를 나열하면 패키지가 중복되는 오류도 방지할 수 있다.

또한 /var/lib/apt/lists를 제거하여 캐시를 적절히 정리하면 캐시가 레이어에 저장되지 않기 때문에 이미지 용량을 줄일 수 있다. RUN 구문은 apt-get update와 함께 시작하므로, 패키지 캐시는 항상 apt-get install 전에 정리될 것이다.

Debian Ubuntu에서는 자동으로 apt-get clean을 수행해주므로 이럴 필요가 없다.

Pipe

몇 몇 RUN 커맨드는 |에 의존하여 동작할 수 있다. 예를 들어

RUN wget -O - https://some.site | wc -l > /number

도커는 /bin/sh -c 커맨드를 사용하여, 이러한 명령어를 실행한다. 이 명령어는 마지막 작업의 종료코드만 확인하여 성공 실패 여부를 결정한다. 위의 예제에서 살펴보면, 이 빌드 단계는 wget 명령어가 실패하더라도, wc -l 명령어가 성공하면 새로운 이미지를 만들어 낼 것이다.

파이프의 어느 단계에서든 오류로 인해 명령이 실패하도록 하려면, set -o pipefail &&를 앞에 추가하면 된다.

RUN set -o pipefail && wget -O - https://some.site | wc -l > /number

모든 쉘이 -o pipefail을 제공하는 것은 아니므로, 아래와 같이 별도로 나눠서 실행해야 할 수도 있다.

RUN ["/bin/bash", "-c", "set -o pipefail && wget -O - https://some.site | wc -l > /number"]

CMD

https://docs.docker.com/engine/reference/builder/#cmd

CMD 는 나열되어 있는 인수와 함께, 이미지에 포함되어 있는 소프트웨어를 실행하는데 사용된다. CMD는 거의 대부분 항상 ["실행 파일", "param1", "param2"...] 와 같은 형태로 사용되어야 한다.

대부분의 경우, CMD는 bash, paython, perl과 같은 대화형 셸이 필요하다. 예를들어 CMD ["perl", "-de0"], CMD ["python"], or CMD ["php", "-a"] 등이 있다. 이러한 형태를 사용하면, docker run -it python고과 같은 것을 실행하면 바로 셸로 진입할 수 있다.

EXPOSE

https://docs.docker.com/engine/reference/builder/#expose

EXPOSE는 컨테이너가 연결을 받는 포트를 나타낸다. 따라서 애플리케이션에서 공통으로 사용되는 기존 포트를 사용해야 한다. (아파치 EXPOSE 80, 몽고 디비 EXPOSE 27017과 같이)

외부에서 접근을 위해 docker run에 플래그를 사용하여 이 포트가 어떤 포트에 연결될지 지정할 수 있다.

ENV

https://docs.docker.com/engine/reference/builder/#env

ENV를 사용하여 컨테이너가 설치하는 소프트웨어의 PATH 환경변수를 업데이트 할 수 있다. 예를 들어, ENV PATH=/usr/local/nginx/bin:$PATHCMD ["nginx"] 명령어가 실행될 수 있도록 해준다.

또한 Postgres의 PGDATA와 같이 컨테이너에 포함하려는 서비스와 관련된 필수 환경변수를 제공하는데 유용하다.

마지막으로, ENV는 일반적으로 사용되는 버전 번호를 설정하기 위해 사용할 수도 있다.

ENV PG_MAJOR=9.3
ENV PG_VERSION=9.3.4
RUN curl -SL https://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgres && …
ENV PATH=/usr/local/postgres-$PG_MAJOR/bin:$PATH

프로그램에 상수값이 있는 것과 비슷하게, ENV를 사용하면 자동적으로 컨테이너 내부의 소프트웨어 버전을 지정하는 것도 가능하다.

ENV 라인은 RUN과 동일하게 새로운 중간 레이어를 생성한다. 즉, 이후 레이어에서 환경변수를 설정해제하더라도, 이 레이어에서 계속 유지되며 해당 값이 덤프될 수 있다.

# syntax=docker/dockerfile:1
FROM alpine
ENV ADMIN_USER="mark"
RUN echo $ADMIN_USER > ./mark
RUN unset ADMIN_USER
$ docker run --rm test sh -c 'echo $ADMIN_USER'

mark

이러한 사태를 방지하고, 환경 변수를 실제로 해지 하기 위해서는 RUN을 사용하여 변수를 단일 레이어에서 설정, 사용, 해제를 하면 된다. 이 명령어는 ; &&을 사용하여 구분할 수 있다. 후자를 사용한다면, 명령이 실패한다면 도커 빌드도 실패한다.

# syntax=docker/dockerfile:1
FROM alpine
RUN export ADMIN_USER="mark" \
    && echo $ADMIN_USER > ./mark \
    && unset ADMIN_USER
CMD sh

ADD or COPY

ADD COPY 두 명령어가 기능적으로 거의 유사하지만, COPY가 일반적으로 더 사용된다. 그 이유는 ADD 보다 더 순수하기 때문이다. COPY는 단순히 컨테이너에 있는 로컬 파일을 복사할 뿐이다. 반면 ADD는 몇가지 추가적이 있다. (로컬 전용 tar 파일 해제, 원격 URL 지원 등) 따라서 ADDADD rootfs.tar.xz / 와 같은 상황에서 사용하는 것이 좋다.

컨텍스트에서 다른 파일을 사용하는 여러 단계가 Dockerfile 내부에 있을 경우, 한번에 하지말고 개별적으로 복사하는 것이 좋다. 이렇게 하면 각 단계의 빌드 캐시는 필요한 파일이 변경되었을 경우에만 무효화 (재실행)된다.

예를 들어

COPY requirements.txt /tmp/
RUN pip install --requirement /tmp/requirements.txt
COPY . /tmp/

COPY . /tmp/ 를 앞에 두는 경우보다 캐시 무효화가 더 줄어든다.

이미지 크기는 중요한 문제이므로, 원격 URL에서 패키지를 가져올때는 ADD를 사용하는 것은 권장되지 않는다. 대신 curl wget을 사용해야 한다. 이렇게 하면 파일 압축을 해제한 후 더이상 필요 없는 파일을 삭제할 수 있으며, 이미지에 다른 레이어를 추가할 필요가 없다. 예를 들어, 아래와 같은 경우는 피해야 한다.

ADD https://example.com/big.tar.xz /usr/src/things/
RUN tar -xJf /usr/src/things/big.tar.xz -C /usr/src/things
RUN make -C /usr/src/things all

이 대신,

RUN mkdir -p /usr/src/things \
    && curl -SL https://example.com/big.tar.xz \
    | tar -xJC /usr/src/things \
    && make -C /usr/src/things all

자동 tar 파일 압축 해제 기능 등이 필요하지 않은 다른 항목 (파일, 디렉토리) 에는 COPY를 쓰자.

ENTRYPOINT

https://docs.docker.com/engine/reference/builder/#entrypoint

ENTRYPOINT를 쓰는 가장 좋은 방법은 이미지의 메인 커맨드를 설정해두어, 해당 명령어를 기본으로 사용할 수 있게 하는 것이다.

ENTRYPOINT ["s3cmd"]
CMD ["--help"]

이렇게 하면 아래와 같이 실행했을 때 도움말을 볼 수 있다.

docker run s3cmd

혹은 파라미터를 바로 두어서 바로 커맨드를 실행할 수도 있다.

docker run s3cmd ls s3://mybucket

ENTRYPOINT 명령은 helper 스크립트와 함께 사용할 수 있으므로, 특정 tool 을 시작할때 위의 명령어와 유사한 방식으로 동작할 수도 있다.

예를 들어, Postgres 공식 이미지는 다음 스크립트를 ENTRYPOINT로 사용한다.

#!/bin/bash
set -e

if [ "$1" = 'postgres' ]; then
    chown -R postgres "$PGDATA"

    if [ -z "$(ls -A "$PGDATA")" ]; then
        gosu postgres initdb
    fi

    exec gosu postgres "$@"
fi

exec "$@"

helper 스크립트는 컨테이너에 복사되고, 컨테이너 시작시 ENTRYPOINT에 의해 실행된다.

COPY
Learn more about the "COPY" Dockerfile command.
 ./docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["postgres"]

이 스크립트는 유저가 Postgres를 다양한 방식으로 상호작용할 수 있도록 해준다.

단순히 Postgres를 실행할수도 있고

 docker run postgres

서버에 파라미터를 전달하여 실행할 수도있고

 docker run postgres postgres --help

또한 Bash와 같은 완전히 다른 툴에서도 실행할 수 있다.

 docker run --rm -it postgres bash

VOLUME

https://docs.docker.com/engine/reference/builder/#volume

VOLUME은 도커 컨테이너에서 만든 데이터 저장소 영역, 설정 저장소, 또는 파일이나 폴더를 노출하는데 사용해야 한다. 이미지의 변경 가능한 부분 및 사용자가 수정가능한 부분에는 VOLUME을 사용하는 것이 좋다.

USER

https://docs.docker.com/engine/reference/builder/#user

서비스를 실행하는데 별도로 권한이 필요 없다면, USER 를 사용하여 루트가 아닌 사용자로 변경해야 한다. RUN groupadd -r postgres && useradd --no-log-init -r -g postgres postgres와 같은 명령어로 유저나 그룹을 생성할 수 있다.

sudo는 문제를 일으킬 여지가 있으므로 사용하지 않는 것이 좋다. 그럼에도 sudo가 어쩔 수 없이 필요한 경우 gosu의 사용을 고려해보자.

마지막으로, 레이어와 복잡성을 줄이기 위해서는 너무 자주 USER를 사용하지 않는 것이 좋다.

WORKDIR

https://docs.docker.com/engine/reference/builder/#workdir

명확성, 그리고 신뢰성을 위해 WORKDIR은 항상 절대 경로를 사용해야 한다. 읽기 어렵고, 유지보수도 어려운 RUN cd … && do-something 대신 WORKDIR을 사용하자.

ONBUILD

https://docs.docker.com/engine/reference/builder/#onbuild

ONBUILD 명령은 현재 Dockerfile의 빌드가 완료된 후 실행된다. ONBUILD는 현재 이미지에서 파생된 하위 이미지에서 실행된다. ONBUILD 명령은 상위 Dockerfile이 하위 Dockerfile에 제공하는 명령이라고 보면 된다.

도커는 하위 Dockerfile의 명령에 앞서 ONBUILD를 수행한다.

ONBUILD는 지정된 이미지에서 빌드할 이미지가 필요할 때 유용하다. 예를 들어, Dockerfile내에서 해당 언어로 소프트웨어를 필요로 하는 이미지가 있다면, ONBUILD 명령어가 유용하다.

ONBUILD로 빌드된 이미지에는 별도 태그가 있어야 한다. (ruby:1.9-onbuild ruby:2.0-onbuild)

ONBUILDADD COPY를 넣을 때 주의하자. 새 빌드 컨텍스트에 이렇게 추가되는 리소스가 없을 경우 하위 "onbuild" 이미지가 실패할 것이다. 위에서 권장한대로 태그를 추가해서 구별하면, Dockerfile 작성자가 이를 선택할 수 있으므로 이러한 문제를 예방할 수 있다.