본문 바로가기

서버운영 (TA, ADMIN)/인프라

[Container] 도커 알아보기(4) - Dockerfile 개념 및 인스트럭션

출처 - https://nirsa.tistory.com/category/DevOps/Docker

 

1편 (RUN,CMD,ENTRYPOINT) nirsa.tistory.com/66?category=868315

2편 (ONBUILD, STOPSIGNAL, HEATHCHECK) nirsa.tistory.com/68?category=868315

3편 (COPY, ADD, ENV, ARG, WORKDIR) nirsa.tistory.com/69?category=868315

4편 (USER, LABEL, EXPOSE, VOLUME) nirsa.tistory.com/70?category=868315


Dockerfile이란?

도커는 기본적으로 이미지가 있어야 컨테이너를 생성하고 동작시킬수 있다. dockerfile은 필요한 최소한의 패키지를 설치하고 동작하기 위한 자신만의 설정을 담은 파일이고, 이 파일로 이미지를 생성(빌드)하게 된다.

 

패키지 설치, 환경 변수 변경, 설정 파일 변경등 다양한 작업을 하나하나 컨테이너를 만들고 설정을 적용할 필요 없이 dockerfile을 사용하여 적용할 수 있고, 작업자의 실수로 인한 설정 누락 예방 등의 장점이 있다.

FROM centos:7

RUN touch /etc/yum.repos.d/nginx.repo && echo -e '[nginx]\nname=nage repo\nbaseurl=http://nginx.org/packages/centos/7/$basearch/\ngpgcheck=0\nenabled=1' > /etc/yum.repos.d/nginx.repo

RUN yum -y install nginx

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]
$ docker build -t web_image .

위에서 빌드할때 이미지의 이름을 web_img 라는 이미지가 생성된다. 단순히 패키지 설치뿐만 아니라 컨테이너 안에서의 환경 변수나 여러가지 설정들을 할 수 있다.

Dockerfile 명령어 종류

명령은 대소문자 상관없다. DB에서 가독성을 높이기 위해 관례적으로 명령들을 대문자 사용하는 것과 같다.

인스트럭션 설명
FROM 빌드하는 이미지와 기반 이미지를 지정 (베이스 이미지 지정)
RUN 이미지를 빌드할 때 컨테이너에서 실행할 명령어를 지정 (명령 실행)
COPY 호스트에서 컨테이너로 파일 및 디렉토리를 복사 (파일 복사)
ADD COPY의 기능에 압축 파일 자동 해제 및 URL 로부터 컨테이너에 파일 및 디렉토리 추가.
운영 체제를 담은 기반 이미지를 만들 때처럼 특수한 경우에 활용 (파일/디렉토리 추가)
CMD 컨테이너에서 포어그라운드로 실행할 명령어 정의 (컨테이너 실행 명령)
ENTRYPOINT 컨테이너를 실행 가능 파일로 사용할 때 정의하는 명령. CMD와 ENTRYPOINT를 함께 사용할 수 있다. (컨테이너 실행 명령)
ARG docker image build를 실행할때 사용하는 변수 (Dockerfile에서의 변수)
ENV 컨테이너 안의 환경 변수를 정의 (환경변수)
EXPOSE 컨테이너가 노출하는 포트 (포트 익스포트)
VOLUME 호스트나 다른 컨테이너에서 마운트할 수 있는 포인트를 생성 (볼륨 마운트)
LABEL 이미지에 추가하는 메타데이터 (라벨 설정)
STOPSIGNAL 컨테이너에 전달되면 컨테이너를 종료하는 시스템 시그널 설정 (시스템 콜 시그널 설정)
HEALTHCHECK 컨테이너 안에서 명령을 실행하고 그 결과를 헬스 체크에 사용 (컨테이너의 헬스 체크)
USER 컨테이너 실행시 컨테이너 사용자. 이미지 빌드시 USER 정의 뒤에 나오는 RUN 인스트럭션도 해당 사용자 권한으로 실행된다
WORKDIR 컨테이너의 작업 디렉토리
ONBUILD 컨테이너 안에서 실행되는 명령을 정의한다. 이미지에서는 실행되지 않는다. ONBUILD를 정의한 이미지를 기반 이미지로 삼아 다른 이미지를 빌드할 때 실행된다 (빌드 완료 후 생성된 이미지가 다른 dockerfile에서 FROM 으로 불러질때 실행되는 명령)

