Dockerfile best practices

seheon·2020년 9월 20일
8

원문

Docker 는 이미지를 Dockerfile 의 명령어들을 읽고 그에 맞게 자동으로 빌드한다. Dockerfile 은 주어진 이미지를 빌드하는데 필요한 모든 커맨드가 텍스트 형식으로 담긴 파일이다. Dockerfile 의 자세한 형식이나 어떤 명령어를 사용할 수 있는지는 Dockerfile reference 에서 자세히 설명하고 있다.

Docker 이미지는 Dockerfile 명령어의 결과로 만들어진 읽기 전용의 레이어들로 구성되어 있다. 읽기 전용의 레이어이기 때문에 이전 레이어에서 변경된 내용이 있다면 다음 레이어가 되어 그 위에 쌓이게 된다. 다음 Dockerfile 을 살펴보자.

FROM	ubuntu:18.04
COPY	. /app
RUN		make /app
CMD		python /app/app.py

각각의 명령어들은 각각의 레이어를 만든다.

  • FROM 은 Docker 이미지 ubuntu:18.04 에서 레이어를 만든다.
  • COPY 는 Docker 밖의 파일을 Docker 안에 추가한다.
  • RUNmake 를 사용해 실제 응용프로그램을 빌드한다.
  • CMD 는 컨테이너 안에서 실행할 커맨드를 지정한다.

이미지를 실행시키고 컨테이너를 만들면, 쓰기가 가능한 새로운 레이어 (컨테이너 레이어) 를 아래 레이어 위에 추가한다. 컨테이너를 실행할 때 파일을 만들거나 수정하거나 삭제하는 등의 모든 변경점들은 이 쓰기 가능한 레이어에 기록된다.

기본적인 가이드라인과 몇 가지 권고 사항들

일회용 한 컨테이너 만들기

Dockerfile 에서 정의된 이미지는 가능한 가장 일회용 (ephemeral) 한 컨테이너를 생성해야 한다. 컨테이너가 일회용 하다는 것은 컨테이너를 멈추고 삭제한 후 다시 빌드하고 최소한의 몇 가지 설정을 바꾸는 것 만으로 컨테이너를 대체할 수 있다는 걸 의미한다.

빌드 컨텍스트에 대해 이해하기

docker build 커맨드를 입력하면 현재 작업중인 디렉토리를 빌드 컨텍스트 라고 부르게 된다. 기본적으로 Dockerfile 은 여기에 위치해 있다고 가정하지만 -f 플래그로 다른 위치를 지정해줄 수도 있다. Dockerfile 이 어디에 있던지 상관 없이 현재 디렉토리에 있는 모든 파일과 디렉토리는 Docker 데몬에게 빌드 컨텍스트로 전달되게 된다.

빌드 컨텍스트 예제

빌드 컨텍스트의 디렉토리를 만들고 cd 커맨드로 이동한다. hello 라는 이름의 텍스트 파일에 "hello" 라고 적고 그 파일을 cat 하는 Dockerfile 하나를 만든다. 빌드 컨텍스트 (.) 에서 이미지를 빌드한다.

mkdir myproject && cd myproject
echo "hello" > hello
echo -e "FROM busybox\nCOPY /hello /\nRUN cat /hello" > Dockerfile
docker build -t helloapp:v1 .

Dockerfilehello 를 다른 분리된 디렉토리로 옮기고 마지막 빌드 캐시에 의존하지 않는 방식으로 두 번째 버전의 이미지를 빌드한다. -f 플래그로 Dockerfile 의 위치를 설정했고 빌드 컨텍스트의 디렉토리도 설정했다.

mkdir -p dockerfiles context
mv Dockerfile dockerfiles && mv hello context
docker build --no-cache -t helloapp:v2 -f dockerfiles/Dockerfile context

이미지를 빌드하는데 필요하지 않은 파일들을 추가하는 것은 빌드 컨텍스트와 이미지 크기를 키우게 된다. 또한 이미지를 빌드하는 시간, pull & push 하는 시간, 컨테이너의 런타임 크기가 모두 증가하게 된다. 빌드 컨텍스트의 크기를 확인하려면 Dockerfile 의 빌드할 때 표시되는 메시지에서 다음 문구를 찾으면 된다.

