此前的RUN和CMD,我們知道,RUN是構建的時候運行的命令,在鏡像完成后RUN就運行結束。隨后推送到倉庫中,這些RUN命令是不會在進行運行的。
- init
在Docker上下文中,一個Docker只會運行一個應用程序,那么應該運行那個程序,又是什么應用?
一般情況下,能擁有生產能力的應用通常在宿主機上一般表現是運行在后台守護進程程序,如:mysql,nginx等。
這些服務在運行時候,都是以某個進程運行。某個進程都應該是某個進程的子進程,除init之外,而init是由內核啟動的,一般我們在啟動一個進程的時候,是以shell的子進程運行的,在命令行下創建的任何進程都是shell的子進程,而有一些經常也會直接占據shell的終端設備,就算使用&放置后台,啟動的父進程也仍然是shell。進程終止的時候會將所有的子進程銷毀,這種情況下我們會使用nohub command &,這樣一來就類似於將啟動的進程init
那么在Docker中運行的init進程(init的id是1)是由內核啟動,還是托管shell啟 動。如果基於內核啟動ls /etc/*
,|
等shell特性是無法使用的,那么如果基於shell啟動,那init的id就不再是1了
- exec
假如想基於shell的方式來啟動一個主進程,那么shell的id號就是1,而后基於此在啟動主進程,但是這樣一來shell就不能退出,那可能需要一種能夠剝離終端的方式啟動,但是剝離了終端的方式啟動,主進程號又不是1了。不過,我們可以使用exec來解決,shell啟動是沒有問題,進程號id是1也沒有關系,exec頂替shell的id為1,取代shell進程,shell退出后exec就成了id為1的進程。
在很多時候,在容器內啟動一個應用程序的時候可以不基於shell,直接啟動也可以,也可以基於shell,如果基於shell啟動,並且不違背shell主進程id為1的調節關系,那么就可以使用第二種方式,exec。
I. CMD
RUN是構建的鏡象build時候執行的,而cmd是定義一個鏡象文件啟動為容器時候默認要運行的程序,而Docker容器默認運行一個程序,在運行CMD的時候,是可以寫多條CMD的,而最后一條CMD是生效的。而RUN是可以從上倒下接多RUN命令逐一運行。
CMD類屬於RUN命令,CMD指令也可以用於運行任何命令或應用程序,不過,二者的運行時間點不同
- RUN指令運行與映像文件構建過程中,而CMD指令運行於基於Dockerfile構建出的新映像文件啟動一個容器時
- CMD指令的首要目的在於為啟動的容器指定默認要運行的程序,且運行結束后,容器也將終止;不過,CMD指令的命令其可以被Docker run命令選項所覆蓋
- 在Dockerfile中可以存在多個CMD指令,但僅最后一個會生效
命令
CMD <command>
CMD ["<executable>","<paraml>","<param2>"]
CMD ["<param1>","<param2>"]
前兩種語法格式的意義同RUN
第一種的CMD的命令執行是直接寫命令的,並且PID不為1,也無法接收信號(接收信號的必然是pid為1的超級管理進程),docker stop也無法停止。
第二種直接啟動為ID為1的進程,可接受處理shell信號的。
第三種則用於ENTRYPOINT指令提供默認參數
- 編寫Dockerfile
如,創建目錄后追加文件,最后用CMD直接調用httpd啟動
FROM busybox
LABEL maintainer="linuxea.com" app="CMD"
ENV WEB_ROOT="/data/wwwroot"
RUN mkdir -p ${WEB_ROOT}
&& echo '<h1> helo linuxea .</h1>' >> ${WEB_ROOT}/index.html
CMD /bin/httpd -f -h ${WEB_ROOT}
開始build
[root@linuxEA /data/linuxea2]$ docker build -t marksugar/httpd:9
Sending build context to Docker daemon 2.048kB
Step 1/5 : FROM busybox
---> 59788edf1f3e
Step 2/5 : LABEL maintainer="linuxea.com" app="CMD"
---> Running in b6e91f2461dd
Removing intermediate container b6e91f2461dd
---> 53559ed7015a
Step 3/5 : ENV WEB_ROOT="/data/wwwroot"
---> Running in 3e615febfd44
Removing intermediate container 3e615febfd44
---> a7917cb7ecbb
Step 4/5 : RUN mkdir -p ${WEB_ROOT} && echo '<h1> helo linuxea .</h1>' >> ${WEB_ROOT}/index.html
---> Running in 15153c929109
Removing intermediate container 15153c929109
---> 8e5548f3c00a
Step 5/5 : CMD /bin/httpd -f -h ${WEB_ROOT}
---> Running in feeb34a9c423
Removing intermediate container feeb34a9c423
---> a091b6d8a31d
Successfully built a091b6d8a31d
Successfully tagged marksugar/httpd:9
從這里可以看到,這條啟動命令是/bin/sh啟動的子進程,在此后啟動的時候會替換成id1,也就是默認執行exec將/bin/sh替換掉
[root@linuxEA /data/linuxea2]$ docker inspect marksugar/httpd:9
...
"Cmd": [
"/bin/sh",
"-c",
"/bin/httpd -f -h ${WEB_ROOT}"
...
而后run起來,但是這里是沒有交互式接口的,盡管使用了-it
[root@linuxEA /data/linuxea2]$ docker run --name linuxea --rm -it marksugar/httpd:9
不過,可以使用exec進入容器,/bin/httpd -f -h /data/wwwroot
的id為1
- 我們在Dockerfile中直接使用命令的方式避免他不是1,那么這里就直接啟動為1,默認執行力exec替換。這也就說明了,盡管使用-it仍然進入不了容器的原因,init1的進程不是shell。進入就要在使用exec繞過進入
[root@linuxEA ~]$ docker exec -it linuxea sh
/ # ps aux
PID USER TIME COMMAND
1 root 0:00 /bin/httpd -f -h /data/wwwroot
7 root 0:00 sh
13 root 0:00 ps aux
/ #
第二種格式
CMD ["/bin/httpd","-f","-h ${WEB_ROOT}"]
以這種方式進行build
FROM busybox
LABEL maintainer="linuxea.com" app="CMD"
ENV WEB_ROOT="/data/wwwroot"
RUN mkdir -p ${WEB_ROOT}
&& echo '<h1> helo linuxea .</h1>' >> ${WEB_ROOT}/index.html
#CMD /bin/httpd -f -h ${WEB_ROOT}
CMD ["/bin/httpd","-f","-h ${WEB_ROOT}"]
啟動就會報錯No such file
[root@linuxEA /data/linuxea2]$ docker run --name linuxea --rm -it marksugar/httpd:10
httpd: can't change directory to ' ${WEB_ROOT}': No such file or directory
報錯No such file是因為CMD ["/bin/httpd","-f","-h ${WEB_ROOT}"]
並不會運行成shell的子進程,而此變量是shell的變量,內核卻不知道這個路徑,所以會報錯。
不過,我們可以指定為shell,如: CMD ["/bin/sh","-c","/bin/httpd","-f","-h ${WEB_ROOT}"]
- 引言
此前我們使用一條命令運行容器的時候,CMD的指令是可以被覆蓋的,如下
[root@linuxEA ~]$ docker run --name linuxea --rm -it marksugar/httpd:9 ls /etc
group hosts mtab passwd shadow
hostname localtime network resolv.conf
上面這條命令是說,運行這個容器,ls /etc
覆蓋了此前鏡像中的CMD中的啟動httpd的命令。
但是有時候我們不希望被覆蓋,就使用ENTRYPOINT
II. ENTRYPOINT
類似於CMD指令的功能,用於為容器指定默認的運行程序,從而使得容器像是一個單獨的可執行文件
與CMD不同的是由ENTRYPOINT啟動的程序不會被docker run命令行指定的參數所覆蓋,而且,這些命令行參數會被當作參數傳遞給ENTRYPOINT指令的指定程序
不過,docker run命令--entrypoint選項參數可覆蓋ENTRYPOINT指令指定的程序
ENTRYPOINT <command>
ENTRYPOINT ["<executable>","<param1>","<param2>"]
docker run命令傳入的命令參數會覆蓋CMD指令的內容並且附加到ENTRYPOINT命令最后作為其參數使用
Dockerfile文件中也可以存在多個ENTRYPOINT指令,但僅有最后一個生效
我們先編寫一個Dockerfile,使用NETRYPOINT啟動
FROM busybox LABEL maintainer="linuxea.com" app="CMD" ENV WEB_ROOT="/data/wwwroot"
RUN mkdir -p ${WEB_ROOT}
&& echo '<h1> helo linuxea .</h1>' >> ${WEB_ROOT}/index.html
ENTRYPOINT /bin/httpd -f -h ${WEB_ROOT}
而后build
[root@linuxEA /data/linuxea2]$ docker build -t marksugar/httpd:11 .
Sending build context to Docker daemon 2.048kB
Step 1/5 : FROM busybox
---> 59788edf1f3e
Step 2/5 : LABEL maintainer="linuxea.com" app="CMD"
---> Using cache
---> 53559ed7015a
Step 3/5 : ENV WEB_ROOT="/data/wwwroot"
---> Using cache
---> a7917cb7ecbb
Step 4/5 : RUN mkdir -p ${WEB_ROOT} && echo '<h1> helo linuxea .</h1>' >> ${WEB_ROOT}/index.html
---> Using cache
---> 8e5548f3c00a
Step 5/5 : ENTRYPOINT /bin/httpd -f -h ${WEB_ROOT}
---> Running in 34c028efac0d
Removing intermediate container 34c028efac0d
---> b7be6f74fc65
Successfully built b7be6f74fc65
Successfully tagged marksugar/httpd:11
啟動是沒有問題的
[root@linuxEA /data/linuxea2]$ docker run --name linuxea --rm -it marksugar/httpd:11
我們獲取到這個ip。訪問試試
[root@linuxEA ~]$ docker inspect -f {{.NetworkSettings.IPAddress}} linuxea
192.168.100.2
[root@linuxEA ~]$ curl 192.168.100.2
<h1> helo linuxea .</h1>
- ENTRYPOINT
而后使用CMD的方式同樣來覆蓋
[root@linuxEA /data/linuxea2]$ docker run --name linuxea --rm -it marksugar/httpd:11 ls /etc
容器依然運行起來,但我們並沒有看到ls /etc
的內容。這是因為在run的時候使用了ls /etc
並不會替換Dockerfile中ENTRYPOINT的運行命令,只是在ENTRYPOINT命令之后加了ls /etc
,而httpd識別不出ls /etc
而已
如果一定要進行覆蓋,就需要使用--entrypoint
,如下:
docker run --name linuxea --rm -it --entrypoint "/bin/ls" marksugar/httpd:11 -al /etc
[root@linuxEA ~]$ docker run --name linuxea --rm -it --entrypoint "/bin/ls" marksugar/httpd:11 -al /etc
total 28
drwxr-xr-x 1 root root 66 Dec 8 09:07 .
drwxr-xr-x 1 root root 6 Dec 8 09:07 ..
-rw-rw-r-- 1 root root 307 Sep 6 20:11 group
-rw-r--r-- 1 root root 13 Dec 8 09:07 hostname
-rw-r--r-- 1 root root 177 Dec 8 09:07 hosts
-rw-r--r-- 1 root root 127 May 4 2018 localtime
lrwxrwxrwx 1 root root 12 Dec 8 09:07 mtab -> /proc/mounts
drwxr-xr-x 6 root root 79 Oct 1 22:37 network
-rw-r--r-- 1 root root 340 Sep 6 20:11 passwd
-rw-r--r-- 1 root root 114 Dec 8 09:07 resolv.conf
-rw------- 1 root root 243 Sep 6 20:11 shadow
III. 示例
ENTRYPOINT
此時我們知道ENTRYPOINT是作為入口點的指令,通過exec 指定,指定的命令和參數作為一個JSON數組,那就意味着需要使用雙引號而不是單引號
ENTRYPOINT ["executable", "param1", "param2"]
使用此語法,Docker將不使用命令shell,這意味着不會發生正常的shell處理。如果需要shell處理功能,則可以使用shell命令啟動JSON數組。
ENTRYPOINT [ "sh", "-c", "echo $HOME" ]
另一種選擇是使用腳本來運行容器的入口點命令。按照慣例,它通常在名稱中包含入口點。在此腳本中,您可以設置應用程序以及加載任何配置和環境變量。下面是一個如何使用ENTRYPOINT
exec語法在Dockerfile中運行它的示例。
COPY ./docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["postgres"]
例如,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 "$@"
- docker-compose 的寫法:
Docker Compose文件中使用的命令是相同的,除了使用小寫字母。
entrypoint: /code/entrypoint.sh
可以在docker-compose.yml中使用列表定義入口點。
entrypoint:
- php
- -d
- zend_extension=/usr/local/lib/php/xdebug.so
- -d
- memory_limit=-1
- vendor/bin/phpunit
不過仍然可可以使用docker run --entrypoint
或docker-compose run --entrypoint
標記覆蓋入口的指令
CMD/command
CMD
(Dockerfiles)/ command
(Docker Compose文件)的主要目的是在執行容器時提供默認值。這些將在入口點之后被附加到入口的參數。
例如,如果運行docker run <image>
,則將執行Dockerfiles中CMD
/所指定的命令和參數command
。
在Dockerfiles中,可以定義CMD
包含可執行文件的默認值。例如:
CMD ["executable","param1","param2"]
如果省略了可執行文件,則還必須指定一條ENTRYPOINT
指令。
CMD ["param1","param2"]
(作為ENTRYPOINT的默認參數)
注意:其中只能有一條CMD
指令Dockerfile
。如果列出多個CMD
,則只有最后一個CMD
生效。
Docker Compose命令
使用Docker Compose時,可以在docker-compose.yml中定義相同的指令,但它以小寫形式寫成完整的單詞command
。
command: ["bundle", "exec", "thin", "-p", "3000"]
覆蓋CMD
可以覆蓋CMD
運行容器時指定的命令。
docker run rails_app rails console
如果指定了參數docker run
,那么它們將覆蓋指定的默認值CMD
。
語法最佳實踐
還有EXEC語法,shell語法兩個另一個有效的選項ENTRYPOINT
和CMD
。這將以字符串形式執行此命令並執行變量替換。
ENTRYPOINT command param1 param2
CMD command param1 param2
CMD
應該幾乎總是以形式使用
CMD [“executable”, “param1”, “param2”…]。因此,如果鏡象是用於服務的,例如Apache和Rails,那么你可以運行類似的東西
CMD ["apache2","-DFOREGROUND"]`。實際上,建議將這種形式的指令用於任何基於服務的鏡象。
所述
*ENTRYPOINT*
shell形式防止任何*CMD*
或*run*
被使用命令行參數覆蓋,但是有缺點,*ENTRYPOINT*
將被開始作為一個子命令*/bin/sh -c*
,其不通過信號。這意味着可執行文件將不是容器*PID 1*
- 並且不會收到Unix信號 - 因此您的可執行文件將不會收到*SIGTERM*
來自*docker stop <container>*
如果
*CMD*
用於為*ENTRYPOINT*
指令提供默認參數,則應使用JSON數組格式指定*CMD*
和*ENTRYPOINT*
指令。
Both
CMD
和ENTRYPOINT
instructions指定運行容器時執行的命令。很少有規則描述它們如何相互作用。
- Dockerfiles應至少指定一個
CMD
或ENTRYPOINT
命令。 ENTRYPOINT
應該在將容器用作可執行文件時定義。CMD
應該用作定義ENTRYPOINT
命令的默認參數或在容器中執行ad-hoc命令的方法。CMD
在使用替代參數運行容器時將被覆蓋。
延伸閱讀 :
https://docs.docker.com/engine/reference/builder/
https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
https://docs.docker.com/engine/reference/builder/#usage
https://docs.docker.com/compose/compose-file/
https://docs.docker.com/develop/develop-images/dockerfile_best-practices/