Docker介绍配置以及基本指令

docker是什么

与虚拟机通过操作系统实现隔离不同,容器技术只隔离应用程序的运行时环境但容器之间可以共享同一个操作系统,这里的运行时环境指的是程序运行依赖的各种库以及配置。docker可以直接使用主机的环境,解决里不同主机使用项目时需要重新配置环境的麻烦。方便于开发,主要基于容器技术。

来自于知乎的通俗解释:
Docker的思想来自于 集装箱,集装箱解决了什么问题?在一艘大船上,可以把货物规整的摆放起来。并且各种各样的货物被集装箱标准化了,集装箱和集装箱之间不会互相影响。那么我就不需要专门运送水果的船和专门运送化学品的船了。只要这些货物在集装箱里封装的好好的,那我就可以用一艘大船把他们都运走。
docker就是类似的理念。现在都流行云计算了,云计算就好比大货轮。docker就是集装箱。

1.不同的应用程序可能会有不同的应用环境,比如.net开发的网站和php开发的网站依赖的软件就不一样,如果把他们依赖的软件都安装在一个服务器上就要调试很久,而且很麻烦,还会造成一些冲突。比如IIS和Apache访问端口冲突。这个时候你就要隔离.net开发的网站和php开发的网站。常规来讲,我们可以在服务器上创建不同的虚拟机在不同的虚拟机上放置不同的应用,但是虚拟机开销比较高。docker可以实现虚拟机隔离应用环境的功能,并且开销比虚拟机小,小就意味着省钱了。

2.你开发软件的时候用的是Ubuntu,但是运维管理的都是centos,运维在把你的软件从开发环境转移到生产环境的时候就会遇到一些Ubuntu转centos的问题,比如:有个特殊版本的数据库,只有Ubuntu支持,centos不支持,在转移的过程当中运维就得想办法解决这样的问题。这时候要是有docker你就可以把开发环境直接封装转移给运维,运维直接部署你给他的docker就可以了。而且部署速度快。

3.在服务器负载方面,如果你单独开一个虚拟机,那么虚拟机会占用空闲内存的,docker部署的话,这些内存就会利用起来。

总之docker就是集装箱原理。

Java号称“一次编译,到处运行”,因为java虚拟机解决平台的兼容性问题,所以有java虚拟机的地方就能跑java代码;

Docker是:“一次封装,到处运行”,因为docker解决了应用环境的问题,安装了docker的平台就能跑“docker包”,这样就决绝了“开发环境能跑,一上线就崩”的尴尬。

传统虚拟机技术:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QchAMuyj-1652968082671)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20220516145345001.png)]

Docker:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nwUEkFYO-1652968082682)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20220516145811254.png)]

Image和container理解

image:container = 1:N

类比class文件:Java对象 = 1:N

例如 一个项目所需的一切环境,代码,配置文件可以打包成一个image

container是image运行起来的一个实例

docker:运行机制

物理主机安装docker engine 拿到image 根据image 执行container

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s49Bd9c5-1652968082683)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20220516150524659.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CgeFtBo6-1652968082683)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20220516150539764.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nlokzjL7-1652968082683)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20220516150600672.png)]

Docker环境安装

1.首先准备虚拟机

2.参照官网配置环境

Install Docker Engine on Ubuntu | Docker Documentation

Docker基本指令

镜像和容器

1.拉取镜像

docker pull imgname

2.基于镜像建立容器

docker run --name containername -d -p 80:80 imgname

3.进入容器

docker exec -it containername /bin/bash

4.将容器提交成为镜像

docker commit [OPTIONS] CONTAINER [镜像名]  

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YMfYcaDv-1652968082684)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20220518210650901.png)]

现在将对webserver容器做出的修改提交为一个新镜像nginx:v1,只需要执行docker commit webserver nginx:v1

使用commit定制镜像的缺陷

在实际的环境中,我们一般不会使用Commit去构建一个镜像,因为它存在很多的缺陷:

首先,由于commit会将对容器做出所有的修改都保存为镜像,这就意味着我们可能会保存许多不必要的文件变化,例如我仅仅只是为了修改nginx的欢迎页,但是我新增了一些文件,那么这些文件也会跟着保存到镜像中去,显然它们是多余的。

此外,docker commit意味着所有对镜像的操作都是黑箱操作,生成的镜像也被称为黑箱镜像,换句话说,就是除了制作镜像的人知道执行过什么命令、怎么生成的镜像,别人根本无从得知。而且,即使是这个制作镜像的人,过一段时间后也无法记清具体在操作的。虽然docker diff或许可以告诉得到一些线索,但是远远不到可以确保生成一致镜像的地步。这种黑箱镜像的维护工作是非常痛苦的。

既然docker commit存在这么多缺陷,是不是有更好的替代方法呢?没错,不要着急。后面我们会介绍更好的定制镜像的方法:Dockerfile!!

启动容器

第一种方式:新建并启动。

docker run命令会基于指定的镜像创建一个容器并且启动它。docker run的基本语法如下:

docker run [OPTIONS] 镜像名 [COMMAND] [ARG]  