Sending build context to Docker daemon	187.8MB

stdin 으로 여러 Dockerfile 입력하기

Docker는 stdin 의 파이프를 통해 여러 Dockerfile 을 처리할 수 있다. Dockerfilestdin 으로 처리하면 디스크나 어느 곳에든지 Dockerfile 을 따로 작성하지 않아도 된다는 점에서 유용할 때도 있다.

예를 들어 다음의 두 커맨드의 결과는 같다.

echo -e 'FROM busybox\nRUN echo "hello world"' | docker build -
docker build -<<EOF
FROM busybox
RUN echo "hello world"
EOF

상황에 따라 최적의 방법으로 빌드를 할 수 있다.

빌드 컨텍스트를 보내지 않고 stdin 을 통한 Dockerfile 로 이미지 빌드하기

다음 문법을 사용하면 stdinDockerfile 로 빌드 컨텍스트 없이 이미지를 빌드할 수 있다. - 기호는 PATH 의 위치를 대신하고 Docker가 빌드 컨텍스트를 디렉토리가 아닌 stdin 을 통해 입력받도록 설정할 수 있다.

docker build [OPTIONS] -

다음 예제는 stdin 을 통한 Dockerfile 로 이미지를 빌드하고 데몬에게 어떤 파일도 빌드 콘텍스트로 보내지 않는다.

docker build -t myimage:latest -<<EOF
FROM busybox
RUN echo "hello world"
EOF

빌드 컨텍스트를 보내지 않는 것은 Dockerfile 이 어떤 파일도 필요하지 않을 때 데몬에게 아무 파일도 보내지 않으므로 빌드 속도를 향상시킨다는 측면에서 아주 유용할 수 있다.

만약 특정 파일을 빌드 컨텍스트에서 제외시키는 방법으로 빌드 속도를 향상시키고 싶다면 .dockerignore 에 대한 문서를 참조하면 도움이 된다.

Dockerfile 로 빌드할 때, 이 문법에서는 COPY 또는 ADD 를 사용하면 오류가 발생할 것이다. 다음 예제는 이 에러가 발생한다.

# create a directory to work in
mkdir example
cd example

# create an example file
touch somefile.txt

docker build -t myimage:latest -<<EOF
FROM busybox
COPY samefile.txt .
RUN cat /somefile.txt
EOF

# observe that the build fails
...
Step 2/3 : COPY somefile.txt .
COPY failed: stat /var/lib/docker/tmp/docker-builder249218248/somefile/txt:no such file or directory

stdin 을 통한 Dockerfile 로 로컬 빌드 컨텍스트를 빌드하기

다음 문법을 로컬 파일 시스템의 파일을 사용해 stdin 을 통한 Dockerfile 로 이미지를 빌드할 수 있다. -f (또는 --file ) 옵션으로 사용할 Dockerfile 을 설정할 수 있고 - 을 파일 이름으로 사용해 stdin 을 통해 Dockerfile 을 전달할 수 있다.

아래 예제는 현재 디렉토리 ( . ) 를 빌드 컨텍스트로 stdin 을 통해 입력받은 Dockerfile 을 사용해 이미지를 빌드한다.

docker build [OPTIONS] -f- PATH

아래 예제는 현재 디렉토리 ( . ) 를 빌드 컨텍스트로 stdin 을 통해 입력받은 Dockerfile 을 사용해 이미지를 빌드한다.

# create a directory to work in
mkdir example
cd example

# create an example file
touch somefile.txt

# build an image using the current directory as context, and a Dockerfile passed through stdin
docker build -t myimage:latest -f- . <<EOF
FROM busybox
COPY somefile.txt .
RUN cat /somefile.txt
EOF

stdin 을 통한 Dockerfile 로 원격 빌드 컨텍스트 빌드하기

다음 문법으로 원격 git 레포지토리를 stdin 을 통한 Dockerfile 로 빌드할 수 있다. -f (또는 --file ) 옵션을 사용하므로써 사용할 Dockerfile 을 지정할 수 있고 - 을 파일 이름으로 사용해 stdin 을 통해 Dockerfile 을 전달할 수 있다.

docker build [OPTIONS] -f- PATH

