目录

目录 1

1. 前言 1

2. 基本概念 2

2.1. 仓库 2

2.2. 镜像ID和容器ID 2

3. 最简镜像 2

3.1. 目录结构 2

3.2. hello.go 2

3.3. Dockerfile 3

3.4. CMD和ENTRYPOINT 3

3.5. RUN和CMD 4

3.6. 生成镜像 4

3.7. 启动容器 5

4. 镜像进阶 5

4.1. 下载基础镜像 6

4.2. 准备本地程序源码 6

4.3. 编写Dockerfile 6

4.4. 生成镜像 7

4.5. 启动容器 7

5. 常见问题 7

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

附:安装GO 8

 

  1. 前言

本文介绍在CentOS7上从构建一个最简单无依赖的镜像开始,逐步揭示Docker镜像的构建和Dockerfile的应用。

什么是镜像?可理解镜像(image)为一个可执行程序文件,而容器(container)则是进程(运行态),Kubernetes(即k8s)中的概念POD则相当于进程组。

谨记:容器运行在Linux内核之上,不包含位于内核之上的glibc等库,以及ls等命令。如果容器中的程序依赖glibc等库或者依赖ls等命令,则容器自身应当包含这些设施。另外,容器中的程序等必须和内核兼容,否则将会遇到“FATAL: kernel too old”错误,该错误和库文件ld-linux.so有关。

  1. 基本概念
  1. 仓库

Docker仓库(Repository)是存储Docker镜像的地方。

  1. 镜像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
  1. 最简镜像

从最简镜像开始,有助于快速了解Dockerfile和Docker镜像的构建。

  1. 目录结构
# tree /root/docker/hello
/root/docker/hello
|-- Dockerfile
|-- hello
`-- hello.go
 
0 directories, 3 files
  1. 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
  1. Dockerfile

编写一个最简单(不基于任何已有镜像)的Dockerfile,仅将本地的hello程序打包到镜像中,并在启动容器时运行hello。内容如下:

# cat Dockerfile
FROM scratch
COPY hello /
CMD ["/hello"]

 

Dockerfile格式解释:

关键词

说明

#

表示注释

FROM

用于指定基础镜像,scratch表示不基于任何基础镜像。

COPY

表示复制本地文件到容器的指定目录,注意本地文件目录是相对Dockerfile文件所在的目录,而不是系统的根目录。如果是远端的文件,则需使用ADD命令。

CMD

用于指定启动容器时默认执行的命令,一个Dockerfile只有最后一条CMD有效,其它的CMD会被忽略,CMD有三种书写格式。

  1. CMD和ENTRYPOINT

如果在Dockerfile中没有指定ENTRYPOINT,执行命令“docker run”也没有指定“--entrypoint”,则执行CMD指定的命令。另外,可通过命令行参数“--entrypoint”覆盖ENTRYPOINT。

Dockerfile中的CMD有三种书写格式:

 

书写格式

说明

格式1

CMD ["executable","param1","param2"]

EXEC执行方式

格式2

CMD ["","param2"]

指定了ENTRYPOINT时,作为ENTRYPOINT的参数,请注意ENTRYPOINT也分EXECShell两种书写格式。

格式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

 

  1. 什么是Shell执行方式?
# sh -c "/bin/whoami"
root

 

如果CMD和ENTRYPOINT组合使用,则两者均需JSON数组格式。

  1. RUN和CMD

Dockerfile中的每一条RUN命令均会产生一个新的镜像,因此应当尽可能减少RUN命令数,如使用“&&”将多条写成一条。

RUN mkdir /data/test && chown test /data/test

 

RUN和CMD完全不同,RUN是生成镜像时执行,而CMD是启动容器时执行。RUN和镜像相关,CMD和容器相关。

  1. 生成镜像

执行命令“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
  1. 启动容器

最简单的启动容器方法:

# 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”来删除

  1. 镜像进阶

这一节的镜像不从零开始,而是基于已有镜像生成新的镜像。

从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镜像。

  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)
  1. 准备本地程序源码

以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
  1. 编写Dockerfile
# cat Dockerfile.echo1
FROM centos
COPY echo1 /
CMD ["/echo1"]
  1. 生成镜像
# docker build --tag echo1 -f Dockerfile.echo1 .
  1. 启动容器

默认不带参数方式运行(因为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
  1. 常见问题
  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这个文件。

  1. 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文件所在目录,然后再创建镜像。

  1. 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)。

  1. 上传安装包

将安装包(比如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

 

  1. 安装和设置

在/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