Docker build와 Image layer

여러 패키지 설치나 설정 등을 저장한 dockerfile을 빌드할때(이미지파일로 변환시킬때)는 layer 구조를 보이는데, 이미지가 계층적으로 하나씩 쌓이면서 빌드된다.

# Step1
FROM centos:7

# Step2
RUN touch /etc/yum.repos.d/nginx.repo && echo -e '[nginx]\nname=nage repo\nbaseurl=http://nginx.org/packages/centos/7/$basearch/\ngpgcheck=0\nenabled=1' > /etc/yum.repos.d/nginx.repo

# Step3
RUN yum -y install nginx

# Step4
EXPOSE 80

# Step5
CMD ["nginx", "-g", "daemon off;"]

각각의 커맨드들이 Step으로 구분된다. build 로그를 자세히 보면 이미지 id가 보이는데, 스탭별로 이미지가 생성됨을 알 수있다.

docker build -t [생성할 이미지명] .
docker build -t [생성할 이미지명] -f [dockerfile 파일명] .

dockerfile을 build했을때 각 스탭별로 이미지가 생성되며 layer 구조를 이루게 된다. docker container run 명령을 실행하면 가장 마지막에 container layer를 생성하게 된다. 컨테이너 레이어는 컨테이너를 삭제하면 같이 삭제된다.

 

계층적으로 나누어진 이미지들이 어떻게 컨테이너를 실행하면 하나의 파일 시스템으로 보이는 것일까? 하나로 보이는 이유는 모든 레이어들이 준비되었다면 유니온 파일 시스템(Union File System)을 사용하여 여러개로 나누어져 있는 파일 시스템을 하나의 파일 시스템으로 마운트하여 실제 사용했을때는 하나로 보이게 되기 때문이다.


Dockerfile과 같은 디렉토리에 있는 모든 파일들을 컨텍스트(context)라고 한다. docker build를 사용하여 이미지를 생성할때 컨텍스트 모두를 Docker 데몬에 전송하기 때문에 / 디렉토리에 Dockerfile을 생성하면 시스템 처리 속도가 굉장히 느려질 수 있다. 이러한 이유로 Dockerfile은 따로 디렉토리를 새로 생성하는 것을 권장한다.

 

컨텍스트에서 파일이나 디렉토리를 제외할때 Dockerfile과 같은 경로에 .dockerignore 파일을 생성하여 제외하고 싶은 파일이나 디렉토리를 지정해줄수 있다.

1. RUN

RUN 명령은 패키지 설치 등에 사용되는데, 조금더 특징지어 말한다면 RUN 명령은 image layer를 만들어 낸다.

 

/bin/sh 또는 exec 형식으로 작성할 수 있는데 exec 형식은 JSON 배열로 지정된다.

FROM centos:7

# nginx 설치 (/bin/sh 예시)
RUN yum -y install nginx

# nginx 설치 (exec 형식 예시)
RUN ["/bin/bash", "-c", "yum -y install nginx"]

기본적으로 /bin/sh -c 를 통해 실행되기 때문에 yum -y install nginx만 써도 정상적으로 실행된다. 이미지에 /bin/sh가 없을 경우 exec 형식을 사용하면 되고, 쉘을 경유하지 않고 직접 실행되기 때문에 환경변수 등을 지정할 수 없다.

 

RUN 명령은 되도록 한줄로 작성하는 것이 좋다. RUN 명령은 image layer를 생성해나가는 과정이기 때문에 같은 결과를 가져오더라도 RUN을 여러줄로 작성하면 image layer가 여러개 생성되고, RUN을 한줄로 작성하면 image layer가 1개로 생성된다.

FROM centos:7