이 문법은 Dockerfile 이 없거나 따로 설정한 Dockerfile 을 레포지토리에 따로 fork 할 필요 없이 레포지토리의 파일로 이미지를 빌드할 수 있다. 다음 예제는 Github의 "hello-world" 레포지토리 에서 hello.cstdin 을 통한 Dockerfile 로 빌드한다.

docker build -t myimage:latest -f- https://github.com/docker-library/hello-world.git <<EOF
FROM busybox
COPY hello.c .
EOF
프로그램 안에서...

Git 레포지토리를 빌드 컨텍스트로 빌드할 때, Docker 는 로컬에 git clone 을 통해 레포지토리를 복사하고 데몬에게 빌드 컨텍스트로 넘긴다. 이 기능은 docker build 하는 호스트에서 git 의 설치가 필요하다.

.dockerignore 로 파일 제외하기

빌드와 상관 없는 파일을 .dockerignore 을 통해 원본 레포지토리의 변경 없이 제외할 수 있다. 이 파일은 .gitignore 과 비슷한 방식으로 파일을 제외한다. 자세한 내용은 .dockerignore file 을 참고하면 도움이 될 것이다.

멀티-스테이지 빌드 사용하기

멀티-스테이지(multi-stage)는 중간 레이어나 파일을 줄이려고 아둥바둥거리지 않아도 최종 이미지의 크기를 효과적으로 줄일 수 있게 해준다.

이미지는 마지막 빌드 처리 과정에서 만들어지기 때문에, 빌드 캐시의 이점을 활용해 이미지 레이어를 최소화할 수 있다.

예를 들어 만약 빌드가 여러개의 레이어를 갖고 있을 때, 수정 빈도가 낮은 레이어부터 높은 순서로 빌드 캐시를 재사용할 수 있게 정렬할 수 있다.

  • 응용프로그램을 빌드하는데 필요한 툴을 설치한다.
  • 필요한 관련 라이브러리를 설치하거나 업데이트한다.
  • 응용프로그램을 생성한다.

Go 응용프로그램을 위한 Dockerfile 은 다음과 같다.

FROM	golang:1.11-alpine AS build

# Install tools required for project
# RUN `docker build --no-cache .` to update dependencies
RUN		apk add --no-cache git
RUN		go get github.com/golang/dep/cmd/dep

# List project dependencies with Gopkg.toml and Gopkg.lock
# These layers are only re-built when Gopkg files are updated
COPY	Gopkg.lock Gopkg.toml /go/src/project/
WORKDIR	/go/src/project/
# Install library dependencies
RUN		dep ensure -vendor-only

# Copy the entire project and build it
# This layer is rebuilt when a file changes in the project directory
COPY	. /go/src/project/
RUN		go build -o /bin/project

# This result in a single layer image
FROM	scratch
COPY	--from=build /bin/project /bin/project
ENTRYPOINT	["/bin/project"]
CMD		["--help"]

필요없는 패키지를 설치하지 말기

복잡도, 의존도, 파일 크기를 줄이고 빌드 시간을 줄이기 위해 추가적인, 필요 없는 패키지를 "있으면 좋은!" 이라는 의미로 설치하지 말아야 한다. 예를 들어 데이터베이스 이미지에 텍스트 에디터를 설치하면 안된다.

응용프로그램 나누기

각각의 컨테이너들은 하나의 주제가 있어야 한다. 응용프로그램을 여러 개의 컨테이너로 나누는 것은 수평적인 구조를 만들기 쉽고 컨테이너를 재활용하기 쉽게 한다. 실제로 웹 응용프로그램 스택은 각각의 고유한 이미지를 가지고 웹, 데이터베이스, 그리고 캐시를 관리하는 세 가지 컨테이너로 나뉠 수 있다.

각각의 컨테이너를 하나의 프로세스로 제한하는 것은 좋은 규칙이지만 견고하고 빠른 규칙은 아니다. 예를 들어 컨테이너가 프로세서에 의해 생성될 때, 몇 개의 프로그램도 각자의 규칙에 의해 추가적인 프로세스를 생성할 수 있다. 실제로 Celery는 여러 워커 프로세스를 생성할 수 있고, Apache도 요청에 따라 프로세스를 하나씩 생성할 수 있다.

