본문 바로가기
Kubernetes & Docker

Docker(file) CMD & ENTRYPOINT & RUN 그리고 docker 컨테이너 생명주기와 프로세스 (container lifecycle & process)

by ahsung 2022. 7. 1.

CMD & ENTRYPOINT & RUN 는 모두 Dockerfile을 작성할 때 사용되는 문법이며 docker images에 저장되어 있는 값들입니다.

 

RUN

run은 docker 이미지가 빌드될 때 실행되는 명령어로 주로 패키지 설치, 빌드 명령어 등에 사용됩니다.

run은 다른 포스팅 글에서도 이해하기 쉬운 설명이 많으므로 간략하게 패스하겠습니다.

 

 

CMD

공식 docker 문서에 의하면 cmd는 3가지 형태로 사용될 수 있습니다.

 

The CMD instruction has three forms:

  • CMD ["executable","param1","param2"] (exec form, this is the preferred form)
  • CMD ["param1","param2"] (as default parameters to ENTRYPOINT)
  • CMD command param1 param2 (shell form)

일단 3번 shell form은 ENTRYPOINT에서도 설명하겠지만 선호하는 스타일은 아닙니다.

docker의 생명주기 관리에 있어서 좋지 않은 형태이므로 shell form은 되도록 사용하지 않습니다.

2번 또한 ENTRYPOINT가 사용될 때 의미가 있으므로 ENTRYPOINT에서 1,2번은 통합하여 설명하도록 하겠습니다.

 

1번 형태가 docker를 사용할 때 가장 보편적이고 선호하는 형태의 사용법입니다.

2번은 ENTRYPOINT와 함께 사용될 때라고 했는데, ENTRYPOINT를 사용하지 않고 1번 형태의 CMD만 사용하는 것이 보편적입니다.

 

1번 CMD은 리스트 형태로  첫 번째로 실팽파일, 뒤로 프로세스 인자가 따라옵니다.

docker images는 빌드후에 컨테이너가 만들어질 때 이 CMD를 실행시킵니다.

 

CMD는 아래 문법으로 컨테이너를 생성할 때 변경할 수 있습니다.

CMD는 여러개가 중복 사용되지 않으므로, 마지막에 입력된 CMD만 적용됩니다.

docker run <이미지> <CMD>

# 예시

$ docker run -it --rm --name test  ubnutu echo hello
# 옵션 상세는 docker run --help 참고
# -it는 tty, stdin 붙음 즉 콘솔 가능
# --rm은 컨테이너 종료시 삭제, 진짜 운영중인 컨테이너에서는 쓰면 안댐(프로세스 꺼지면 다시 살려야지..)
# --name 컨테이너 이름
# 이미지: ubuntu
# CMD: ["echo", "hello"]

CMD executable로 실행된 프로세스가 docker 컨테이너의 1번 프로세스가 되며,

도커 컨테이너의 생명주기는 이 1번 프로세스로 결정됩니다.

 

1번 프로세스가 죽으면(종료) 컨테이너는 그대로 죽게(종료)됩니다.

물론 컨테이너 자체는 삭제되지 않았으므로 다시 docker start로 실행시킬 수 있습니다.

그리고 이때 실행되는건 당연하게 컨테이너에 입력되있는 CMD입니다.

 

또한 docker stop으로 컨테이너를 종료할 때,

docker는 컨테이너 내부에 SIG TERM 시그널을 보내게됩니다.

어디로? 컨테이너의 1번 프로세스입니다.

 

1번 프로세스가 SIGTERM 시그널을 이해할 수 있게 짜여져있다면 graceful하게 종료될 것이고

그렇지 않다면, docker는 일정시간 기다린후 SIGKILL를 보내서 강제로 프로세스를 죽이고 컨테이너를 종료합니다.

 

자세한건 아래 링크에서 확인 가능

참조

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

 

Dockerfile reference

 

docs.docker.com

 

 

 

ENTRYPOINT

docker 공식 문서에 의하면 ENTRYPOINT는 두 가지 형태가 있습니다.

 

The exec form, which is the preferred form:

ENTRYPOINT ["executable", "param1", "param2"]

The shell form:

ENTRYPOINT command param1 param2

 

CMD와 마찬가지로 컨테이너 생성시 실행되는 프로세스 옵션입니다.

ENTRYPOINT와 CMD의 차이점이라면,

ENTRYPOINT가 있을 경우 해당 컨테이너의 CMD는 2번 형태로 실행됩니다.

 

즉 ENTRYPOINT는 반드시 컨테이너 생성시 실행되며,

CMD는 2번 형태로 ENTRYPOINT 뒷부분에 이어서 오는 프로세스 argumnets입니다.

 

ENTRYPOINT는 이미지가 컨테이너로 생성될 때, 실행되는 프로세스를 강제하는 옵션이라고 생각 할 수 있습니다.

(docker run의 --entrypoint 옵션으로 이미지 실행시 변경이 가능하긴합니다.)

또한 위에서 생명주기를 설명했 듯이 이렇게 강제로 실행된 프로세스가 PID 1번이며 컨테이너 생명주기를 책임집니다.

즉 메인이되는 프로세스를 컨테이너가 실행될 때 지정하기 보다는, 이미지 자체에서 강제하는 느낌이 강합니다.

 

ENTRYPOINT(1번)와 CMD(2번) 사용 예시

# Dockerfile
$ cat Dockerfile
FROM ubuntu
ENTRYPOINT ["top", "-b"]
CMD ["-c"]