其中,

  • docker run: Docker创建并启动容器的命令关键词;
  • OPTIIONS: 命令选项,最常用的包括-d后台运行容器并返回容器ID-i以交互模式运行容器,-t为容器分配一个伪输入终端,--name 指定启动容器的名称。更多选项请参考Docker帮助文档;
  • 镜像名: 以<仓库名>:<标签>的方式来指定;
  • COMMAND: 设置启动命令,该命令在容器启动后执行;
  • ARG: 其他一些参数。

例如:创建并启动一个容器,容器中具有ubuntu的运行环境,输出hello docker

docker run ubuntu:14.04 echo 'hello docker'
第二种方式:启动一个已经终止的容器
  1. docker start [OPTIONS] 容器 [容器2...]

其中:

  • docker start: Docker启动容器的命令关键词;
  • OPTIIONS: 命令选项;
  • 容器: 需要启动的容器,该容器用“容器ID”或“容器名”表示,如果指定了多个容器,那么就将这些容器都启动。

假设一个名为firstContainer的容器处于终止状态,现在需要将它启动,可以这么做:执行docker start firstContainer,命令执行后,尝试启动firstContainer容器,并执行该容器的启动命令。

但是如果想启动第一个实例创建的容器,既不知道容器的名字(因为我没有指定)而且也不知道它的id。该怎么办呢?

查看容器信息

Docker中有这样一条命令docker ps,可以查看容器的信息,包括容器ID,基础镜像,启动命令,创建时间,当前状态,端口号,容器名字。

如果不加任何参数,只执行docker ps,将会显示所有运行中的容器。例如执行docker ps,如下图所示,在当前的Docker环境中,只有一个正在运行的容器,它的容器Idfe263c9359dd,基于ubuntu:latest镜像,启动命令为“/bin/bash”,创建时间为2分钟之前,当前状态为“Up 2 minutes”,也就是已经运行了2分钟了,容器名为:firstContainer

预览大图

而如果docker ps –a命令,可以查看Docker环境中所有的容器,包括已经停止的容器。执行docker ps –a后,如下图所示:除了名为firstContainer的容器外,还可以看到一个idee826f1d58ff的容器容器(容器id随机生成)。但是这个容器的当前状态为Exited (0) 3 minutes ago,这表示它是处于终止状态的,而且是在3分钟前退出的。

对于这个处于终止状态的容器,可以通过docker start ee826f1d58ff或者docker start g\fracious_lewin启动该容器了。

停止一个容器

使用docker stop停止一个容器

docker stop可以用来终止一个正在运行的容器。它的命令格式如下:

docker stop [OPTIONS] Container [Container ...]  

其中:

  • docker stopDocker停止容器的命令关键词;
  • OPTIONS:命令选项,其中-t指定等待多少秒后如果容器还没终止,就强行停止,默认等待10秒;
  • Container:需要启动的容器,该容器用“容器ID”或“容器名”表示,如果指定了多个容器,那么就将这些容器都启动。

例如想要停止一个名为firstContainer 的容器,可以这么执行docker stop firstContainer。该命令执行完之后,firstContainer将会处于终止状态。而上一节我们谈到过,终止状态的容器,可以使用docker ps –a查看到。

实际工作中,执行docker stop可能并不会立即终止容器,而是需要等待一段时间。前面我们说过,容器实际上是一个进程。而执行docker stop之后,首先会向容器的主进程发送一个SIGTERM信号,让主进程释放资源保存状态,尝试自己终止。但当等待时间超过了-t设置的时间后,会向容器的主进程发送一个SIGKILL信号,使容器立即终止。

在什么情况下容器启动后会立即终止?

实际情况中,除了使用docker stop命令来强制地终止一个容器以外,当容器的启动命令终结时,容器也自动会终止。

docker run --name testcontainer ubuntu echo 'hello docker'为例,echo 'hello docker'就是该容器的启动命令。实际上执行完这条命令后,执行docker ps -a,可以发现testcontainer容器是处于终止状态的,如下图所示:

[root@localhost Desktop]# docker run --name testcontainer ubuntu echo 'hello docker'  hello docker  [root@localhost Desktop]# docker ps -a  CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                     PORTS               NAMES  da14116bf641        ubuntu              "echo 'hello docker'"    6 seconds ago       Exited (0) 4 seconds ago                       testcontainer  

前面我们说过,在容器启动时,会执行容器的启动命令。而执行上述命令创建并启动容器后,由于容器的启动命令echo 'hello docker'会立刻执行完毕,所以容器也随之终止,因此使用docker ps -a查看该容器的状态是终止状态。

现在你应该明白上一关中,为什么docker start ee826f1d58ff去启动第一个实例的容器,然后使用docker ps查看,会看不到该容器了吧?

之前我们介绍过Docker容器是一个进程,实际上它以sh作为主进程。如果主进程停止了,那么容器也就停止了。而如果容器的“启动命令”执行完之后,由于主进程没有命令继续执行,所以主进程会停止,容器也就因此而停止了。

如何才能使容器启动后不立即终止?

如果容器的sh主进程不停止,是不是以为这容器就不会停止?答案是肯定的。因此,如果使启动命令不能执行完毕,或者在执行完启动命令后,容器的sh主进程不停止,那么容器在启动后就不会立即终止了!