컨테이너를 최대한 깨끗하고 모듈화 된 구조를 유지할 수 있는 최고의 판단을 해라. 만약 컨테이너가 다른 컨테이너에게 의존한다면, 컨테이너들 간 통신을 보장하기 위해 Docker 컨테이너 네트워크를 사용할 수도 있다.

레이어의 수 최소화 하기

Docker 의 과거버전에 빌드의 성능을 보장하기 위해 이미지의 레이어 수를 최소화 하는 것이 중요했었다. 다음 기능은 이 제한을 줄이기 위해 추가되었다.

  • RUN, COPY, ADD 의 세 가지 명령어는 레이어를 만든다. 다른 명령어는 임시 중간 이미지를 만들고 빌드의 크기를 증가시키지 않는다.
  • 가능하면 멀티-스테이지 빌드를 사용하고 최종 이미지에서 하나의 복사만 하게 되면 여러 툴과 여러 중간 빌드 스테이지에 대한 디버그 정보를 최종 이미지 크기의 증가 없이 포함할 수 있다.

여러 줄의 인자들 정렬하기

가능하다면 여러 줄의 인자들을 문자숫자로 정렬하는 것이 나중에 보기 편하다. 정렬을 하게 되면 중복 패키지를 피할 수 있고 리스트를 업데이트하기 쉽게 만들 수 있다. 또한 PR을 더 읽기 쉽고 리뷰하기 편하게 만들 수도 있다. 빈 칸을 \ 전에 넣으면 더 좋다.

다음은 buildpack-deps 이미지에서 가져온 예시이다.

RUN	apt-get update && apt-get install -y \
  bzr \
  cvs \
  git \
  mercurial \
  subversion

빌드 캐시의 영향력

이미지를 빌드할 때, Docker는 Dockerfile 에 있는 명령어들을 하나하나 순서대로 실행시킨다. 각각의 명령어들이 검사되기 때문에, Docker는 새로운 이미지를 만드는 대신 재사용할 수 있는 캐시 이미지가 존재하는지 확인할 수 있다.

만약 모든 캐시의 사용을 원하지 않는다면, docker build 커맨드에서 --no-cache=true 옵션을 사용할 수 있다. 그러나 만약 Docker가 캐시를 사용하게 한다면 맞는 이미지에 대한 캐시를 찾을 수 있는지 없는지 이해하는 것은 아주 중요하다. Docker의 기본적인 규칙은 다음과 같다.

  • 이미 캐시에 있는 부모 이미지에서 시작했다면, 다음 명령어는 해당 이미지의 모든 자식 이미지들과 모두 같은 명령어인지 비교한다. 만약 다르다면, 그 캐시는 제외된다.
  • 대부분의 경우, Dockerfile 의 명령어들을 자식 이미지 중 하나와 비교하는 것으로 충분할 때도 있다. 그러나 특정 명령어는 더 자세한 분석이 필요하다.
  • ADD, COPY 명령어에서, 이미지의 파일들은 검사되고 각각의 파일마다 체크섬을 계산한다. 파일의 마지막 수정 시간과 마지막 접근 시간은 체크섬에 고려되지 않는다. 캐시를 찾아볼 때, 체크섬은 기존 이미지들의 체크섬과 비교된다. 만약 파일의 각각의 파일이나 메타데이터 등 어떤 내용이 변경되었다면, 그 캐시는 제외된다.
  • ADD, COPY 커맨드에서, 캐시 체크는 컨테이너 안에 있는 파일은 비교해보지 않는다. 예를 들어, RUN apt-get -y update 커맨드를 실행할 때, 컨테이너로 업데이트된 파일은 캐시가 존재하는지 결정할 때 고려할 대상이 아니다. 이러한 경우, 커맨드 문자열만 비교하게 된다.

캐시가 제외되고나면, Dockerfile 의 모든 하위 명령어들은 새로운 이미지를 만들고 기존 캐시는 사용되지 않는다.

Dockerfile 명령어

다음 권고 사항들은 Dockerfile 을 효과적이고 유지보수에 용이하게 만들기 위해 디자인 된 것이다.

FROM

가능하다면 기초 이미지는 현재 공식 이미지를 사용해라. Alpine 이미지가 여전히 리눅스에 포함되는 강력하게 통제되고 5 MB 이하의 작은 크기를 갖고 있기 때문에 추천한다.