RUN yum -y install httpd
RUN yum -y install php
RUN yum -y install mysql
FROM centos:7

RUN yum -y install httpd php mysqld

2. CMD, ENTRYPOINT

CMD와 ENTRYPOINT 명령은 두개를 비교해가며 보는게 이해하기 편하다. 두가지의 명령 모두 이미지를 바탕으로 생성된 컨테이너에서 사용된다. (docker run로 생성/실행 또는 docker start로 실행시)

 

centos 7버전을 베이스로 하고 nginx를 설치한다. 컨테이너에서는 nginx가 실행되어야 한다. 그럴때 쓰는 것이 CMD 또는 ENTRYPOINT이다. 실서버에 centos 7을 깔고, nginx를 설치한 후 system start nginx를 입력해주는 것과 같다.

FROM centos:7

RUN touch /etc/yum.repos.d/nginx.repo && echo -e '[nginx]\nname=nage repo\nbaseurl=http://nginx.org/packages/centos/7/$basearch/\ngpgcheck=0\nenabled=1' > /etc/yum.repos.d/nginx.repo

RUN yum -y install nginx

# nginx foreground 실행
CMD ["nginx", "-g", "daemon off;"]

CMD와 ENTRYPOINT의 차이는 docker run을 사용하여 새로운 명령을 지정한 경우 이 명령이 실행되는지 안되는지의 차이이다.

 

CMD: 컨테이너가 실행될 때 명령어 및 인자값 전달하여 실행, 단 docker run 명령에 쉘 명령어 및 인자값 전달할 경우 CMD에 작성된 명령어와 인자값을 무시된다(즉, 우선순위는 docker run > CMD)

FROM centos:7

CMD ["/bin/ls", "-lh", "/root"]

 

ENTRYPOINT: 컨테이너가 실행될때 명령어 및 인자값 전달하여 실행, 단 docker run 명령에 쉘 명령어 및 인자값 전달할 경우 쉘 명령어는 영향을 받지 않으며, 인자값만 영향을 받는다. (즉, 우선순위는 ENTRYPOINT(인자값) < docker run < ENTRYPOINT(명령어))

 

ENTRYPOINT는 container run의 --entrypoint="실행명령" 옵션을 사용하여 Dockerfile에 정의한 ENTRYPOINT를 무시할 수 있다. 아래는 --entrypoint 옵션을 사용하여 ent_hw 이미지를 생성 및 실행하며 cat /etc/hosts를 수행하는 내용이다.

FROM centos:7

ENTRYPOINT ["/bin/ls", "-lh", "/root"]
$ docker container run -it --entrypoint="cat" ent_hw /etc/hosts

 

CMD, ENTRYPOINT 정리

CMD는 명령과 인자값이 변경될 수 있고, 컨테이너에서 명령 설정하지 않을시 CMD에 기재된 명령을 기본값으로 실행한다. ENTRYPOINT는 명령 수정이 불가능하여 사용자에 의해 변경되지 않고 고정적으로 실행될 명령은 ENTRYPOINT를 사용하는 것이 좋다.

 

간혹 override라는 단어가 등장하는데, 일반적으로 override는 물려받은 값(변수 등)을 다르게 만들어 생성하는 개념이다. CMD에 선언한 명령을 ls라고 선언하고 container run을 사용할때 ps를 사용하거나, ENTRYPOINT에서 선언한 명령을 --entrypoint=옵션을 사용하여 변경하는 경우 등 기존에 상속받은 정해진 값을 변경하여 사용할때 override를 했다고 한다.

4. ONBUILD

ONBUILD는 조금 특이하게 처음 사용한 Dockerfile에서 빌드할때(이미지 생성) 실행되는 명령이 아니다. ONBUILD 명령을 사용했던 이미지를, 다른 Dockerfile에서 FROM image를 사용하여 빌드했을때 동작한다.

 

일반적으로 사용하는 명령은 아니지만 웹서버를 구동시키기 위한 이미지를 만들고 ONBUILD ADD web.tar /var/www/html과 같이 미리 정의해둔뒤, 개발자들이 해당 경로에 웹서버에 필요한 코드들을 해당 경로에 web.tar를 만들어서 빌드 후 실행시키는 케이스 등에서 사용한다고 한다.