$ docker build -t test_top_image .
$ docker run -it --rm --name test_top_name test_top_image -H
top - 17:54:53 up 591 days,  9:30,  0 users,  load average: 0.01, 0.06, 0.11
Threads:   1 total,   1 running,   0 sleeping,   0 stopped,   0 zombie
%Cpu(s):100.0 us,  0.0 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
MiB Mem :   1837.8 total,    150.8 free,    549.6 used,   1137.3 buff/cache
MiB Swap:      0.0 total,      0.0 free,      0.0 used.   1038.1 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
    1 root      20   0    7296   1712   1260 R   0.0   0.1   0:00.03 top

$ docker exec -it test_top_name ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.2  0.0   7264  1856 pts/0    Ss+  18:08   0:00 top -b -H
root         6  0.0  0.0   7044  1440 pts/1    Rs+  18:09   0:00 ps aux

$ docker container inspect test_top_name | grep -i -A 4 entrypoint
            "Entrypoint": [
                "top",
                "-b"
            ],
            "OnBuild": null,
           
$ docker container inspect test | grep -i -A 4 CMD
            "Cmd": [
                "-H"
            ],
            "Image": "test",
            "Volumes": null,

 

 

inspect 명령으로 컨테이너의 상태를 확인할 수 있으며 Entrypoint가 반드시 pid 1번으로 실행되고 CMD가 뒤에 옵션으로 추가된 것을 알 수 있습니다.

Entrypoint를 사용하지 않을 경우, null로 표시되고 CMD는 1번 형태로 사용되며 이런 사용법이 보편적입니다.

 

물론 Entrypoint를 사용하지 않는 것은 아닙니다.

컨테이너 이미지 특성상 runtime 환경 즉 플랫폼을 제공하는 성격이 강하기 때문에 이미지에 프로세스를 지정하지 않는 경우가 많을 뿐이지 프로세스까지 포함하여 하나의 플랫폼이라고 생각한다면 충분히 좋은 형태로 사용할 수 있습니다.

 

예로 jenkins와 같은 경우, Entrypoint로 jenkins를 실행시키고 cmd는 비어있습니다. (jenkins를 실행하는 것 까지가 플랫폼!)

또는

pid 1번으로 반드시 관리용도의 프로세스를 실행하게 하고 (흔히 사용되는 tini),

해당 프로세스가 CMD인자로 들어온 메인 애플리케이션을 fork로 실행시키는 형태도 다수 존재합니다.

 

예로 쿠버네티스에서 많이 사용되는 ingress-nginx의 도커 이미지는 Entrypoint로 dump용 프로세스가 있고

dump프로세스가 fork하여 CMD로 들어온 nginx 프로세스를 실행합니다.

$ docker container inspect ingress-nginx | grep -i -A 4 entry
            "Entrypoint": [
                "/usr/bin/dumb-init",
                "--"
            ],
            "OnBuild": null,
$ docker container inspect ingress-nginx | grep -i -A 4 cmd
            "Cmd": [
                "/nginx-ingress-controller",
                "--election-id=ingress-controller-leader",
                "--controller-class=k8s.io/ingress-nginx",
                "--configmap=ingress-nginx/ingress-nginx-controller"
                

# ingress-nginx 컨테이너에 ps가 제한되어있어서 호스트에서 실행
# 부모, 자식 프로세스라는 관계는 확인 가능
$ ps auxf | grep nginx-ingress | grep -v grep
101      27954  0.0  0.0    196     4 ?        Ss    2021   0:00  |   \_ /usr/bin/dumb-init -- /nginx-ingress-controller --election-id=ingress-controller-leader --controller-class=k8s.io/ingress-nginx --configmap=ingress-nginx/ingress-nginx-controller
101      27970  0.1  1.4 743860 26640 ?        Ssl   2021 434:51  |       \_ /nginx-ingress-controller --election-id=ingress-controller-leader --controller-class=k8s.io/ingress-nginx --configmap=ingress-nginx/ingress-nginx-controller

# 컨테이너의 PID 1번은 dumb-init이 맞음
$ docker exec -it ingress-nginx bash
bash-5.1$ cat /proc/1/comm
dumb-init

 

 

마지막으로 ENTRYPOINT의 shell form을 되도록 사용하지 않는 이유는 

 

docker docs 인용

The shell form prevents any CMD or run command line arguments from being used, but has the disadvantage that your ENTRYPOINT will be started as a subcommand of /bin/sh -c, which does not pass signals. This means that the executable will not be the container’s PID 1 - and will not receive Unix signals - so your executable will not receive a SIGTERM from docker stop <container>.

 

pid 1번이 제대로 설정되지 않기 때문에, (/bin/sh로 설정됨)

SIGTERM등을 받을 수 없고 docker의 생명주기가 내가 생각한 것과는 다르게 운영될 수 있습니다.

(물론 signal은 자식 프로세스에게 일반적으로 전달되기 때문에 의외로 작동은 잘 될 수도?)

쉘폼의 장점이라면 shell 방식의 문법 지원등이 있습니다.

e.g

HOME="HH"


CMD ["echo", "$HOME" ]
-> $HOME 출력

CMD echo $HOME
-> HH 출력

 

참조

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

 

Dockerfile reference

 

docs.docker.com

 

'Kubernetes & Docker' 카테고리의 다른 글

쿠버네티스(kubernetes) kubelet.go node not found #NotReady  (0) 2021.01.24
쿠버네티스란?  (0) 2020.09.09

댓글