LABEL

이미지에 라벨을 추가하여 프로젝트에 도움이 될 수 있고 라이센스 정보를 기록하여 자동화에 도움이 될 수 있고 다른 여러가지 이유로 사용할 수 있다. 각각의 라벨에는 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-data="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-data="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

길고 복잡한 RUN 문장을 \ 를 이용해 여러 줄로 나누는 것은 Dockerfile 을 더 읽기 쉽고 이해하기 쉽고 수정하기 쉽게 만든다.

APT-GET

아마도 RUN 을 가장 많이 사용하는 응용프로그램은 apt-get 일 것이다. apt-get 를 통해 패키지를 설치하기 때문에 RUN apt-get 는 자주 보이는 커맨드이다.

업그래이드가 제한된 컨테이너에서 부모 이미지의 많은 필수 패키지를 업그래이드할 수 없기 때문에, RUN apt-get upgradedist-upgrade 를 피해야 한다. 만약 부모 이미지의 패키지가 구 버전이라면, 관리자에게 문의해야 한다. 만약 패키지 중 foo 라는 업데이트가 필요한 패키지가 있다면, apt-get install -y foo 를 사용해 자동으로 업데이트 시킬 수 있다.

RUN apt-get updateapt-get install 과 항상 같은 RUN 구문에서 같이 사용해야 한다. 예를 들어보자.

RUN	apt-get update && apt-get install -y \
	package-bar \
	package-baz \
	package-foo

apt-get update 하나만 RUN 으로 실행하면 캐시 관련 문제를 만들고 하위 apt-get install 명령어가 실패한다. 예를 들어 다음과 같은 Dockerfile 을 살펴보자.

FROM	ubuntu:18.04
RUN		apt-get update
RUN		apt-get install -y curl

이미지를 빌드한 후, 모든 레이어는 Docker 캐시에 있다. 나중에 apt-get install 에 패키지를 추가했다고 가정해보자.

FROM	ubuntu:18.04
RUN		apt-get update
RUN		apt-get install -y curl nginx

Docker 는 초기와 그 후 수정된 명렁어를 동일하게 인식하고 이전 단계에서 발생한 캐시를 재사용한다. apt-get update 의 결과는 빌드가 캐시된 버전을 사용하기 때문에 실행되지 않는다. apt-get update 가 실행되지 않기 때문에, 해당 빌드는 구 버전의 curlnginx 를 받을 수 있다.

RUN apt-get update && apt-get install -y 를 사용하면 Dockerfile 이 추가적인 코딩이나 매뉴얼 없이도 최신 버전의 패키지를 확실히 설치한다. 이 기술은 "cache busting" (부서지는 캐시) 으로 알려져있다. Cache-busting 은 패키지의 버전을 특정하므로써 처리할 수도 있다. 이 방법은 "version pinning" (버전 안전핀) 으로 알려져있다.

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

Version pinning 은 캐시와 상관 없이 특정 버전을 빌드에게 강제한다. 이 기술은 필요 패키지의 예상치 못한 변화에 의한 실패를 줄일 수 있다.

다음은 권고 사항이 잘 지켜진 RUN 명령어다.

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

s3cmd 는 버전이 1.1.* 로 특정되었다. 만약 이미지가 이전에 하위 버전을 사용했다면, 새로운 버전을 사용하는 것은 apt-get update 의 cache bust 를 발생시키고 새 버전을 확실하게 설치한다. 패키지를 각 줄에 나열하는 것은 패키지를 중복 작성하는 것을 방지해준다.

추가적으로, apt 의 캐시를 삭제하고싶다면 /var/lib/apt/lists 를 삭제하므로써 이미지의 크기를 줄일 수 있다. RUN 명령어를 apt-get update 로 시작하므로써 패키지 캐시는 항상 apt-get install 전에 재설정된다.

공식 Debian, Ubuntu 이미지는 자동으로 apt-get clean 을 실행한다. 따라서 추가적인 명령이 필요 없다.

파이프 사용하기

몇몇의 RUN 커맨드는 한 커맨드의 결과를 다른 커맨드의 입력으로 사용하는 파이프 기능에 의존하기도 한다. 파이프는 | 를 통해 사용할 수 있다. 다음 예제를 살펴보자.

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

