目录
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! "); } |
编译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 "); else printf("=> ECHO1: %s ", 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 |