下面举两个能使容器启动后不立即停止的例子:

将启动命令设置为死循环

1. docker run ubuntu:14.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"
这条命令在创建并启动容器之后,会执行/bin/sh -c "while true; do echo hello world; sleep 1; done",由于该命令永远都不会执行完毕,除非强行终止,所以容器的主进程sh不会停止,因此容器也将不会停止。但是这样的做的话,无法正常的操作容器,而且它会占据资源,所以这种做法在实际的工作中意义并不大。

将启动命令设置为“启动一直运行的子进程”

2. docker run --name first_container -it ubuntu /bin/bash

执行完这条命令后,创建并启动容器之后,执行/bin/bash,会启动一个子进程,此时父进程(也就是容器的主进程sh)会进入sleep状态,由于sleep状态不是终止状态,所以容器会继续运行。

为什么在容器中输入exit或者执行ctrl D后,容器将会终止呢,这是因为exit会退出(结束)当前进程,也就是/bin/bash,由于子进程结束,sh主进程恢复到运行态,然而由于没有命令需要继续执行,所以sh主进程结,因此容器终止。

save load镜像

将镜像保存到tar包
  1. docker save [OPTIONS] IMAGE [IMAGE...]

    其中:

    • docker saveDocker将镜像保存到tar包的命令关键词;
    • OPTIIONS:命令选项,-o指定写到一个文件中,而不是标准输出流中;
    • IMAGE: 需要保存到tar包的镜像,可以指定多个,用空格隔开。

    例如,将alpine:latest镜像保存到tar包,对应的语句如下:

    docker save alpine:latest > alpine.tar  或者  docker save -o alpine:lateste alpine.tar  
    
从tar包加载镜像

docker load使用docker save保存的tar文件加载镜像,它的具体语法如下:

docker load [OPTIONS]  

其中:

  • docker load:Dockertar包加载镜像的命令关键词;
  • OPTIIONS: 命令选项,-i指定从一个tar文件中读取,而不是标准输入流中。

例如,从alpine.tar中加载镜像,对应的语句如下:

docker load < alpine.tar  或者  docker load -i alpine.tar  
实例

假设想要将本机的alpine镜像传到另一台机器,首先,在本机执行docker save alpine > alpine-latest.taralpine镜像保存成一个tar文件。

[root@localhost dir1]# docker save alpine > alpine-latest.tar  [root@localhost dir1]# ls  alpine-latest.tar  

然后我们将alpine-latest.tar文件复制到了到了另一个机器上,然后在该机器上执行docker load < alpine-latest.tar,这样就可以使用tar包将镜像加载进来了。如下图所示,可以通过docker images alpine查看到alpine:latest镜像。

[root@localhost tempdir]# docker load < alpine-latest.tar  5bef08742407: Loading layer 4.221 MB/4.221 MB  Loaded image: alpine:latest  [root@localhost tempdir]#docker images alpine   REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE  alpine              latest              7328f6f8b418        5 weeks ago         3.96 MB  

如果我们结合这两个命令以及ssh甚至pv的话,利用 Linux强大的管道,我们可以写一个命令完成从一个机器将镜像迁移到另一个机器,并且带进度条的功能:
docker save <镜像名> | bzip2 | pv | ssh <用户名>@<主机名> 'cat | docker load'

导入导出容器

将“容器的文件系统”保存到tar包

docker export是将“容器的文件系统”导出为一个tar包。注意是操作的对象是容器!它的具体语法如下:

docker export [OPTIONS] CONTAINER  

其中:

  • docker export: Docker将容器导出到tar包的命令关键词;
  • OPTIIONS: 命令选项,-o指定写到一个文件中,而不是标准输出流中;
  • Container: 需要导出到tar包的容器。

例如,将容器container1的“文件系统”保存到tar包,对应的语句如下:

docker export container1 > container1.tar  或者  docker export  container1 -o container1.tar  
从tar包导入一个镜像

docker import使用docker export导出的tar包加载为一个镜像。它的具体语法如下:

docker import [OPTIONS] 文件|URL|- [镜像名]  

其中:

  • docker import: Dockertar包加载镜像的命令关键词;
  • OPTIIONS: 命令选项;
  • 文件|URL|: 指定docker import的对象,可以是文件或者某个URL
  • [镜像名]: 以<仓库名>:<标签>的方式来指定。

例如,从container1.tar中加载镜像,镜像名为test:v1.0,对应的语句如下:

cat container1.tar | docker import - test:v1.0  
实例

在本机以ubuntu镜像为基础创建了一个容器,并在容器的/dir1目录下创建了1.txt2.txt两个文件,然后将改容器导出为tar文件。

[root@localhost step2]# docker run -it ubuntu /bin/bash  root@a2864c3ed14f:/# touch /dir1/1.txt  root@a2864c3ed14f:/# touch /dir1/2.txt  [root@localhost tempdir]# docker export a286 > ubuntu-test.tar  [root@localhost tempdir]# ls  ubuntu-test.tar  

执行cat ubuntu-test.tar | docker import - ubuntu:test命令,将导出的tar(ubuntu-test.tar)导入成一个镜像,镜像名为ubuntu:test。然后使用ubuntu:test创建一个容器,查看容器中/dir1的内容,发现1.txt2.txt都存在。