Docker 는 위 커맨드를 파이프의 마지막 커맨드의 반환 코드로 커맨드의 실행 결과를 판단하는 /bin/sh -c 인터프리터로 실행시킨다. 예를 들어 위의 빌드는 wget 커맨드가 실패해도 wc -l 커맨드가 성공했다면 성공적으로 마무리되고 새로운 이미지를 만들어낸다.

만약 파이프의 어느 단계에서든지 에러를 반환할 때 커맨드의 결과를 실패로 처리하고 싶다면, set -o pipefail && 를 앞에 추가하므로써 예상치 못한 오류가 발생해도 빌드를 성공했다고 처리하는 상황을 방지할 수 있다.

RUN	set -o pipefail && wget -O - https://some.site | wc -l > /number
모든 쉘이 -o pipefail 옵션을 제공하는건 아니다

Debian 파생 이미지의 dash 쉘의 경우, pipeshell 옵션을 제공하는 쉘을 선택하기 위해 다음과 같은 exec 형식을 RUN 에 사용하는걸 고려해볼 수 있다.

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

CMD

CMD 명령어는 이미지에 포함된 소프트웨어를 여러 매개변수와 실행하는데 사용된다. CMDCMD ["executable", "param1", "param2" ... ] 과 같은 형식으로 주로 사용된다. 그러므로 만약 이미지가 Apache 나 Rails 와 같은 서비스를 위한것이라면, CMD ["apache2", "-DFOREGROUND"] 와 같이 실행할 수 있다. 추가적으로, 이 명령어의 형식은 모든 서비스 형식의 이미지에 추천된다.

대부분의 다른 경우에, CMDbash, python, perl 과 같은 상호작용되는 쉘을 제공한다. 예를 들어, CMD ["perl", "-de0"]. CMD ["python"], 또는 CMD ["php", "-a"] 과 같이 작성할 수 있다. 이 형식을 docker run -it python 과 같이 사용한다는 것은, 사용할 수 있는 쉘로 환경을 변경한다는 뜻이다. CMD 를 사용할 때, ENTRYPOINT 에 익숙한 사람들은 드물게 ENTRYPOINTCMD ["param", "param"] 이 결합된 형식으로 사용되기도 한다.

EXPOSE

EXPOSE 명령어는 컨테이너가 연결에 대기할 포드 번호를 명시한다. 따라서 보통, 응용 프로그램의 전통적인 포트 번호를 사용해야 한다. 예를 들어 Apache 웹 서버를 작동시키는 이미지는 EXPOSE 80 을 사용해야 하고, MongoDB 를 사용하는 이미지는 EXPOSE 27017 을 사용해야 한다.

외부에서 접근할 때, 유저는 docker run 에서 플래그로 어떻게 포트와 포트를 연결할 지 명시할 수 있다. 컨테이너 링크를 할 때, Docker 는 환경 변수를 소스에 작성해 전달할 수 있다 (MYSQL_PORT_3306_TCP 와 같이).

ENV

새로운 소프트웨어를 실행하기 쉽게 만들려면, ENV 명령어를 해당 소프트웨어가 사용하는 PATH 환경 변수를 업데이트 하는데 사용할 수 있다. 예를 들어, ENV PATH /usr/local/nginx/bin:$PATHCMD ["nginx"] 가 잘 작동하는걸 보장해준다.

ENV 명령어는 또한 Postgres 의 PGDATA 와 같이 컨테이너에 필수적인 환경 변수를 제공하는데 아주 유용하다.

마지막으로, ENV 명령어는 보통 버전 번호를 설정해 버전 충돌 관리에 자주 사용된다.

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

프로그램에 상수 변수를 갖고있는 것과 비슷하게 (값을 하드코딩하는 것을 방지하기 위해), ENV 명령어는 컨테이너의 소프트웨어 버전을 마법과 같이 자동으로 바꿔준다.