5. STOPSIGNAL

docker container stop 명령을 입력하면 Docker 데몬이 컨테이너에게 signal을 보내 중지하는데, 기본적으로 STOPSIGNAL을 명시하지 않을 경우 SIGTERM을 사용하게 된다. 즉 container stop 할때에 보낼 시그널을 지정할 수 있다.

STOPSIGNAL [시그널]  ## 시그널 번호 또는 이름

docker container stop 실행시 SIGTERM signal을 받은 컨테이너가 프로세스를 정상적으로 종료할 수 있을때까지 기다리게 되는데, 지정된 시간 (default 10sec, 사용자 지정 가능)동안 종료되지 않으면 SIGKILL을 전송한다.

 

일반적으로 많이 사용하지는 않지만, github에서는 nginx를 사용해 유닉스 소켓을 사용하면 container stop 시 Docker 데몬이 컨테이너에게 SIGTERM signal을 보내 nginx가 중단되고 모든 연결이 한번에 끊어지는 현상이 발생하여 비정상적인 종료가 된다고 한다. 때문에 STOPSIGNAL SIGQUIT을 사용하는게 옳다는 토론이 벌어지고 있기도 하다.

6. HEALTHCHECK

컨테이너의 HEALTHCHECK를 사용하여 컨테이너의 프로세스 상태를 체크할 수 있고, 두가지 방법이 있다.

  - HEALTHCHECK [OPTIONS] CMD command (컨테이너 내부에서 명령 실행하여 컨테이너 상태 확인, 이 방법을 통해 웹페이지 등 확인 가능)

  - HEALTHCHECK NONE (베이스 이미지에서 상속된 상태 확인을 비활성화)

 

또한 Dockerfile에서 HEALTHCHECK는 하나의 명령만이 유효하고, 여러개가 있다면 가장 마지막에 선언된 HEALTHCHECK가 적용된다.

옵션 설명 기본값
--interval=DURATION 헬스 체크 간격 30s
--timeout=DURATION 타임 아웃 30s
--retries=N 타임 아웃 횟수 3

 

HEALTHCHECK의 처음 상태는 starting 이고, HEALTHCHECK가 통과될때마다 healthy(이전 상태와 상관없이) 된다. 그리고 옵션에 정한 일정 횟수가 실패된다면 unhealthy 상태로 된다.

EXIT CODE 설명
0: success 컨테이너가 정상적이고 사용 가능한 상태
1: unhealthy 컨테이너가 올바르게 작동하지 않는 상태
2: starting 예약된 코드

이 상태는 docker container inspect [컨테이너명] 또는 docker container ls에서 확인할수 있다.

 

HEALTHCHECK을 확인해보기 위해 아래 코드를 Dockerfile에 넣은 후 centos7 버전에 nginx를 설치하고 컨테이너를 실행한다.

FROM centos:7

RUN touch /etc/yum.repos.d/nginx.repo && echo -e '[nginx]\nname=nage repo\nbaseurl=http://nginx.org/packages/centos/7/$basearch/\ngpgcheck=0\nenabled=1' > /etc/yum.repos.d/nginx.repo

RUN yum -y install nginx curl

HEALTHCHECK --interval=10s --timeout=3s CMD curl -f http://127.0.0.1/ || exit 1

10초마다(interval) HEALTHCHECK를 하고, 3초 이상이 소요(timeout)되면서 3번의 재시도(retries, 초기값3)가 실패하면 unhealthy 상태로 변경됨.

7. COPY, ADD

두 명령 모두 호스트OS의 파일 또는 디렉토리를 컨테이너 안의 경로로 복사한다.

 

COPY의 경우 호스트OS에서 컨테이너 안으로 복사만 가능하지만, ADD의 경우 원격 파일 다운로드 또는 압축 해제 등과 같은 기능을 갖고 있다. 즉, 호스트OS에서 컨테이너로 단순히 복사만을 처리할 때 COPY를 사용한다.

 