[root@localhost tempdir]# cat ubuntu-test.tar | docker import - ubuntu:test  sha256:34be0173049d9f177d84117a786bc02de18c9c84137ea9c61288810c0917c671
docker export和docker save的区别

首先,两者的操作对象不同。docker save是将一个镜像保存为一个tar包,而docker export是将一个容器快照保存为一个tar包。

然后,docker export导出的容器快照文件将丢弃所有的历史记录和元数据信息,即仅保存容器当时的快照状态;而docker save保存的镜像存储文件将保存完整记录,体积也要大。下图就能够很好的说明,ubuntu:test仅仅占97.8MBubuntu:latest却占了120MB

[root@localhost step2]# docker images ubuntu  REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE  ubuntu              test                34be0173049d        5 seconds ago       97.8 MB  ubuntu              latest              14f60031763d        2 weeks ago         120 MB  [root@localhost tempdir]# docker run ubuntu:test ls /dir1  1.txt  2.txt  

删除镜像

删除镜像

如果要删除本地的镜像,可以使用 docker rmi (注意rm为删除容器,而rmi为删除镜像,其中i代表image)命令,它的具体语法如下:

docker rmi [OPTIONS] IMAGE [IMAGE...]

其中:

  • docker rmiDocker删除镜像的命令关键词;
  • OPTIIONS: 命令选项,-f强制删除镜像;
  • IMAGE:需要删除的镜像。这里的镜像可以用“镜像短ID”、“镜像长ID”、“镜像名”、“镜像的digest”来标识。

使用docker images --digests查看镜像的具体信息,包括镜像的digest;如下图所示:

[root@localhost Desktop]# docker images --digests ubuntu  REPOSITORY          TAG                 DIGEST   IMAGE ID            CREATED             SIZE  ubuntu              latest             sha256:84c334414e2bfdcae99509a6add166bbb4fa4041dc3fa6af08046a66fed3005f   14f60031763d        2 weeks ago         120 MB 

删除ubuntu:latest镜像,有以下几种方法:

  1. 镜像短IDdocker rmi 14f6;(这个代表镜像id14f6开头的镜像,一般而言,前四位可以唯一标志,如果不可以,docker会提示的)
  2. 镜像长IDdocker rmi 14f60031763d
  3. 镜像名: docker rmi ubuntu:latest
  4. 镜像的digestdocker rmi ubuntu@sha256:84c334414e2bfdcae99509a6add166bbb4fa4041dc3fa6af08046a66fed3005f

以上的方法都能删除掉ubuntu:v1镜像。但日常生活中,我们比较常用的是短ID以及镜像名,因为用起来最方便。

删除多个镜像

我们可以使用 docker images -q来配合使用docker rmi,这样可以成批的删除希望删除的镜像。

docker images -q redis会输出所有仓库名为redis的镜像id,所以如果想要删除所有仓库名为redis的镜像,可以这么写:

docker rmi $(docker images –q redis)  

如果想要删除所有镜像,可以这么写:

docker rmi $(docker images –qa)

如果想要使用docker rmi删除一个镜像,需要注意需要先将使用该镜像的容器删除掉,否则该镜像不能删除成功。当然也可以使用docker rmi -f强制删除该镜像!

镜像管理,建立私人仓库

创建一个私人仓库

Docker Hub中提供了创建私人仓库的镜像Resposity(镜像仓库):Registry,本例将以Registry:2镜像为例,构建一个私人仓库。

docker run -d -p 5000:5000 --restart=always --name registry registry:2  

只需要上面这一条命令,一个私人仓库就创建好了。从这条命令可以看出,这个私人仓库以容器的形式运行着。其中--restart=always是指在Docker服务重启或者registry容器退出时会重新启动。而-p是指将宿主机的5000端口映射到容器的5000端口,这样就可以通过宿主机ip:5000访问到容器的5000端口了。(registry容器默认会监听5000端口)。-d参数是指在后台运行。

当然还有其他的配置,例如-v指定私人仓库的存储位置,添加-v /mnt/registry:/var/lib/registry可以将私人仓库的存储位置设置为宿主机的/mnt/registry

更多更详细的配置可以参考:
https://docs.docker.com/registry/deploying/#start-the-registry-automatically 。

私人仓库(容器)已经构建好了,那怎么将镜像推送到私人仓库或者将私人仓库拉取镜像呢?

将镜像推送到私人仓库
(1)使用docker tag 给镜像加上一个标签

如果想要将镜像推送到私人仓库而不是Docker Hub,首先必须使用docker tag命令,使用主机名和端口来标记一个镜像,如下所示,为ubuntu:latest镜像加上一个localhost:5000/my-ubuntu:latest的标签。

docker tag ubuntu:latest localhost:5000/my-ubuntu  
(2)使用docker push将镜像推送到私人仓库

使用docker push命令可以将镜像推送到仓库,默认情况下会将镜像推送到官方仓库Docker Hub中去,但是如果推送一个“用主机名和端口来标记”的镜像,那么就会推送到私人仓库。

docker push localhost:5000/my-ubuntu  
从私人仓库拉取一个镜像