각각의 ENV 명령줄은 RUN 커맨드와 같이 새 중간 레이어를 만든다. 이것은 나중에 환경 변수를 지정해제하지 않는다면, 그 변수는 끝까지 레이어에 남아있고 그 값은 버려지지 않는다는 것을 뜻한다. 다음과 같은 Dockerfile 에서 이 내용을 테스트해볼 수 있다.

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 명령어를 사용해 환경 변수를 설정하고 사용하고 지정 해제를 한 번에 할 수 있다. 커맨드를 ; 또는 && 로 나눌 수 있다. 만약 여러 커맨드를 사용하고 그 중 하나의 커맨드가 실패한다면, docker build 역시 실패 코드를 반환한다. 이건 보통 좋은 개념이다. \ 를 리눅스 Dockerfile 에서 사용하면 가독성을 높일 수 있다. 또한 모든 커맨드를 쉘 스크립트에 넣을 수 있고 실행하기 위해 RUN 커맨드를 사용할 수 있다.

FROM	alpine
RUN		export ADMIN_USER="mark" \
		&& echo $ADMIN_USER > ./mark \
		&& unset ADMIN_USER
CMD		sh
docker run --rm test sh -c 'echo $ADMIN_USER'

ADD or COPY

ADDCOPY 는 기능적으로 비슷하지만 보통 다들 COPY 를 사용해야 한다고 말한다. ADD 보다 더 투명하기 때문이다. COPY 는 로컬 파일을 컨테이너로 복사해오는 기본적인 기능만 지원하지만, ADD 는 즉각적으로 보이지 않는 몇 가지 기능을 더 갖고있다 (tar 파일의 압축 해제나 원격 URL의 지원과 같은). 결과적으로, ADDADD rootfs.tar.xz / 과 같이 tar 파일을 자동으로 압축 해제해 이미지에 추가하는데 사용하는 것이 가장 좋다.

만약 컨텍스트의 다른 파일을 사용하는 여러 개의 Dockerfile 을 갖고있다면, 한번에 COPY 하는 것 보다 개별적으로 COPY 해야 한다. 이것은 각각의 단계의 필요한 파일이 바뀌었을 때, 빌드 캐시가 무효화 (그 단계가 다시 실행되게 강제하는 것) 되는 것을 보장해준다.

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

COPY . /tmp/RUN 앞에 두었을 때 보다 결과적으로 캐시 무효화가 더 적다.

이미지 사이즈 측면에서, ADD 를 원격 URL 로부터 패키지를 받아오는 것은 아주 추천하지 않는 방법이다. 원격 파일 저장소로부터 패키지를 받기 위해서는 curlwget 을 사용해야 한다. 그렇게 하면 이미지에 다른 레이어를 추가하지 않아서 더 이상 사용하지 않는 파일을 삭제할 수 있다. 예를 들어 다음과 같은 방법은 피해야 한다.

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

대신 다음과 같은 방법을 사용할 수 있다.

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

다른 ADD 의 tar 압축 해제 기능이 필요 없는 디렉토리나 파일들은 COPY 로 복사해야 한다.

ENTRYPOINT

ENTRYPOINT 의 최고의 사용 방법은 이미지의 메인 커맨드를 설정하고 그 이미지가 그 커맨드인 것 처럼 사용하는 것이다 (CMD 로 기본 플래그를 설정할 수 있다).

커맨드 라인 툴인 s3cmd 의 이미지 예제를 살펴보자.

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

이제 이 이미지는 다음과 같은 커맨드로 s3cmd 의 도움 내용을 볼 수 있다.

$ docker run s3cmd

또는 커맨드를 실행하기 위해 파라미터를 사용할 수 있다.

$ docker run s3cmd ls s3://mybucket

이것은 파일 이름이 실제 프로그램으로 가는 레퍼런스의 이름으로도 사용되고 커맨드로도 사용되므로 아주 유용하다.

ENTRYPOINT 명령어는 또한 위 커맨드와 비슷한 방법으로 함수처럼 사용되는 것 처럼 헬프 스크립으로도 사용할 수 있다.

예를 들어 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 "$@"

PID 1 로 응용프로그램 구성하기

스크립트는 exec Bash 커맨드를 사용하기 때문에 마지막으로 실행되는 응용 프로그램은 컨테이너의 PID 1이 된다. 이것은 응용 프로그램이 컨테이너에게 보낸 Unix 신호를 받을 수 있다는 뜻이다. 자세한 내용은 ENTRYPOINT 레퍼런스 를 참고해라.