ADD와 COPY 두 명령 모두 사용법은 아래와 같으며 소유자와 소유그룹도 수정이 가능하다.

ADD [--chown=<user>:<group>] <src>...<dest>
ADD [--chown=<user>:<group>] ["<src>",..."<dest>"] (공백이 포함된 경로에 사용)

COPY [--chown=<user>:<group>] <src>...<dest>
COPY [--chown=<user>:<group>] ["<src>",..."<dest>"] (공백이 포함된 경로에 사용)

 

소유자와 소유그룹 필요없이 사용할때는 다음과 같이 사용하면 된다.

## COPY <호스트OS 파일 경로> <Docker 컨테이너 안에서의 경로>
COPY test.sh /root/copy/test.sh

## ADD <호스트OS 파일 경로> <Docker 컨테이너 안에서의 경로>
ADD test.sh /root/add/test.sh

 

ADD 명령의 경우 아래와 같이 url을 입력하여 다운로드도 가능하다.

## ADD <다운 받을 URL> <Docker 컨테이너 안에서의 경로>
ADD http://.../index.php /root/add_url/index.php

8. ENV, ARG

ENV와 ARG는 비슷해 보이지만 다른 명령이다. ENV는 Dockerfile 또는 컨테이너 안에서 환경 변수로 사용이 가능하고, ARG는 Dockerfile에서만 사용이 가능하다.

# ENV 사용법

# ENV [key][value]
ENV myName nirsa
ENV myAddress nirsa.tistory.com

# ENV [key]=[value] ## 한번에 여러개의 값을 설정할때 사용
ENV myName=nirsa \
  myAddress=nirsa.tistory.com
# ARG 사용법

# [key]=[value]
ARG myName=nirsa
ARG myAddress=nirsa.tistory.com

때문에 ARG의 경우 Dockerfile 작성하는데에 필요한 변수를 선언하여 Dockerfile을 좀더 편하게 작성할 수 있고, ENV는 변수 선언은 물론 컨테이너 안에서 사용할 환경 변수(작업 디렉토리 지정) 등 선언하여 사용할 수 있다.

 

ENV가 실제로 컨테이너 안에서 환경변수로 작동하는지 알아보기 위한 테스트용 Dockerfile 예제이다.

FROM centos:7

COPY test.sh /root/workdir/test.sh
ENV DIR=/root/workdir/

RUN echo ${DIR}
CMD ${DIR}/test.sh

빌드할때 DIR 변수를 정확히 /root/workdir로 인식하고 있고, container run을 할때도 $DIR/test.sh 즉, /root/workdir/test.sh를 정확히 실행했다.

 

test.sh의 코드를 그대로 사용하되 ENV를 ARG로 변경한다.

FROM centos:7

COPY test.sh /root/workdir/test.sh
ARG DIR=/root/mkdir

RUN echo ${DIR}
CMD ${DIR}/test.sh

빌드할때엔 ARG로 선언한 변수 DIR가 제대로 /root/workdir를 선언하게 되고, container run할때는 ARG로 선언한 변수가 사라져서 /root/workdir/test.sh를 찾지 못하고, 루트 디렉토리 및에서 test.sh를 찾지 못했다고 나온다.

 

이러한 설정들은 docker container inspect [컨테이너명] 을 입력하고 확인할 수 있다.

9. WORKDIR

WORKDIR은 명령을 실행하기 위한 디렉토리를 지정한다.

 

우선 WORKDIR을 확인하기 위해 Dockerfile이 있는 디렉토리 안에 아래와 같은 코드의 test.sh 생성 및 Dockerfile을 수정해준다. (COPY 명령은 이후 ADD와 같이 업로드할 것이다. 간단히 호스트OS test.sh를 컨테이너 안의 /root/workdir/test.sh 으로 복사하는 명령이다)

 