除了推送以外,当然还可以从私人仓库拉取镜像,docker pull可以从仓库拉取某个镜像,默认情况下,也是从官方仓库拉取。当我想从私人仓库拉取my-ubuntu:latest镜像。执行以下命令就行了。

docker pull localhost:5000/my-ubuntu  
查看或者删除私人仓库中的镜像

Docker提供的Registry镜像没有提供查看镜像和删除镜像的指令,但是有第三方的软件可以提供这些功能,例如:harbor

harbor提供一个可视化的界面来操作私人仓库,包括查看私人仓库中的镜像以及删除私人仓库中的镜像,除此以外,还有日志等非常有用的功能。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pGUcFdZA-1652968082686)(https://data.educoder.net/api/attachments/169473)]

具体的安装与介绍请参照:https://github.com/vmware/harbor/ 。

删除私人仓库

私人仓库实质上就是一个容器,所以删除私人仓库就是删除私人仓库对应的容器。我们可以使用docker rm -f 强制删除删除它,但是这样删除之后,私人仓库中存储的镜像并不会被删除掉。如果你想在删除私人仓库的同时,也将镜像删除,需要添加-v参数,也就是docker rm -f -v。例如删除本地的私人仓库,可以执行以下语句:

docker rm -vf myregistry  

Dockerfile构建镜像

commit构建一个镜像,由于commit在构建镜像时,很容易将无关内容添加到镜像且维护起来十分困难。所以我们不推荐使用commit来构建一个镜像。官方推荐使用Dockerfile来构建一个镜像,

Dockerfile简介

从之前的学习中我们可以了解到:镜像的定制实际上就是定制每一层所添加的配置、文件。那么如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是Dockerfile

Dockerfile描述了组装镜像的步骤,其中每一条命令都是单独执行的,除了FROM指令外,其他每一条指令都在上一条指定所生成的镜像基础上执行,执行完会生成一个新的镜像层,新的镜像层覆盖在原来的镜像层之上,从而形成了新的镜像。Dockerfile所生成的最终镜像就是在基础叠加镜像上一层层的镜像层组成的。

Dockerfile中,指令不区分大小写,但是为了与参数区分,推荐大写。Docker会顺序执行Dockerfile中的指令,第一条必须是FROM指令,它用于指定构建镜像的基础镜像。在Dockerfile中,以#开头的行是注释。

下面我们开始介绍Dockerfile最基本的两条指令:FROM指令和RUN指令。

FROM指令和RUN指令

FROM指定基础镜像;
格式:FROM <image>FROM <image>:<tag>

FROM指令的功能是为后面的指令提供基础镜像,因此一个有效的Dockerfile必须以FROM指令作为第一条非注解指令。若FROM指令中tag参数为空,则tag默认为latest;若参数imagetag指定镜像不存在,则返回错误。

RUN执行命令;
格式:RUN <command>shell格式)或RUN [“executable”, “param1“, “param2”]exec格式,非常推荐)。

RUN 指令是用来执行命令行命令的。RUN指令会在前一条命令创建出的镜像的基础上创建一个容器,并在容器中运行命令。在命令结束运行后提交新容器为新镜像,新镜像被Dockerfile的下一条指令使用。

之前说过,Dockerfile中每一个指令都会建立一个镜像层,RUN也不例外。每一个RUN 的行为,就和之前学习的docker commit定制镜像的过程一样:在之前镜像的基础上创建一个容器,在其上执行这些命令,执行结束后,最后 commit 这一层的修改,构成新的镜像。

使用Dockerfile构建一个镜像

下面介绍使用Dockerfile构建一个镜像,步骤如下:

  • 首先创建一个空文件夹:mkdir newdir
  • 然后进入该文件夹:cd newdir
  • 在该文件夹下创建一个名为Dockerfile的文件,根据实际需求补全Dockerfile的内容;
  • 使用Dockerfile构建一个镜像:docker build -t testimage .(注意这个小数点)其中-t指定新镜像的镜像名。

下面举一个实例,使用Dockerfile构建一个名为testimage的镜像,该镜像具备ubuntu:latest的运行环境,而且在镜像的/目录下创建了一个dir1文件夹。

#先创建一个新的空文件夹  mkdir newdir  #进入这个新文件夹中  cd newdir  #创建一个Dockerfile文件  touch Dockerfile  #补全Dockerfile的内容(为了方便展示,这里用的是echo向Dockerfile中输入内容)  echo "FROM ubuntu:latest" > Dockerfile  echo "RUN mkdir /dir1" >> Dockerfile  #使用该Dockerfile构建一个名为testimage的镜像  docker build -t testimage .  
Dockerfile构建镜像的过程详解:

上面的实例创建了一个Dockerfile文件,Dockerfile的内容如下:

FROM ubuntu:latest  RUN mkdir /dir1  

执行docker build命令,指定使用Dockerfile构建一个镜像。执行结果如下所示:

[root@localhost newdir]# docker build -t testimage .  Sending build context to Docker daemon 2.048 kB  Step 1/2 : FROM ubuntu   ---> 14f60031763d  Step 2/2 : RUN mkdir dir1   ---> Running in c5117d908931   ---> cb0193727724  Removing intermediate container c5117d908931  Successfully built cb0193727724  