헬프 스크립은 컨테이너로 복사되고 컨테이너의 시작에서 ENTRYPOINT 로 실행된다.

COPY	./docker-entrypoint.sh/
ENTRYPOINT	["/docker-entrypoint.sh"]
CMD	["postgres"]

이 스크립트는 유저가 Postgres 를 여러 방법으로 사용할 수 있게 해준다.

$ docker run postgres

Postgres 를 실행하고 파라미터를 서버에게 보낼 수 있다.

$ docker run postgres postgres --help

마지막으로, Bash 와 같은 완전히 다른 툴을 실행하는데 사용될 수도 있다.

$ docker run --rm -it postgres bash

VOLUME

VOLUME 명령어는 여러 데이터베이스 저장소 구역을 나타내고 저장소를 설정하고 파일이나 폴더를 Docker 컨테이너에 생성하기 위해 사용된다. VOLUME 을 이미지에서 변하기 쉽거나 유저에게 서비스 할 수 있는 부분에 사용하는걸 권장한다.

USER

만약 서비스가 권한없이 실행될 수 있다면, USER 을 통해 root 가 아닌 유저로 변경해야 한다. Dockerfile 에서 유저를 만들고 그룹에 넣는 것은 다음과 같다.

RUN groupadd -r postgres && useradd --no-log-init -r -g postgres postgres

고정된 UID/GID 를 고려하자

이미지의 유저와 그룹은 결정되지 않은 UID/GID 로 할당되었고 "다음" UID/GID 는 이미지의 빌드와 관련이 없다. 따라서 만약 그것이 민감한 문제라면, 고정된 UID/GID 를 고려해볼 수 있다.

몇몇개의 극소수의 파일에서 발생하는 Go 의 패키지 압축 버그 때문에, 엄청나게 큰 UID 를 Docker 컨테이너에 만드는 것은 컨테이너 레이어의 /var/log/faillog 가 널 (\0) 문자로 가득 차기 때문에 디스크 과부하가 올 수 있다. 이 문제를 회피하는 방법은 유저를 추가할 때 --no-log-init 플래그를 추가하는 것이다. Debian/Ubuntu 의 adduser 명령어는 이 플래그를 지원하지 않는다.

sudo 를 설치하거나 사용하는 등 예상하지 못하는 TTY 나 문제가 발생할 수 있는 신호가 발생하는 행동은 피해야 한다. 만약 sudo 와 같은 데몬을 root 로 초기화하지만 root 로 동작하지 않는 기능이 절대적으로 필요하다면, "gosu" 를 사용하는 것을 고려해보자.

마지막으로, 레이어와 복잡도를 감소시키기 위해, USER 를 자주 바꾸는 것 또한 피해야 한다.

WORKDIR

투명성과 신뢰성을 위해, WORKDIR 로는 항상 절대경로를 줘야 한다. 또한 읽기도 힘들고 문제도 많이 생기고 유지보수도 어려운 RUN cd .. && do-something 같은 불어난 명령어 대신 WORKDIR 을 사용할 수도 있다.

WORKDIR

ONBUILD

ONBUILD 커맨드는 현제 Dockerfile 이 끝난 후 실행하는 명령이다. ONBUILDFROM 으로 가져온 어떤 이미지에서든지 실행된다. ONBUILD 커맨드를 부모 Dockerfile 이 자식 Dockerfile 에게 준 명령어라고 생각하자.

Docker 빌드는 ONBUILD 커맨드를 모든 자식 Dockerfile 의 어떤 커맨드를 시작하기도 전에 실행한다.

ONBUILD 로 빌드된 이미지는 별도의 태그가 필요하다. 예를 들어 ruby:1.9-onbuildruby:2.0-onbuild 와 같은 것들이다.

ONBUILD 에서 ADDCOPY 를 할 떄는 주의를 해야한다. "Onbuild" 된 이미지는 만약 새 빌드 컨텍스트에 추가할 리소스가 없다면 비극적으로 실패한다. 위에서 추천한 방법으로 별도의 태그를 추가하는 것은 이 것을 누그러뜨리고 Dockerfile 의 작성자로 하여금 선택을 할 수 있게 해준다.

공식 이미지의 예제들

다음은 공식 이미지의 Dockerfile 예시들이다.

참고자료

0개의 댓글