目錄
5.1. stat /bin/sh: no such file or directory 7
5.2. COPY failed: ... stat no such file or directory 7
5.3. exec user process caused "no such file or directory" 8
1. 前言
本文介紹在CentOS7上從構建一個最簡單無依賴的鏡像開始,逐步揭示Docker鏡像的構建和Dockerfile的應用。
什么是鏡像?可理解鏡像(image)為一個可執行程序文件,而容器(container)則是進程(運行態),Kubernetes(即k8s)中的概念POD則相當於進程組。
謹記:容器運行在Linux內核之上,不包含位於內核之上的glibc等庫,以及ls等命令。如果容器中的程序依賴glibc等庫或者依賴ls等命令,則容器自身應當包含這些設施。另外,容器中的程序等必須和內核兼容,否則將會遇到“FATAL: kernel too old”錯誤,該錯誤和庫文件ld-linux.so有關。
2. 基本概念
2.1. 倉庫
Docker倉庫(Repository)是存儲Docker鏡像的地方。
2.2. 鏡像ID和容器ID
鏡像(image)是靜態的,容器(container)是運行中的鏡像。如果說鏡像是程序文件,則容器是進程。把鏡像ID看作文件名,則容器ID可視為進程ID,因此每次啟動的容器ID是不相同的。
同一鏡像可以啟動多個容器,容器間的ID不會相同:
# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 7518f632b6d0 centos "/bin/bash" 4 seconds ago Up 2 seconds focused_turing d97bd379589c centos "/bin/bash" 6 minutes ago Up 6 minutes friendly_nightingale |
3. 最簡鏡像
從最簡鏡像開始,有助於快速了解Dockerfile和Docker鏡像的構建。
3.1. 目錄結構
# tree /root/docker/hello /root/docker/hello |-- Dockerfile |-- hello `-- hello.go
0 directories, 3 files |
3.2. hello.go
GO編譯出來的可執行程序不依賴libc、libdl、linux-vdso和libonion等庫,可以構建最簡單的Dockerfile和最小的鏡像。hello.go源代碼如下:
# cat hello.go package main import "fmt" func main() { fmt.Println("Hello, world!\n"); } |
編譯hello.go,生成可執行程序hello:
# go build -o hello hello.go # ls hello hello.go |
3.3. Dockerfile
編寫一個最簡單(不基於任何已有鏡像)的Dockerfile,僅將本地的hello程序打包到鏡像中,並在啟動容器時運行hello。內容如下:
# cat Dockerfile FROM scratch COPY hello / CMD ["/hello"] |
Dockerfile格式解釋:
關鍵詞 |
說明 |
# |
表示注釋 |
FROM |
用於指定基礎鏡像,scratch表示不基於任何基礎鏡像。 |
COPY |
表示復制本地文件到容器的指定目錄,注意本地文件目錄是相對Dockerfile文件所在的目錄,而不是系統的根目錄。如果是遠端的文件,則需使用ADD命令。 |
CMD |
用於指定啟動容器時默認執行的命令,一個Dockerfile只有最后一條CMD有效,其它的CMD會被忽略,CMD有三種書寫格式。 |
3.4. CMD和ENTRYPOINT
如果在Dockerfile中沒有指定ENTRYPOINT,執行命令“docker run”也沒有指定“--entrypoint”,則執行CMD指定的命令。另外,可通過命令行參數“--entrypoint”覆蓋ENTRYPOINT。
Dockerfile中的CMD有三種書寫格式:
|
書寫格式 |
說明 |
格式1 |
CMD ["executable","param1","param2"] |
EXEC執行方式 |
格式2 |
CMD ["","param2"] |
指定了ENTRYPOINT時,作為ENTRYPOINT的參數,請注意ENTRYPOINT也分EXEC和Shell兩種書寫格式。 |
格式3 |
CMD command param1 param2 |
Shell執行方式,這要求鏡像中有可執行程序“/bin/sh”,執行時實際是: /bin/sh -c "command param1 param2", 如果鏡像中無“/bin/sh”,則在啟動容器時報錯“stat /bin/sh: no such file or directory”。 |
1) 什么是EXEC執行方式?
# /bin/whoami root |
2) 什么是Shell執行方式?
# sh -c "/bin/whoami" root |
如果CMD和ENTRYPOINT組合使用,則兩者均需JSON數組格式。
3.5. RUN和CMD
Dockerfile中的每一條RUN命令均會產生一個新的鏡像,因此應當盡可能減少RUN命令數,如使用“&&”將多條寫成一條。
RUN mkdir /data/test && chown test /data/test |
RUN和CMD完全不同,RUN是生成鏡像時執行,而CMD是啟動容器時執行。RUN和鏡像相關,CMD和容器相關。
3.6. 生成鏡像
執行命令“docker build”生成鏡像(也叫構建鏡像,一個鏡像由鏡像ID唯一標識),執行命令“docker images”查看鏡像列表,生成鏡像有點類似於編譯。
# docker images REPOSITORY TAG IMAGE ID CREATED SIZE
# 參數“--tag”用於指定鏡像名(或叫鏡像標簽), # 如果不指定“--tag”,則鏡像名為匿名(<none>)。 # 如果文件Dockerfile沒有發生變化, # 則重復執行build不會生成新的鏡像。 # docker build --tag hello . # 或docker build --tag hello -f Dockerfile . Sending build context to Docker daemon 2.013MB Step 1/3 : FROM scratch ---> Step 2/3 : COPY hello / ---> be473a78a240 Step 3/3 : CMD /hello ---> Running in e6584dd16fe2 Removing intermediate container e6584dd16fe2 ---> 92672788bc94 Successfully built 92672788bc94 <-- 這是鏡像ID Successfully tagged hello:latest
# docker images # “IMAGE ID”為鏡像ID,這里值為92672788bc94 REPOSITORY TAG IMAGE ID CREATED SIZE hello latest 92672788bc94 2 seconds ago 2.01MB |
3.7. 啟動容器
最簡單的啟動容器方法:
# docker run hello Hello, world!
|
也可如下方式啟動容器:
docker run -it hello 或 docker run -i -t hello 也可帶上“--rm”參數(容器停止后自動刪除): docker run -it --rm hello |
這里的參數“-i”和參數“-t”,分別表示:
參數 |
作用 |
-i |
i是interactive的縮寫,作用是讓容器的標准輸入保持打開,以進入命令交互界面模式 |
-t |
t是tty的縮寫,作用是讓docker分配一個偽終端並綁定到容器的標准輸入上 |
-d |
d是deamon的縮寫,作用是讓容器以后台守護方式運行 |
-p |
p是port的縮寫,作用是指定端口映射 |
-P |
P是port的縮寫,作用是隨機分配端口 |
--name |
為容器指定一個新的名字 |
--rm |
容器退出時自動刪除,如果不指定,則需要通過命令“docker rm”來刪除 |
4. 鏡像進階
這一節的鏡像不從零開始,而是基於已有鏡像生成新的鏡像。
從scratch創建一個實用的鏡像不易,也是不必要的,除了學習目的。容器雖然運行在本地的Linux內核之上,但依賴的庫(運行時環境)卻需要容器本身包含,比如核心的libc和libdl等庫。這也是在創建最簡鏡像時采用GO程序的原因,避免了這些依賴,然而實際中很難避免這些依賴,因此最好的辦法是基於其它鏡像構建自己的鏡像。
alpine是Docker官方提交的只有5MB多大小的Linux鏡像,包管理工具為apk,可以用來做學習研究用。alpine不帶glibc庫,它帶的是musl libc(一個輕量級的C標准庫)。如果有glibc需求,可用基於alpine的alpine-glibc鏡像,這個也有Docker官方提供的。
另外,還有一個第三方的tinycore鏡像,只有7MB多大小,包含了libc等更為豐富基礎設施。如果可以訪問docker.io,則可直接執行命令“docker pull tinycore”將tinycore鏡像拉取到本地,否則通過Docker的鏡像導出(先在一台可以訪問docker.io機器上pull鏡像,然后導出成tar文件)和導入功能間接拉取到。
不同的基礎鏡像除了所帶的庫等不同外,鏡像大小也是考慮的重要因素之一,原則上越小越好,本節內容官方的Centos鏡像。
4.1. 下載基礎鏡像
這里選擇官方的centos作為基礎鏡像,執行拉取鏡像命令:
# docker pull docker.io/centos |
如想找其它的centos鏡像,可執行命令“docker search centos”搜索。如果本地不能訪問docker.io,則可在一台可訪問docker.io機器先拉取下來,然后使用Docker的導出(save)導入(load)載入進來。
檢查centos鏡像是否可用:
# docker images | grep centos centos latest 0f3e07c0138f 2 months ago 220MB |
檢查鏡像centos版本:
# docker run -it --rm centos cat /etc/centos-release CentOS Linux release 8.0.1905 (Core) |
4.2. 准備本地程序源碼
以C程序為例,源代碼如下:
# cat echo1.c #include <stdio.h> int main(int argc, char* argv[]) { if (argc == 1) printf("=> ECHO1: docker\n"); else printf("=> ECHO1: %s\n", argv[1]); } |
編譯生成可執行程序:
# gcc -g -o echo1 echo1.c |
4.3. 編寫Dockerfile
# cat Dockerfile.echo1 FROM centos COPY echo1 / CMD ["/echo1"] |
4.4. 生成鏡像
# docker build --tag echo1 -f Dockerfile.echo1 . |
4.5. 啟動容器
默認不帶參數方式運行(因為Dockerfile.echo1中沒有ENTRYPOINT,所以執行的是CMD部分命令):
# docker run -it --rm echo1 => Hello: docker |
帶參數方式執行(實為“--entrypoint”方式):
# docker run -it --rm echo1 /echo1 centos => ECHO1: centos |
上述等同於:
# docker run -it --rm --entrypoint='/echo1' echo1 => ECHO1: docker |
“--entrypoint”帶參數方式如下(參數在最后,並不是“--entrypoint”值的一部分):
# docker run -it --rm --entrypoint='/echo1' echo1 world => ECHO1: world |
5. 常見問題
5.1. stat /bin/sh: no such file or directory
啟動窗口時報如下錯誤,可能是Dockerfile中的CMD格式錯誤:
docker: Error response from daemon: OCI runtime create failed: container_linux.go:345: starting container process caused "exec: \"/bin/sh\": stat /bin/sh: no such file or directory": unknown. ERRO[0000] error waiting for container: context canceled |
原因是CMD書寫為Shell格式,但鏡像中沒有/bin/sh這個文件。
5.2. COPY failed: ... stat no such file or directory
在創建鏡像時報如下錯誤,是因為COPY命令的源文件或目錄不是相對Dockerfile所在目錄的路徑,比如使用了本地路徑。
COPY failed: stat /data/docker/tmp/docker-builder891858880/bin/sh: no such file or directory |
比如下列COPY即會報這個錯誤:
COPY /bin/sh /bin/ |
解決辦法是先將/bin/sh復制到Dockerfile文件所在目錄,然后再創建鏡像。
5.3. exec user process caused "no such file or directory"
運行容器時報如下錯誤:
standard_init_linux.go:211: exec user process caused "no such file or directory" |
這個錯誤有多種原因,比如:
1) Dockerfile非UNIX格式(換符符);
2) 容器中的可執行程序依賴的庫不存在,比如沒有libc庫;
3) CMD格式錯誤。
附:安裝GO
安裝GO步驟:
1) 下載安裝包
從GO的官網(https://golang.org/dl/)上下載,選擇Linux安裝包(本文下載的為go1.13.5.linux-amd64.tar.gz)。
2) 上傳安裝包
將安裝包(比如go1.13.5.linux-amd64.tar.gz)上傳到/usr/local目錄。如果Linux能夠訪問網絡,也可直接在/usr/local上下載,比如:
# cd /usr/local # wget https://dl.google.com/go/go1.13.5.linux-amd64.tar.gz |
3) 安裝和設置
在/usr/local目錄下解壓即完成安裝,實際上也可能解壓到其它目錄。
# cd /usr/local # tar xzf go1.13.5.linux-amd64.tar.gz |
設置環境變量,以方便執行(go.sh可無可執行權限):
# cat /etc/profile.d/go.sh export PATH=/usr/local/go/bin:$PATH |
如果不想重新登錄而直接生效,可手工直接執行一次go.sh:
# source /etc/profile.d/go.sh |