Docker指令是从上到下一层一层执行的,所以在使用这个Dockerfile构建镜像时,首先执行FROM ubuntu:latest这条指令。

FROM ubuntu:latest指定ubuntu:latest作为基础镜像,也就是将ubuntu:latest镜像的所有镜像层放置在testimage镜像的最下面。

然后执行RUN mkdir dir1指令,前面我们说过,执行RUN指令时,会在之前指令创建出的镜像的基础上创建一个临时容器,在这里的容器Idc5117d908931,并在容器中运行命令。在命令结束运行后提交新容器为新镜像,并删除临时创建的容器c5117d908931

Dockerfile的所有指令执行完后,新镜像就构建完成了!

注意事项,谨慎使用RUN
修改前的Dokcerfile文件

既然RUN就像 Shell 脚本一样可以执行命令,那么是否就可以像Shell 脚本一样把每个命令对应一个RUN呢?比如这样:

FROM debian:jessie  RUN apt-get update  RUN apt-get install -y gcc libc6-dev make  RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.tar.gz"  RUN mkdir -p /usr/src/redis  RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1  RUN make -C /usr/src/redis  RUN make -C /usr/src/redis install  

上面这个Dockerfile是为了编译、安装 redis可执行文件。虽然它能够完成了所需的功能,但是正如之前说过,Dockerfile中每一个指令都会建立一层,RUN 也不例外。每一个RUN的行为,都会创建一个新的镜像层。

而上面的这种写法,创建了8层镜像(1层基础镜像+7层由RUN执行创建的镜像)。这是完全没有意义的,而且很多运行时不需要的东西,都被装进了镜像里,比如编译环境、更新的软件包等等。结果就是产生非常臃肿、非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。

修改后的Dockerfile文件

因为之前所有的命令只有一个目的,就是编译、安装 redis 可执行文件。因此没有必要建立很多层,这只是一层的事情。因此,修改之后的Dockerfile文件并没有使用很多个RUN指令,而仅仅使用一个RUN 指令,并使用 &&将各个命令串联起来。除此以外,把redis的编译环境、更新的软件包也通通清除掉了,减少镜像占用的存储空间。如下所示,修改之后的Dockerfile构建完成后是就只会有2层镜像了(1层基础镜像+1层由RUN执行创建的镜像)。

FROM debian:jessie  RUN buildDeps='gcc libc6-dev make' \  && apt-get update \  && apt-get install -y $buildDeps \  && wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.tar.gz" \  && mkdir -p /usr/src/redis \  && tar -xzf redis.tar.gz -C /usr/src/redis --strip-component  s=1 \  && make -C /usr/src/redis \  && make -C /usr/src/redis install \  && rm -rf /var/lib/apt/lists/* \  && rm redis.tar.gz \  && rm -r /usr/src/redis \  && apt-get purge -y --auto-remove $buildDeps  

Dockerfile的编写过程中一定要牢记一点:镜像的每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除。在最终容器运行的时候,虽然不会看到这个文件,但是实际上该文件会一直跟随镜像。

docker build copy add

docker build命令详解

Dockerfile创建完成后,可以使用docker build命令根据Dockerfile构建一个镜像。在上一关中,我们在Dockerfile所在的文件夹下执行docker build -t myimage .这条命令,然后镜像就被构建了。现在我们来详细地将这条命令。该docker build的命令格式如下:

docker build [OPTIONS] 上下文路径|URL  

其中:

  • docker build: 用Dockerfile构建镜像的命令关键词;
  • [OPTIONS]: 命令选项,常用的指令包括-t指定镜像的名字,-f显示指定Dockerfile,如果不使用-f,则默认将上下文路径下的名为Dockerfile的文件认为是构建镜像的“Dockerfile”;
  • 上下文路径|URL: 指定构建镜像的上下文的路径,构建镜像的过程中,可以且只可以引用上下文中的任何文件。

现在让我们在看看docker build -t myimage .这条命令,在这条命令中,使用-t指定了镜像名为myimage,由于没有使用-f指令,所以默认使用上下文路径下名为Dockerfile的文件认为是构建镜像的“Dockerfile”。最后指定上下文路径,在这条命令中,上下文路径是.

如果你学过Linux,你应该非常清楚上述命令中的小数点.代表的意思。在Linux中,小数点.代表着当前目录。所以docker build -t myimage .中小数点.其实就是将当前目录设置为上下文路径。

执行docker build后,会首先将上下文目录的所有文件都打包,然后传给Docker daemon,这样Docker daemon收到这个上下文包后,展开就会获得构建镜像所需的一切文件。

如下图所示,在执行完docker build后,会首先sending build context to Deckor daemon,也就是将上下文目录下所有文件打包发给Docker daemon。所以在使用Dockerfile文件时构建镜像时,一般将它放在一个空文件夹下,就是为了防止将其他多余的文件传出去。然后依次执行Dockerfile的指令,如果指令正确执行,则继续执行下一条,直到所有指令执行正确完毕,镜像构建完成;如果指令执行出错,终止镜像构建。

