Docker
docker介绍配置及基本命令行
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'
第二种方式:启动一个已经终止的容器
docker start [OPTIONS] 容器 [容器2...]
其中:
docker start
:Docker
启动容器的命令关键词;OPTIIONS
: 命令选项;容器
: 需要启动的容器,该容器用“容器ID
”或“容器名”表示,如果指定了多个容器,那么就将这些容器都启动。
假设一个名为firstContainer
的容器处于终止状态,现在需要将它启动,可以这么做:执行docker start firstContainer
,命令执行后,尝试启动firstContainer
容器,并执行该容器的启动命令。
但是如果想启动第一个实例创建的容器,既不知道容器的名字(因为我没有指定)而且也不知道它的id
。该怎么办呢?
查看容器信息
Docker
中有这样一条命令docker ps
,可以查看容器的信息,包括容器ID
,基础镜像,启动命令,创建时间,当前状态,端口号,容器名字。
如果不加任何参数,只执行docker ps
,将会显示所有运行中的容器。例如执行docker ps
,如下图所示,在当前的Docker
环境中,只有一个正在运行的容器,它的容器Id
是fe263c9359dd
,基于ubuntu:latest
镜像,启动命令为“/bin/bash
”,创建时间为2
分钟之前,当前状态为“Up 2 minutes
”,也就是已经运行了2
分钟了,容器名为:firstContainer
。
而如果docker ps –a
命令,可以查看Docker
环境中所有的容器,包括已经停止的容器。执行docker ps –a
后,如下图所示:除了名为firstContainer
的容器外,还可以看到一个id
为ee826f1d58ff
的容器容器(容器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 stop
:Docker
停止容器的命令关键词;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包
-
docker save [OPTIONS] IMAGE [IMAGE...]
其中:
docker save
:Docker
将镜像保存到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
:Docker
从tar
包加载镜像的命令关键词;OPTIIONS
: 命令选项,-i
指定从一个tar
文件中读取,而不是标准输入流中。
例如,从alpine.tar
中加载镜像,对应的语句如下:
docker load < alpine.tar 或者 docker load -i alpine.tar
实例
假设想要将本机的alpine
镜像传到另一台机器,首先,在本机执行docker save alpine > alpine-latest.tar
将alpine
镜像保存成一个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
:Docker
从tar
包加载镜像的命令关键词;OPTIIONS
: 命令选项;文件|URL|
: 指定docker import
的对象,可以是文件或者某个URL
;[镜像名]
: 以<仓库名>:<标签>
的方式来指定。
例如,从container1.tar
中加载镜像,镜像名为test:v1.0
,对应的语句如下:
cat container1.tar | docker import - test:v1.0
实例
在本机以ubuntu
镜像为基础创建了一个容器,并在容器的/dir1
目录下创建了1.txt
和2.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.txt
和2.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.8MB
而ubuntu: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 rmi
:Docker
删除镜像的命令关键词;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
镜像,有以下几种方法:
- 镜像短
ID
:docker rmi 14f6
;(这个代表镜像id
以14f6
开头的镜像,一般而言,前四位可以唯一标志,如果不可以,docker
会提示的) - 镜像长
ID
:docker rmi 14f60031763d
; - 镜像名:
docker rmi ubuntu:latest
; - 镜像的
digest
:docker 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
;若参数image
或tag
指定镜像不存在,则返回错误。
●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
指令时,会在之前指令创建出的镜像的基础上创建一个临时容器,在这里的容器Id
为c5117d908931
,并在容器中运行命令。在命令结束运行后提交新容器为新镜像,并删除临时创建的容器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
<源路径> <目标路径>;
ADD
与COPY
指令在功能上十分相似,但是在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
命令时,如果显示地指定了容器的启动命令,那么会将Dockerfile
中CMD
设置的默认启动命令覆盖,也就是说: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
格式,推荐格式)
ENTRYPOINT
和CMD
一样,都可以指定容器默认的启动命令,但是它又和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 **********#
更多推荐
所有评论(0)