빌드 후 container run을 하면 WORKDIR로 이동된 경로와 test.sh가 실행되어 Hello,world!가 출력된다. 즉 WORKDIR 명령은 리눅스의 cd와 비슷한 개념이지만, 작업할 디렉토리를 지정한다.

FROM centos:7

COPY test.sh /root/mkdir/test.sh
WORKDIR /root/mkdir

CMD ./test.sh

10. USER

USER 명령은 RUN, CDM, ENTRYPOINT와 같은 명령을 실행하기 위한 특정 사용자를 지정해야 하는 상황에서 사용된다. 아래와 같이 유저명:그룹명 또는 UID:GID와 같이 사용되고, 그룹명과 GID는 생략가능하다.

USER <user>[:<group>]
# 또는
USER <UID>[:<GID>]

단, 사용시 주의점이 있는데 USER 명령을 사용하기 위한 사용자 계정이 존재해야 한다. (USER 명령은 계정을 생성하는 것이 아니라, 특정 사용자 계정을 사용하기 위한 명령이다)

 

아래와 같이 useradd [사용자명]을 미리 입력하여 계정을 생성하고, 리눅스의 id 명령어로 확인한다.

FROM centos:7

RUN useradd nirsa
RUN id

USER nirsa
RUN id

Step3에서는 root 계정으로 실행되었고, Step4에서 USER nirsa로 특정 사용자를 지정해준 후 Step5에서 실행된 RUN 명령은 nirsa 계정으로 실행된것을 알수 있다.

11. LABEL

LABEL 명령은 이미지의 버전 정보, 작성자, 코멘트와 같이 이미지 상세 정보를 작성해두기 위한 명령이다. 아래와 같이 사용이 가능하고 docker image inspect --format="{{.Config.Labels}}" [이미지명] 으로 확인할 수 있다.

LABEL title="webserver"
LABEL version="2.0"

12. EXPOSE

EXPOSE 명령은 해당 컨테이너가 런타임에 지정된 네트워크 포트에서 수신 대기중이라는 것을 알려준다. 일반적으로 dockerfile을 작성하는 사람과 컨테이너를 직접 실행할 사람 사이에서 공개할 포트를 알려주기 위해 문서 유형으로 작성할때 사용된다.

 

이 명령 자체가 작성된 포트를 실행하여 listening 상태로 올려주거나 하지는 않기 때문에, 실제로 포트를 열기 위해선 container run 에서 -p 옵션을 사용해야 한다.

# EXPOSE 포트번호[/protocol]

EXPOSE 80/tcp
EXPOSE 80/udp

위와 같이 선언이 되지만 프로토콜을 지정하지 않으면 기본값은 TCP이다.

13. VOLUME

컨테이너 안에 있는 데이터는 컨테이너를 삭제하면 모든 데이터가 같이 삭제(휘발성 데이터) 되기 때문에 데이터를 보존하기 위해 VOLUME을 사용하고, VOLUME 명령은 설정한 컨테이너의 데이터를 호스트 OS에 저장하거나, 컨테이너들간의 데이터를 공유가 가능하게 한다.

# Dockerfile
VOLUME ["컨테이너 디렉토리1", "컨테이너 디렉토리2"]

Dockerfile에서 위와 같이 생성한 볼륨은 호스트OS의 /var/lib/docker/volumes에 생성되며, Docker에서 자동 생성한 hash값으로 디렉토리가 생긴다.

 

아래 Dockerfile을 생성하고 build 후 컨테이너를 실행하여 실제로 호스트OS와 컨테이너가 해당 디렉토리를 공유중인지 확인한다(컨테이너의 /data 디렉토리에는 아무런 파일도 없어서 touch volume_test로 파일을 하나 생성해준다).

FROM centos:7
VOLUME ["/var/log/", "/data/"]

docker container inspect --format="{{.Mounts}}" [컨테이너명]에서 확인할 수 있다.

 

docker container run을 이용하여 호스트 OS의 경로를 변경할 수도 있다.

# docker run -it -v [호스트OS 경로]:[컨테이너 경로] [이미지명] /bin/bash
docker run -it -v /root/docker/log/:/var/log/ centos /bin/bash