[root@localhost newdir]# docker build -t myimage .  Sending build context to Docker daemon 2.048 kB  Step 1/2 : FROM ubuntu   ---> 14f60031763d  Step 2/2 : RUN mkdir dir1   ---> Running in c5117d908931   ---> cb0193727724  Removing intermediate container c5117d908931  Successfully built cb0193727724  

除了从本地构建以外,docker build还支持从URL构建,比如可以直接从Git repo中构建,这里也不展开介绍了,如果你对这个感兴趣,可以查看:

https://docs.docker.com/engine/reference/commandline/build/#tarball-contexts

COPY指令和ADD指令

COPY复制文件;
格式:COPY <源路径> <目标路径>;

COPY 指令将从构建上下文目录中 <源路径> 的文件或目录复制到新的一层的镜像内的 <目标路径> 位置。<源路径>所指定的源必须在上下文中,即必须是上下文根目录的相对路径!<目标路径> 可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR指令来指定,后面介绍)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建目录。

ADD更高级的文件复制;
格式:ADD <源路径> <目标路径>;

ADDCOPY指令在功能上十分相似,但是在COPY的基础上增加了一些功能。比如,源路径可以是一个指向一个网络文件的URL,这种情况下,Docker引擎会试图下载这个URL指向的文件到<目标路径>去。

此外,当<源路径>为一个tar压缩文件时,该压缩文件在被复制到容器中时会被解压提取。但是使用COPY指令只会将tar压缩文件拷贝到<目标路径>中。如下图所示:

[root@localhost tempdir]# docker build -t myimage .  Sending build context to Docker daemon  12.8 kB  Step 1/2 : FROM ubuntu   ---> 14f60031763d  Step 2/2 : COPY ./hello.txt.tar /dir1/   ---> 070559867e22  Removing intermediate container 1e55f9f19333  Successfully built 070559867e22  [root@localhost tempdir]# docker run myimage ls /dir1/  hello.txt.tar  

ADD指令如果 <源路径> 为一个tar压缩文件的话,ADD 指令将会自动解压缩这个压缩文件到 <目标路径> 去。如下图所示:

[root@localhost tempdir]# docker build -t myimage .  Sending build context to Docker daemon  12.8 kB  Step 1/2 : FROM ubuntu   ---> 14f60031763d  Step 2/2 : ADD ./hello.txt.tar /dir1/   ---> ead6431f75ba  Removing intermediate container f5fdcd97e196  Successfully built ead6431f75ba  [root@localhost tempdir]# docker run myimage ls /dir1/  hello.txt  

这样,如果你只需要tar包中的文件内容而不需要tar包,不要先COPY ./hello.txt.tar.gz,然后RUN tar –xvf hello.txt.tar.gz && rm hello.txt.tar.gz。请直接使用ADD指令,ADD ./hello.txt.tar.gz

因为镜像的每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。比如,删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除。在最终容器运行的时候,虽然不会看到这个文件,但是实际上该文件会一直跟随镜像。

cmd和entrypoint指令

CMD 指定默认的容器主进程启动命令

格式:CMD <command>shell格式)或 CMD [“executable”,”param1”,”param2”](exec格式,推荐格式)或 CMD[”param1”,”param2”]。(为ENTRYPOINT指令提供参数)

CMD指令提供容器启动时运行的默认命令,例如ubuntu镜像默认的CMD/bin/bash,因此我们可以直接使用 docker run -it ubuntu进入bash

同时也可以使用docker run -it ubuntu cat /etc/os-release,执行该命令后会输出系统版本信息。因为当在执行docker run命令时,如果显示地指定了容器的启动命令,那么会将DockerfileCMD设置的默认启动命令覆盖,也就是说:cat /etc/os-release命令会替代成为容器的启动命令,所以输出了系统版本信息。

在指令格式上,一般推荐使用exec格式,因为使用 shell格式时,实际的命令会被包装为 sh -c 的参数的形式进行执行。比如:CMD echo $HOME,在实际执行中,会将其变更为:CMD [ "sh", "-c", "echo $HOME" ]

ENTRYPOINT指令

ENTRYPOINT 指定默认的容器主进程启动命令

格式:ENTRYPOINT <command>shell格式)或ENTRYPOINT [“executable”,”param1”,”param2”]。(exec格式,推荐格式)

ENTRYPOINTCMD一样,都可以指定容器默认的启动命令,但是它又和CMD有所不同。上面我们说过,用户在执行docker run命令创建并启动容器时,如果指定了启动命令,那么“该启动命令”会覆盖CMD指令设置的默认启动命令,但是ENTRYPOINT设置的启动命令该不能被覆盖。

细心的同学可能发现了CMD命令可以为ENTRYPOINT指令提供参数。实际上,如果使用Dockerfile构建镜像时,既使用了ENTRYPOINT指令,又指定了CMD指令,那么CMD指令的含义就发生了改变, CMD 的内容将作为参数传给 ENTRYPOINT 指令,换句话说实际执行时,变成了<ENTRYPOINT> <CMD>。同时,如果执行docker run基于该镜像创建并启动容器,并设置了启动命令时,docker run设置的“启动命令”依然会覆盖CMD的内容,但也仅仅是作为ENTRYPOINT指令的参数。

实例

假设需要一个得知使用者当前公网IP的镜像,可以使用下面的Dockerfile构建一个镜像。

