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 안에 추가한다.RUN
은 make
를 사용해 실제 응용프로그램을 빌드한다.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 .
Dockerfile
과hello
를 다른 분리된 디렉토리로 옮기고 마지막 빌드 캐시에 의존하지 않는 방식으로 두 번째 버전의 이미지를 빌드한다.-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
을 처리할 수 있다. Dockerfile
을 stdin
으로 처리하면 디스크나 어느 곳에든지 Dockerfile
을 따로 작성하지 않아도 된다는 점에서 유용할 때도 있다.
예를 들어 다음의 두 커맨드의 결과는 같다.
echo -e 'FROM busybox\nRUN echo "hello world"' | docker build -
docker build -<<EOF FROM busybox RUN echo "hello world" EOF
상황에 따라 최적의 방법으로 빌드를 할 수 있다.
stdin
을 통한 Dockerfile
로 이미지 빌드하기다음 문법을 사용하면 stdin
의 Dockerfile
로 빌드 컨텍스트 없이 이미지를 빌드할 수 있다. -
기호는 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.c
를 stdin
을 통한 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
을 효과적이고 유지보수에 용이하게 만들기 위해 디자인 된 것이다.
가능하다면 기초 이미지는 현재 공식 이미지를 사용해라. Alpine 이미지가 여전히 리눅스에 포함되는 강력하게 통제되고 5 MB 이하의 작은 크기를 갖고 있기 때문에 추천한다.
이미지에 라벨을 추가하여 프로젝트에 도움이 될 수 있고 라이센스 정보를 기록하여 자동화에 도움이 될 수 있고 다른 여러가지 이유로 사용할 수 있다. 각각의 라벨에는 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
문장을 \
를 이용해 여러 줄로 나누는 것은 Dockerfile
을 더 읽기 쉽고 이해하기 쉽고 수정하기 쉽게 만든다.
아마도 RUN
을 가장 많이 사용하는 응용프로그램은 apt-get
일 것이다. apt-get
를 통해 패키지를 설치하기 때문에 RUN apt-get
는 자주 보이는 커맨드이다.
업그래이드가 제한된 컨테이너에서 부모 이미지의 많은 필수 패키지를 업그래이드할 수 없기 때문에, RUN apt-get upgrade
와 dist-upgrade
를 피해야 한다. 만약 부모 이미지의 패키지가 구 버전이라면, 관리자에게 문의해야 한다. 만약 패키지 중 foo
라는 업데이트가 필요한 패키지가 있다면, apt-get install -y foo
를 사용해 자동으로 업데이트 시킬 수 있다.
RUN apt-get update
는 apt-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
가 실행되지 않기 때문에, 해당 빌드는 구 버전의 curl
과 nginx
를 받을 수 있다.
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
는 CMD ["executable", "param1", "param2" ... ]
과 같은 형식으로 주로 사용된다. 그러므로 만약 이미지가 Apache 나 Rails 와 같은 서비스를 위한것이라면, CMD ["apache2", "-DFOREGROUND"]
와 같이 실행할 수 있다. 추가적으로, 이 명령어의 형식은 모든 서비스 형식의 이미지에 추천된다.
대부분의 다른 경우에, CMD
는 bash
, python
, perl
과 같은 상호작용되는 쉘을 제공한다. 예를 들어, CMD ["perl", "-de0"]
. CMD ["python"]
, 또는 CMD ["php", "-a"]
과 같이 작성할 수 있다. 이 형식을 docker run -it python
과 같이 사용한다는 것은, 사용할 수 있는 쉘로 환경을 변경한다는 뜻이다. CMD
를 사용할 때, ENTRYPOINT
에 익숙한 사람들은 드물게 ENTRYPOINT
와 CMD ["param", "param"]
이 결합된 형식으로 사용되기도 한다.
EXPOSE
명령어는 컨테이너가 연결에 대기할 포드 번호를 명시한다. 따라서 보통, 응용 프로그램의 전통적인 포트 번호를 사용해야 한다. 예를 들어 Apache 웹 서버를 작동시키는 이미지는 EXPOSE 80
을 사용해야 하고, MongoDB 를 사용하는 이미지는 EXPOSE 27017
을 사용해야 한다.
외부에서 접근할 때, 유저는 docker run
에서 플래그로 어떻게 포트와 포트를 연결할 지 명시할 수 있다. 컨테이너 링크를 할 때, Docker 는 환경 변수를 소스에 작성해 전달할 수 있다 (MYSQL_PORT_3306_TCP
와 같이).
새로운 소프트웨어를 실행하기 쉽게 만들려면, ENV
명령어를 해당 소프트웨어가 사용하는 PATH
환경 변수를 업데이트 하는데 사용할 수 있다. 예를 들어, ENV PATH /usr/local/nginx/bin:$PATH
는 CMD ["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
와 COPY
는 기능적으로 비슷하지만 보통 다들 COPY
를 사용해야 한다고 말한다. ADD
보다 더 투명하기 때문이다. COPY
는 로컬 파일을 컨테이너로 복사해오는 기본적인 기능만 지원하지만, ADD
는 즉각적으로 보이지 않는 몇 가지 기능을 더 갖고있다 (tar 파일의 압축 해제나 원격 URL의 지원과 같은). 결과적으로, ADD
는 ADD 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 로부터 패키지를 받아오는 것은 아주 추천하지 않는 방법이다. 원격 파일 저장소로부터 패키지를 받기 위해서는 curl
과 wget
을 사용해야 한다. 그렇게 하면 이미지에 다른 레이어를 추가하지 않아서 더 이상 사용하지 않는 파일을 삭제할 수 있다. 예를 들어 다음과 같은 방법은 피해야 한다.
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
의 최고의 사용 방법은 이미지의 메인 커맨드를 설정하고 그 이미지가 그 커맨드인 것 처럼 사용하는 것이다 (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
명령어는 여러 데이터베이스 저장소 구역을 나타내고 저장소를 설정하고 파일이나 폴더를 Docker 컨테이너에 생성하기 위해 사용된다. VOLUME
을 이미지에서 변하기 쉽거나 유저에게 서비스 할 수 있는 부분에 사용하는걸 권장한다.
만약 서비스가 권한없이 실행될 수 있다면, 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
로는 항상 절대경로를 줘야 한다. 또한 읽기도 힘들고 문제도 많이 생기고 유지보수도 어려운 RUN cd .. && do-something
같은 불어난 명령어 대신 WORKDIR
을 사용할 수도 있다.
ONBUILD
커맨드는 현제 Dockerfile
이 끝난 후 실행하는 명령이다. ONBUILD
는 FROM
으로 가져온 어떤 이미지에서든지 실행된다. ONBUILD
커맨드를 부모 Dockerfile
이 자식 Dockerfile
에게 준 명령어라고 생각하자.
Docker 빌드는 ONBUILD
커맨드를 모든 자식 Dockerfile
의 어떤 커맨드를 시작하기도 전에 실행한다.
ONBUILD
로 빌드된 이미지는 별도의 태그가 필요하다. 예를 들어 ruby:1.9-onbuild
나 ruby:2.0-onbuild
와 같은 것들이다.
ONBUILD
에서 ADD
나 COPY
를 할 떄는 주의를 해야한다. "Onbuild" 된 이미지는 만약 새 빌드 컨텍스트에 추가할 리소스가 없다면 비극적으로 실패한다. 위에서 추천한 방법으로 별도의 태그를 추가하는 것은 이 것을 누그러뜨리고 Dockerfile
의 작성자로 하여금 선택을 할 수 있게 해준다.
다음은 공식 이미지의 Dockerfile
예시들이다.