Docker是一种基于GO语言开发的轻量级的隔离软件。与虚拟机不同的是,Docker是直接运行在内核中的,不需要虚拟出一套硬件设备和操作系统,因此十分的轻便与快捷。
使用Docker主要解决的是开发、测试与生产环境的统一问题。
镜像(image):镜像是一种特殊的文件系统,提供了容器运行时所需的程序、库、资源、配置等文件,还为运行时准备的一些配置参数。镜像不包含任何动态数据,其内容在生成后也不会改变。
容器(container):容器可以视为镜像的实例,一个容器其实就是一个有着独立命名空间的独立进程,可以被启动、暂停、删除。容器也是Docker分层中的一层。容器在运行时,会以镜像作为基础层,同时创建一个对应的容器存储层。存储层的生命周期与容器同步,会在容器被删除时销毁。如果想让容器写入宿主机的数据,需要使用数据卷(Volume)。
仓库(repository):仓库是集中管理镜像的地方。类似于git仓库,不过管理的内容从代码文件变成了镜像。Docker有着默认的仓库Docker Hub,对应GitHub,其中有着许许多多的通用镜像,有针对语言的python、golang,针对场景的nginx、tomcat,针对操作系统的centos、ubuntu等等。
Docker的镜像并不是一个完整的文件系统,而是一组文件系统的集合,一个文件系统称为一层。一个层可以被多个其他层复用,这样可以大大减小镜像占用的空间。同时,每一层在是完全独立的,在构建完成之后就不会再被改变,每一层也无法对其它层进行操作。因此在构建镜像时,最好在一层构建完成之后删除无用文件,只保留构建时这一层需要的部分,这样可以大大缩减镜像大小。
镜像需要从仓库获取,使用的也是pull命令。如果不指定仓库地址(repository addr),则会从默认的Docker Hub获取镜像,对应的地址位docker.io。仓库名(repository name)由两部分组成,分别是用户名和软件名。如果不指定用户名,默认为library。tag相当于版本号,如果不指定tag,则会获取最新的版本。
docker pull [option] [repository_addr[:port]]/repository_name[:tag]
Docker Hub可以类比于GitHub,是官方维护的镜像仓库,可以在里面获取官方镜像,还可以推送自己的镜像。
docker search 可以通过名称在仓库中查找相关镜像,查找时可以通过–filter=stars=N筛选出收藏数大于N的镜像。随后可以通过docker pull 将镜像拉取到本地。
构建完成后可以通过docker push
运行一个镜像实际上就是基于该镜像创建一个容器并启动该容器,使用的命令为docker run,格式如下
docker run [option] $image
-i:交互式操作,常与-t一起使用。
-t:启动终端,常与-i一起使用。
–name:指定生成的容器名。
-d:使镜像后台运行。
-p:将主机端口与容器端口进行绑定。-p host_port:container_port。
–rm:退出时删除镜像,常用于测试。
–help:查看所有选项及其使用说明。
基本的查询语句为ls
docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
redis latest 5f515359c7f8 5 days ago 183 MB
nginx latest 05a60462f8ba 5 days ago 181 MB
mongo 3.2 fe9198c04d62 5 days ago 342 MB
<none> <none> 00285df0df87 5 days ago 342 MB
ubuntu 18.04 329ed837d508 3 days ago 63.3MB
ubuntu bionic 329ed837d508 3 days ago 63.3MB
查询结果包含以下几列,REPOSITORY-镜像名,TAG-镜像版本,IMAGE ID-镜像id。
p:只保留镜像id,可以与其他命令和选项结合使用批量对镜像进行操作。
-a:显示包含中间层在内的所有镜像,中间层通常不要操作。
镜像名:镜像名可以直接作为选项输出,此时会作为查询条件
-f:删选条件,详细请参考手册。
–format:指定打印的列,格式为–format “table {{.REPOSITORY}} \t {{.TAG}}”。table表示带首行标题。
此外还可以通过docker system df查看镜像占据的实际空间大小。由于docker会复用一些层,因此image占据的实际空间会比ls显示的空间小很多。
$ docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 24 0 1.992GB 1.992GB (100%)
Containers 1 0 62.82MB 62.82MB (100%)
Local Volumes 9 0 652.2MB 652.2MB (100%)
Build Cache 0B 0B
与linux类似,删除使用rm命令
docker image rm [option] <$image1> [<$image2> <image3> ...]
如上述所示,rm可以同时删除多个镜像,并且可以接受其他命令的输出。rm可以通过指定image的id、名称等来删除镜像,只需要能够与其他镜像区分出即可。
删除分为两个阶段,分别是Untagged和Deleted。ID才是区分不同镜像的唯一标识,而一个ID可以派生出多个不同的镜像标签。Untagged就是删除这些标签的过程,在标签删除后,如果该image id还被其他标签或容器所使,那么该image便不会被删除。Deleted是实际删除一个镜像,只有当该镜像没有任何引用时才会自动执行。
在使用docker image ls之后,可能发现一些REPOSITORY和TAG均为none的镜像,这些镜像便被称为虚悬镜像。这种镜像可以理解为已经过期的镜像,如果一个TAG退出的新的版本,在TAG更新之后会被替换为一个新image id对应的镜像,此时原来的image id对应的镜像就会变成REPOSITORY和TAG均为none的虚悬镜像。
虚悬镜像通常已经没什么用了,建议清理。清理方式为
docker image prune
容器中的所有数据是会随着容器的销毁而被删除的,同时容器也无法直接与宿主机进行数据交互,数据卷(VOLUME)可以实现这些功能。
数据卷的功能包括:
实现容器之间的数据共享与通信
记录容器的操作
可以通过docker volume create 创建一个指定名称的数据卷,在创建完成之后可以通过docker volume ls查看创建的数据卷。在启动容器时,可以通过–mount source=
docker volume create <name>
docker volume ls
docker run --name web -itd --mount source=<volume_name>,target=/data/tools nginx
docker volume rm <name>
docker volume prune
除了使用数据卷,–mount命令还可以将本地目录或本地文件与容器中的目录或文件进行绑定。
docker run --name web -itd --mount type=bind,source=/data/tools,target=/data/tools nginx # 本地目录与容器目录绑定
docker run --name web -itd --mount type=bind,source=$HOME/.bash_history,target=/root/.bash_history # 本地文件与容器文件绑定,可以实现bash操作记录的同步
通过前文我们已知镜像的制作过程就是逐层定制每一层的过程,因此可以使用一个脚本记录每一层的修改、安装、配置、操作命令,这样就可以十分方便的完成镜像的制作。这个脚本就是Dockerfile。
Dockerfile本质上就是一个文本文件,其中包含了一条条的指令(instruction),每条指令对应镜像中的一层,因此每条指令的作用也就是确定当前层如何构建。为了使镜像的体积尽可能小,在制作镜像时需要把完成功能的一组命令使用&&集中到一条指令中去执行。
FROM指令的作用是指基础镜像,也就是我们想要以哪个镜像为基础构建新的镜像。例如想要执行Python脚本,可以使用指令FROM python。
这里有一个特殊的基础镜像scratch。scratch代表一个空镜像,即不包含任何内容。当我们想要在镜像中直接运行可执行文件时使用该镜像可以是镜像体积最小。由于GO语言开发出的最终都是可执行文件,因此常用语GO开发。
RUN的作用是运行一个或一组命令,其后面的命令格式与命令行几乎相同。这里需要注意的是,不要使用编写shell脚本的思路来编写Dockerfile文件。因为每条指令对应的是docker中的一层,如果每条命令都使用一个RUN指令,那么最终制作的镜像会十分冗杂,而且有可能超出最大层数限制。正确的写法应该是使用&&连接一组功能的多个指令。为了便于格式化,Dockerfile中还可以使用 \ 进行换行。
FROM debian:stretch
RUN set -x; buildDeps='gcc libc6-dev make wget' \
&& apt-get update \
&& apt-get install -y $buildDeps \
&& wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
&& mkdir -p /usr/src/redis \
&& tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=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
上述代码中还包含了清空缓存和删除无用软件的操作。之前我们已经了解过,Docker中的每层都是独立存在的,一个层无法修改其他层的数据。因此在一些文件已经没有利用价值时我们要在该层中立即删除,否则该文件会永久跟随该镜像,占用额外的空间。
该命令可以将当前上下文中的文件或目录拷贝到镜像中的指定目录,需要注意的是源地址是指定的上下文的相对路径,无法拷贝上下文路径之外的文件。
源路径可以是多个,还可以是通配符。拷贝时可以使用–chown=:来改变文件所属的用户和组。
COPY config.json /usr/local/app
COPY hom?.txt /mydir
COPY hom* /mydir
与COPY类似,也是拷贝文件。与COPY不同的是,ADD可以获取URL上的信息并进行拷贝,同时还会对gzip等格式的文件做自动解压处理。
ADD命令通常不推荐使用,如果想要拷贝文件使用COPY命令即可,如果想要从网上下载文件使用RUN加wget或curl是一个更好的选择。
容器与虚拟机不同的点在于容器本身只是一个进程,当进程结束容器也就会消失,这个进程也被称为容器的主进程。在默认情况下,主进程是/bin/bash,也就是我们常见的命令行。如果一个容器只想与固定的程序绑定,我们可以使用CMD命令更改容器的主进程。
格式就是CMD 命令行,命令行对应的内容便会被绑定为容器的主进程。这里需要注意的是主进程必须是前台执行的,当主进程执行完毕,容器便会退出。例如我们想要运行一个与nginx绑定的容器,可以使用一下命令:
CMD ["nginx", "-g", "daemon off;"]
与CMD类似,其功能也是指定容器的主程序。不同之处在于,ENTRYPOINT会将CMD之后的命令作为主程序的一段参数使用。利用该功能可以实现一些实用的操作。
FROM ubuntu:18.04
RUN apt-get update \
&& apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*
ENTRYPOINT [ "curl", "-s", "http://myip.ipip.net" ]
例如上述代码就可以调用docker run myip -v来获取curl的详细信息。
配置环境变量,这里配置的环境变量在Dockerfile和容器中均可使用。
ENV key value
构建参数。构建镜像时的环境变量,与ENV类似,但生命周期只在构建中。如果在FROM之前生命的ARG参数,则在FROM之后无法使用。
指定一个目录为匿名卷,容器运行时可以将动态数据写入到这个路径下。
VOLUME /data # 将匿名卷挂载到/data目录下,可以存储动态数据,避免数据写入存储层。
声明端口。该声明并不会占用端口,只是预先声明容器可能占用的端口。只有在运行时启动随机映射,才会映射到该端口。
指定工作路径,之后层的工作路径都会以该位置为准。
指定之后的用户,与WORKDIR类似。
这里需要注意的是,如果有想要以root权限执行的命令,不要使用su或sudo,在docker环境下可能出错。可以使用gosu,代码如下:
# 建立 redis 用户,并使用 gosu 换另一个用户执行命令
RUN groupadd -r redis && useradd -r -g redis redis
# 下载 gosu
RUN wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.12/gosu-amd64" \
&& chmod +x /usr/local/bin/gosu \
&& gosu nobody true
# 设置 CMD,并以另外的用户执行
CMD [ "exec", "gosu", "redis", "redis-server" ]
设置健康检查的命令和参数。如果不使用健康检查,只能通过主程序的运行状态来判断容器的状态,当主程序陷入死锁等情况下这种检查就失效了。HEALTHCHECK为我们提供了自定义健康检查的功能,可以通过判断自定义指令的执行情况,来判断容器的健康状态。
HEALTHCHECK [option] CMD line
常用选项有:
–interval=<探测间隔>
–timeout=<探测超时时长>
–retries=<探测重试次数>
与CMD类似,HEALTHCHECK也只能存在一个,新出现的会覆盖之前的配置。
配置了健康检查的容器可以通过docker container ls查看容器的状态,健康检查的结果可以通过docker inspect查看。
ONBUILD之后可以接任意指令,这些指令在构建当前镜像时不会执行,只有在以当前镜像为基础构建上层镜像时才会执行。
使用label指令可以以键值对的形式为镜像添加元数据(metadata),例如作者,联系方式,文档地址等等。
shell指令可以指定RUN,CMD,ENTRYPOINT指令对应的shell命令的执行地址。默认为[“/bin/sh”, “-c”]。
SHELL ["/bin/sh", "-cex"] # 之后的shell命令执行地址位 /bin/sh -cex
在Dockerfile文件编写完成之后,就可以使用docker build命令制作我们的自定义镜像了,docker build指令的格式如下:
docker build [option] <context/url>
context为镜像编译的上下文,也就是相关文件的存储位置。docker在制作镜像前,会将上下文路径下的所有文件拷贝到引擎中,在那里进行调用。因此这里需要注意的是,当上下文路径被指定之后,docker是无法访问上下文路径之外的文件的。
由于Dockerfile通常在项目的根目录,因此这里的上下文通常指定为当前路径 . 。
常用选项:
-f:指定Dockerfile文件,默认为当前路径下的名为Dockerfile的文件。通常不需要指定。
-o:指定输出路径,默认为当前路径。不需要指定
-t:指定输出镜像的名称,格式为 name:tag。
在构建一个大镜像时,我们可以将编译和执行阶段分别写到不同的Dockerfile中,利用前一个Dockerfile生成的镜像去构建下一层镜像,这样可以大大提升构建的速度并减小镜像占用的大小。
每个阶段对应一次FROM。可以使用COPY --from=: 从任意镜像中拷贝文件。
FROM golang:alpine as builder # 编译镜像
RUN apk --no-cache add git
WORKDIR /go/src/github.com/go/helloworld/
RUN go get -d -v github.com/go-sql-driver/mysql
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
FROM alpine:latest as prod # 运行镜像
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/go/helloworld/app .
CMD ["./app"]
除此之外,还可以使用commit命令将一个容器打包成镜像,命令格式如下:
docker commit [选项] <容器ID或容器名> [<仓库名>[:<标签>]]
此命令构建的镜像为黑箱镜像,且没用合理运用镜像的分层功能,会导致镜像十分臃肿,不推荐使用。
docker启动流程:
docker run [option] <image:tag> [cmd]
cmd为用户指定的容器需要执行的主程序,在构建镜像时可以通过CMD和ENTRYPOINT指定默认的主程序,如果没有指定默认启动bash。
当指定了cmd之后cmd便会称为容器的主程序,主程序执行之后容器会进入exited状态。
常见option:
-it: -i -t结合使用,相当于打开了一个命令行交互界面。
-d: 让容器后台执行,可以使用docker ps -a查看启动的容器,并且可以通过docker logs查看后台运行的docker的标准输出。
start可以用来重新启动一个已经进入exited状态的容器。
docker start <container_id>
容器的终止方式包括:
attach相当于是将指定容器从后台运行变为前端运行,因此当退出attach时该容器也会被终止,因此不推荐使用。
docker attach <container_id>
exec相当于是在指定容器上额外开启一个进程,因此不会影响容器的运行状态,exec退出时只会终止新增的进程,推荐使用。需要注意的是由于exec时重新开启一个进程,因此想要开启终端需要重新指定-it选项。
docker exec [-it] <container_id> [cmd]
export可以将容器打包成文件,相当于给容器做了一个快照
docker export <container_id> > <filepath>
import可以将export打包的镜像文件导入为镜像
cat <filepath> |docker import - <image:tag>
rm可以删除一个已经终止的容器,如果想要删除运行中的容器可以指定-f选项。
docker container rm [-f] <container_id>
prune可以快速清理所有终止状态的容器
docker container prune