FROM centos  RUN yum install curl  CMD ["curl","-s","http://ip.cn"]  

执行docker build -t myip .来构建一个名为myip的镜像,镜像构建完成后,如果想要查询当前公网的IP,执行docker run myip,如下所示:

[root@localhost tempdir]# docker run myip  当前 IP:113.247.230.194 来自:湖南省长沙市 电信  

嗯,这么看起来好像可以直接把镜像当做命令使用了,不过命令总有参数,如果我们希望加参数呢?比如从上面的 CMD 中可以看到实质的命令是 curl ,那么如果我们希望显示HTTP头信息,就需要加上-i参数。那么我们可以直接加 -i参数给 docker run myip么?

[root@localhost tempdir]# docker run  myip -i  container_linux.go:247: starting container process caused "exec: \"-i\": executable file not found in $PATH"  docker: Error response from daemon: oci runtime error: container_linux.go:247: starting container process caused "exec: \"-i\": executable file not found in $PATH".  

我们可以看到执行明后后输出了 executable file not found的错误信息,也就是“可执行文件找不到”。之前我们说过,跟在镜像名后面的是command,运行时会替换 CMD 的默认值。因此这里的-i 替换了原来的CMD,而不是添加在原来的 curl -s http://ip.cn 后面。而 -i 根本不是命令,所以自然找不到。

那么如果我们希望加入-i这参数,我们就必须重新完整的输入这个命令:docker run myip curl -s http://ip.cn –i。这显然不是很好的解决方案,而使用 ENTRYPOINT 就可以解决这个问题。现在改写Dockerfile,使用ENTRYPOINT设置启动命令:

FROM centos  RUN yum install curl  ENTRYPOINT ["curl","-s","http://ip.cn"]  

这次我们再来尝试直接使用 docker run myip以及 docker run myip -i :可以看到,这次成功了。这是因为当存在 ENTRYPOINT 后,docker run命令ENTRYPOINT不会被覆盖。它会作为参数传给ENTRYPOINT,从而达到了我们预期的结果。

[root@localhost tempdir]# docker run myip  当前 IP:113.247.230.194 来自:湖南省长沙市 电信  [root@localhost tempdir]# docker run myip -i  HTTP/1.1 200 OK  Server: nginx/1.11.9  Date: Wed, 09 Aug 2017 08:32:24 GMT  Content-Type: text/html; charset=UTF-8  Transfer-Encoding: chunked  Connection: keep-alive  当前 IP:113.247.230.194 来自:湖南省长沙市 电信  
实例2

本关的编程任务是补全step3/dockerfile3.sh文件中的内容,要求使用Dockerfile构建一个名为mydisk:v1的镜像,具体要求如下:

  • 补全Dockerfile的内容,该Dockerfile的内容如下:
  • busybox:latest为基础镜像;
  • 默认情况下,将启动命令设置为df -Th。要求df命令不能被覆盖,但-Th能够被覆盖;(df命令用来查看磁盘的信息)
  • 使用docker build基于该Dockerfile构建一个名为mydisk:v1的镜像。
#创建一个空文件夹,并进入其中

mkdir newdir3

cd newdir3

\#创建一个Dockerfile文件

touch Dockerfile

\#假设我的Dockerfile文件为

\#FROM ubuntu

\#RUN mkdir dir1

\#可以这么写:

\# echo 'FROM ubuntu' > Dockerfile

\# echo 'RUN mkdir dir1'>> Dockerfile

\#输入Dockerfile文件内容

\#********** Begin *********#

\#以busybox为基础镜像

echo 'FROM busybox' > Dockerfile

\#默认情况下,将启动命令设置为df -Th。要求df命令不能被覆盖,但-Th能够被覆盖。

echo 'ENTRYPOINT ["df"]' >> Dockerfile

echo 'CMD ["-Th"]' >> Dockerfile

\#********** End **********#



\#文件内容完毕,在当前文件夹中执行

\#********** Begin *********#

\#以该Dockerfile构建一个名为mydisk:latest的镜像

docker build -t mydisk:latest .

\#********** End **********#

ckerfile构建一个名为mydisk:v1`的镜像。

#创建一个空文件夹,并进入其中

mkdir newdir3

cd newdir3

\#创建一个Dockerfile文件

touch Dockerfile

\#假设我的Dockerfile文件为

\#FROM ubuntu

\#RUN mkdir dir1

\#可以这么写:

\# echo 'FROM ubuntu' > Dockerfile

\# echo 'RUN mkdir dir1'>> Dockerfile

\#输入Dockerfile文件内容

\#********** Begin *********#

\#以busybox为基础镜像

echo 'FROM busybox' > Dockerfile

\#默认情况下,将启动命令设置为df -Th。要求df命令不能被覆盖,但-Th能够被覆盖。

echo 'ENTRYPOINT ["df"]' >> Dockerfile

echo 'CMD ["-Th"]' >> Dockerfile

\#********** End **********#



\#文件内容完毕,在当前文件夹中执行

\#********** Begin *********#

\#以该Dockerfile构建一个名为mydisk:latest的镜像

docker build -t mydisk:latest .

\#********** End **********#
Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