跳转至

(3):一篇学会使用Docker

前言

至今入IT的6年小跑中,学过见过的很多,但记住的不多。记忆犹新的是《代码大全》作者“Steve McConnell”说的一句话“应当对所有重复事情零容忍”,而Docker来的时间刚刚好,给我带来了全方面的效率提升。

Docker作为新时代的产物拥有许多靓丽的地方,很多人对Docker应该还是很迷茫,除了觉得使用它去部署程序或则环境简单外并没有其他的认知或者应用。

Docker可以从以下几个方面提供帮助:

  • 研发人员可以通过Docker去构建与项目贴合的开发环境,并利用Docker的打包和分发能力轻松的进行共享。
  • 运维人员可以通过Docker Compose去编排本机服务,通过k8s去编排集群服务。
  • 程序员更可以结合插件化的VSCode编辑器去打造程序员个人的修仙炉

对个人使用来说,集群服务暂且用不到,因此主要应用的是“修仙炉”和“Compose”。“修仙炉”是对多语言傍身或者多项目开发者来说的利器法宝,而Compose则是主机运维工作的利器法宝。

此次进阶过程我们从几个维度展开:

  • 运行容器
  • 构建镜像
  • 配置容器
  • 编排主机服务
  • 分享容器

在开始前需要先简单了解下容器状态变迁过程

docker状态变迁
docker状态变迁图

图中所示的动作都是docker命令。即:

  • docker create命令创建容器,容器的初始状态为“停止”。
  • docker start命令用于启动容器,使容器进入“运行”状态。
  • docker pause命令将容器中的所有进程暂停,使容器进入“暂停”状态。
  • docker unpause命令恢复容器中被暂停的进程,使容器重新进入“运行”状态。
  • docker stop命令用来停止容器,使容器进入“停止”状态。
  • docker rm命令用来删除容器。
  • 如果在docker create的时候,给了--restart参数,且容器在“运行”时抛出了异常,这时候容器会自动重启。

因此,容器的生命周期从docker create开始,直至docker rm后消亡。


运行容器

从上文我们了解到了docker createdocker start两个命令分别用于“创建”和“运行”容器。而在实际使用中却很少见,那是因为还有一个更便利的命令docker run,该命令能自动完成创建和运行两个动作,除此之外,如果本地没有存在所需的镜像的话,它还能自动从Registry拉取镜像:

$ docker run -d --name http homqyy/example_http
  • -d:daemon,后台运行的意思。
  • --name:设置容器名称,比如这里设置容器名称为http
  • homqyy/example_http:用来运行容器的镜像,homqyy是作者,example_http是镜像名称。

通过docker ps命令,可以查看当前正在运行的容器:

docker ps
docker ps示例图

如上所示,我们可以看到一个名(NAMES)为http的容器正在运行,其运行了两分钟(STATUS: Up 2 minutes),它是通过镜像(IMAGEhomqyy/example_http创建的,且创建于2分钟前(CREATE: 2 minutes ago)。

既然运行了容器,那么我们肯定有操作容器内部进程或文件的需求,就像我们通过操作虚拟机一样。docker也同样支持,只需要使用docker exec命令即可,该命令时可以实现“在目标容器中执行特定命令”:

$ docker exec -it http bash
  • -i:interactive,交互的意思,它将目标容器的STDIN映射到当前终端。
  • -t:tty,伪终端的意思,它将为目标容器创建一个伪终端(这里不详细展开,它超过了本文章的陈述范围),配合-i就起到了跟虚拟机一样的效果,即:打开一个终端(输出),并将输入映射到此容器中。
  • http:目标容器的名称,这里也可以用容器的ID,即在本示例中的 2d9afdd06269
  • bash:要在容器中运行的命令,这里是bash,因此相当于在目标容器中运行一个shell程序,同时由于我们创建了终端并映射了输入,从而达到了跟虚拟机一样的效果。

这时候用ls命令看一下目录情况,可以明显的看出我们已经进入到容器内部中了。我们尝试修改下容器中的nginx配置文件/etc/nginx/nginx.conf,并通过命令/usr/sbin/nginx -t测试一下配置文件是否有误:

$ sed -i 's/listen 8080/listen 8081/g' /etc/nginx/nginx.conf
$ /usr/sbin/nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
  • 为了方便我这里直接用sed工具来修改配置文件,将listen 8080改为listen 8081;
  • 当然,这里就不进行reload重载配置了。

由于容器默认情况下是不允许直接被访问的,因此我们此时打开浏览器访问:http://localhost:8080是无法访问成功的:

无法直接访问容器
无法直接访问容器示例图

为了能够在宿主机中访问容器的服务,需要开放端口,方法是在docker run命令中追加-p参数来指定端口的映射关系,例如这里再新起一个http服务:

docker run -d --name http2 -p 50000:8080 homqyy/example_http
  • -p <host_port:container_port/protocol>:此参数可以指定端口映射关系,此样例的意思是将主机的50000端口映射到容器的8080端口,这意味着可以访问主机的50000端口来访问容器中的8080端口,即:在主机的浏览器中访问http://localhost:50000就相当于访问容器中的8080端口的服务。
    • host_port:宿主机的端口
    • container_port:容器中的端口
    • protocol:协议,值可以是udptcp
  • 此时再次用主机的浏览器访问http://localhost:50000,将得到正确的返回:
访问容器服务
访问容器服务示例图

注意

至此总共运行了http和http2两个容器,且是由相同的镜像homqyy/example_http产生的,这里表明了镜像与容器的关系是“1对n”的。这时查看一下两个容器,看一下彼此间的区别,可以观察到PORTS列有明显区别:

docker ps |grep homqyy/example_http
... PORTS                                                     NAMES
... 80/tcp, 0.0.0.0:50000->8080/tcp, :::50000->8080/tcp       http2
... 80/tcp, 8080/tcp                                          http
  • http2体现出了主机和容器的映射关系,这也说明我们可以通过此来查看容器与主机的映射关系。
  • 而那些没有映射关系的端口是怎么出现的?其实是“镜像”的作者在构建镜像的时候手动指示的,用来告知使用者,运行容器时会提供哪些端口服务。

在运行完样例后可以开始清除容器和镜像了:

docker stop http
docker rm http
docker stop http2
docker rm http
docker rmi homqyy/example_http

构建镜像

前文讲述了如何运行容器,也提到了容器时由镜像产生的,接着就开始着手学习如何构建镜像。

在《Docker系列(1):Welcome to Docker》文章中我提到过:

Docker镜像

镜像是容器要运行的资源集合。容器如同集装箱一样,而镜像则是集装箱里面装着的物资,因为集装箱之间是相互隔离的,因此相同内容的物资可以放在多个集装箱中,即通过一个镜像可以产生N个容器,且不必担心他们之间产生冲突。

因此要想运行自己的容器,必须知道如何构建镜像,而构建镜像的主体是“Dockerfile”文件。

Dockerfile

Dockerfile具有信息表达性,且易于理解,通过版本控制系统来跟踪Dockerfile文件的改动就等效于维护多个镜像版本。Docker利用缓存技术来实现构建过程的重用。我们在文件中声明镜像的构建方法,再通过docker build指令构建镜像,文件的语法可参阅“官方Dockerfile手册”,新手也先阅读“官方指南”。

在开始前我们先了解一下以下两个阶段,这对于编写Dockerfile是有助益的,因为指令仅在对应的阶段中生效:

  • 构建时:该阶段指的是在构建镜像期间,后文提及指令时,没有额外声明的指令都是在“构建时”生效。
  • 运行时:该阶段指的是在运行容器期间,后文提及指令时,对于此阶段才能生效的指令会额外声明。

项目:homqyy/docker-example-http

接着我们先看一下之前“homqyy/docker-example-http”镜像的Dockfile文件

FROM nginx:stable

COPY ./nginx.conf /etc/nginx/nginx.conf

LABEL cn.homqyy.docker.author="homqyy"
LABEL cn.homqyy.docker.email="yilupiaoxuewhq@163.com"
LABEL cn.homqyy.docker.title="example http"

EXPOSE 8080

有了这个文件后就可以开始构建镜像了:docker build -t homqyy/example_http:1.0 .

回顾一下Dockerfile文件内容,以下是对其做解释:

  • FROM
    • 创建镜像是可以从一个基础镜像开始的,比如我们可以基于centos:7镜像创建自己的程序镜像,或则基于python:3.7镜像去创建自己的python程序镜像。本镜像是基于nginx:stable创建的。
    • 语法格式为:FROM [--platform=<platform>] <image>[:tag] [AS <name>]
      • image:镜像全名,比如:homqyy/example_http
      • tag:标签,比如:1.0
      • AS <name>:设置阶段别名。在后文描述此参数。
  • COPY:拷贝主机的nginx.conf文件到容器的/etc/nginx/nginx.conf中。语法是:COPY host_path container_path
  • LABEL:创建镜像的标签,该标签可标识镜像/容器,并被用于搜索镜像/容器。语法是LABEL Key=Value
    • 我们可以通过命令docker images -f label=cn.homqyy.docker.author=homqyy来搜索上述镜像。
  • EXPOSE:暴露端口,这里就是我前文提到PORTS时说的,运行容器后会看到一些没有被映射的端口,而这些端口就是通过此命令来说明的。(记住,该指令不会实质的启动对应的端口服务,仅用来通告使用者容器中可能会使用的端口有哪些)

回顾一下构建命令docker build -t homqyy/example_http:1.0 .,以下是对其做解释:

  • -t:Tag的意思,用来设置镜像的名称,其格式为:user/image_name:version
  • .:构建镜像时的上下文,这里可以理解为“工作目录”。
    • 这里可以回顾下上面的Dockerfile文件内容“COPY ./nginx.conf /etc/nginx/nginx.conf”,可以看到,这里的“./nginx.conf”是一个相对路径,而它相对的就是“工作目录”,也就是说,如果工作目录是/home/example的话,“./nginx.conf”就等效于/home/example/nginx.conf
    • 注意:使用COPY的时候,是不可以退出“工作目录”的,也就是我们不能写“../../nginx.conf”,它必须总是在“工作目录”或则其子目录中。

项目:homqyy/hcore

接着再来看另一个项目“homqyy/hcore”,其用到了一些新命令,它的Dockerfile文件如下:

FROM homqyy/dev_env_centos8

ARG USER=1000
ARG GROUP=1000

USER root

# install 'ripgrep' for 'todo-tree' extension of vscode
RUN curl https://copr.fedorainfracloud.org/coprs/carlwgeorge/ripgrep/repo/epel-7/carlwgeorge-ripgrep-epel-7.repo \
        > /etc/yum.repos.d/carlwgeorge-ripgrep-epel-7.repo \
    && yum -y install ripgrep

RUN groupadd -g $GROUP hcore \
    && useradd -g $GROUP -u $USER hcore

USER $USER:$GROUP

LABEL cn.homqyy.docker.author="homqyy"
LABEL cn.homqyy.docker.email="yilupiaoxuewhq@163.com"
LABEL cn.homqyy.docker.title="hcore"
  • ARG:“构建时”有效,格式为“ARG <name>[=<default value]”。
  • USER:“运行时”有效,设置运行时的用户(/组)身份,值可以是名称也可以是ID,其格式分别为:USER <user>[:<group>]和USER <UID[:GID]>。我们这里先以root身份对容器进行必要的初始化后,再以指定身份$USER:$GROUP作为进入容器的默认身份。
  • RUN:“运行时”有效,用于在容器中执行命令,以按要求初始化容器。我们这里主要安装一些必要的软件并设置用户hcore和组hcore的ID。

项目:homqyy/hengine

接着我们看项目“homqyy/hengine”来继续加深学习,其Dockerfile文件如下:

FROM homqyy/dev_env_centos8 AS compile

RUN yum install -y pcre-devel openssl-devel

WORKDIR /usr/src/hengine

COPY . .

RUN bash ./configure --prefix=/usr/local/hengine --with-http_ssl_module --with-http_sub_module --with-stream_ssl_module --with-stream \
    && make -j && make install

LABEL cn.homqyy.docker.title="hengine"
LABEL cn.homqyy.docker.author="homqyy"
LABEL cn.homqyy.docker.email="yilupiaoxuewhq@163.com"

FROM centos:8

# update yum repos
RUN rm -f /etc/yum.repos.d/* \
        && cd /etc/yum.repos.d/ \
        && curl http://mirrors.aliyun.com/repo/Centos-8.repo > CentOS-Linux-BaseOS.repo \
        && sed -i 's/\$releasever/8-stream/g' CentOS-Linux-BaseOS.repo \
        && cd - \
        && yum clean all \
        && yum makecache

COPY --from=compile /usr/local/hengine/ /usr/local/hengine/

WORKDIR /usr/local/hengine

CMD [ "/usr/local/hengine/sbin/nginx", "-g", "daemon off;" ]

与前文样例比较,这里多了几个命令WORKDIR、CMD,一些命令的用法也不同,比如FROM用了两次,COPY使用参数--from,这里对它们分别做解释:

  • WORKDIR:“运行时”有效,设置容器的默认工作目录,在此命令之后,那些“运行时”有效且携带路径参数的命令,如果使用了相对路径,则会以此目录作为前缀。同时用户进入容器后也会直接进入此目录中。该命令还有一个比较特别的地方就是“如果容器中没有此目录,它会自动创建”。
  • CMD:容器启动后自动运行的命令,简言之就是开机运行的命令(注意,此命令只有最后一个有效,即如果列出了多个CMD,则只有最后一个能生效)。我们这里开机肯定运行的就是nginx了:/usr/local/hengine/sbin/nginx -g "daemon off;"。此指令支持3种书写格式,分别为:
    • CMD ["executable", "param1", "param2"]:这个被称为“执行格式(exec form)”,这里用的就是这个格式。
    • CMD ["param1","param2"]:默认格式,作为ENTRYPOINT指令的参数。
    • CMD command param1 param2:这个被称为“Shell格式(shell form)”,顾名思义就是执行shell命令,其等效于/bin/sh -c command param1 param2
  • ENTRYPOINT:这里虽然没有用到此指令,但是CMD指令与它有关。该命令是用来设置容器的进入点的,即容器运行后运行的命令。当配置了ENTRYPOINT后,CMD只能以“作为其参数”的形式存在。此指令有2种书写格式:
    • ENTRYPOINT ["executable", "param1", "param2"]
    • ENTRYPOINT command param1 param2
  • FROM:前文说过“FROM是声明基础镜像的一条指令”,可以这么说,每一个FROM指令都是一个构建阶段,这里有两个FROM即有两个阶段,分别为“编译程序阶段”和“构建容器阶段”。首先用基础镜像homqyy/dev_env_centos8启动后的容器来编译hengine程序,接着再以centos:8这个镜像作为hengine的基础镜像(说明homqyy/hengine这个镜像是运行在centos8的),构建出hengine镜像。
    • 除了最后一个阶段(FROM)外,其余的阶段产生的容器都属于中间容器。
    • docker build构建镜像时,追加参数--rm可以自动删除中间镜像。
    • 我们发现在FROM之后还有个AS参数,这个参数是给本阶段起一个别名,可以看到这里给第一个阶段起名为compile
  • COPY --from:前文了解到,COPY是用来拷贝文件的,但拷贝源默认是本机,而--from参数用来声明拷贝源是谁,其可以是该指令之前的任意阶段名,比如这里声明的拷贝源是compile,意思是从compile中的/usr/local/hengine/目录,拷贝到当前容器下的/usr/local/hengine/

因此,此容器构建做的事情就是“先编译hengine的源码并安装后,再将程序拷贝到centos8的环境中”。


配置容器

有时候我们会有一些动态需求,比如启动一个HTTP服务器时,有的人想监听80端口,有的人想监听10080端口;或者有人想持久化HTTP服务器的日志;或者有人想修改HTTP服务器的启动配置等。但是仅从上述学习到的“构建镜像”的方法,我们想要实现这类需求,仅能重新构建一个镜像。那么有没有什么方法可以让我们在不重新构建镜像的前提下,可以动态的配置容器呢?就像使用接口或方法一样,通过传递不同的参数来完成容器配置。答案是有的,用到的内容主要有:变量(variable)、参数(ARG)、环境变量(ENV)和卷(volume)。

  • 变量提供了动态的可能,变量在程序里是动态的基本前提;
  • 参数提供了构建容器时,作者与构建程序的交互方式;
  • 环境变量提供了运行容器时,使用者与容器的交互方式;
  • 卷提供了运行容器时,使用者与容器的交互方式以及对配置或数据持久化的方式。

看项目"homqyy/hcore"就是通过参数来完成构建不同用户/组ID的镜像:

...

ARG USER=1000 # 指定参数变量USER,默认值为1000
ARG GROUP=1000 # 指定参数变量GROUP,默认值为1000

...
# 创建用户hcore和组hcore,并设置它们的ID分别为$USER和$GROUP
RUN groupadd -g $GROUP hcore \
    && useradd -g $GROUP -u $USER hcore
USER $USER:$GROUP # 设置身份为$USER:$GROUP


...

其使用可以参考其项目中的jenkins脚本文件“[Jenkinsfile][]”:

pipeline {
    agent { label 'Master' }

    stages {
        stage('init') {
            steps {
                git 'https://gitee.com/homqyy/hcore.git'

                sh 'git submodule init'
                sh 'git submodule update'
            }
        }
        stage('Build') {
            steps {
                git 'https://gitee.com/homqyy/hcore.git'

                sh 'bash -x ./build_on_docker.sh build `id -u` `id -g`'
            }
        }
        stage('Test') {
            steps {
                echo 'Testing..'
            }
        }
        stage('Deploy') {
            steps {
                echo 'Deploying....'
            }
        }
    }
}

这里面有条命令bash -x ./build_on_docker.sh build `id -u` `id -g`就是在向builder传递参数的值。因为第二个和第三个参数就是用户ID和组ID,在脚本build_on_docker.sh中如下使用:

...
function init
{
    if [ -n "$user" ]; then
        BUILD_OPTIONS="--build-arg USER=$user $BUILD_OPTIONS"
    fi

    if [ -n "$group" ]; then
        BUILD_OPTIONS="--build-arg GROUP=$group $BUILD_OPTIONS"
    fi

    if [ "$action" == "build" ]; then
        BUILD_TOOL="$WORK_DIR/build.sh"
    elif [ "$action" == "upload" ]; then
        BUILD_TOOL="$WORK_DIR/upload.sh"
    else
        usage "invalid action"
    fi
}
...
  • BUILD_OPTIONS="--build-arg USER=$user $BUILD_OPTIONS",$user就是脚本的第2个参数。
  • BUILD_OPTIONS="--build-arg GROUP=$group $BUILD_OPTIONS",$group就是脚本的第3个参数。

变量

Dockerfile支持变量,使用方式跟编码是一样的,用${variable_name}符表示使用的是变量variable_name的值,而声明变量的方法有两种:

  • 参数变量:用ARG指令声明的变量
  • 环境变量:用ENV指令声明的变量

变量使用上,除了直接使用外,也支持标准bash下的修饰符进行更复杂的表达:

  • ${variable:-word}:如果variable没有设置值的话,则用word代替,否则使用variable的值。
    • 注意:word是任意字符串
  • ${variable:+word}:如果variable已经设置值的话,则用word代替,否则设置为“空串”。
    • 注意:word是任意字符串

如果想单纯的使用带$符号的字面常量,则需要对$符号进行转义,比如:\${foo}

# 示例
FROM homqyy/dev_env_centos8

ARG USER=1000                       # 声明变量 USER,但仅在构建时有效
ARG GROUP=1000                      # 声明变量 GROUP,但仅在构建时有效

ENV DEV_UID=${USER} DEV_GID=${GID}  # 声明变量 DEV_UID和DEV_GID,在构建时和运行时都有效

WORKDIR /workspace/${USER}-${GROUP} # 设置工作目录:/workspace/1000-1000

COPY . /workspace/${DEV_UID}-${DEV_GID}     # COPY . /workspace/1000-1000
COPY . /workspace/\${DEV_UID}-\${DEV_GID}   # COPY . /workspace/${DEV_UID}-${DEV_GID}

参数

ARG指令定义一个变量且该变量仅在构建时有效,用户用命令docker build的参数--build-arg <varname>=<value>将变量具体的值传递给构建器(builder)。如果用户提供了参数但是该参数没有在Dockerfile中定义,那么在构建时会输出一个警告信息,例如:[Warning] One or more build-args [foo] were not consumed.

Dockerfile中定义参数的格式为:ARG <name>[=<default value>]

  • 在文件中可以提供多个参数;
  • 参数默认值为可选项。

参数仅在声明该参数的构建阶段中有效,比如:

FROM busybox
ARG SETTINGS # 声明一个变量 $SETTING
RUN ./run/setup $SETTINGS

# 在当前阶段末尾和进入下面的构建阶段前,$SETTING的声明周期结束

FROM busybox
ARG SETTINGS # 重新声明一个变量 $SETTING,这是必须的,因为上一个阶段的 $SETTING 已经释放了。
RUN ./run/other $SETTINGS

环境变量

ENV指令设置环境变量且该变量在构建时和运行时都有效(它将以系统环境变量的形式存在于容器中)。其格式为:ENV <key>=<value> ...。该指令支持内插变量,比如:ENV MY_NAME="${USER_NAME}_10"。除了在Dockerfile中定义外,可以通过docker run命令的参数--env <key>=<value>定义。

以下指令支持使用环境变量:

  • ADD
  • COPYS
  • ENV
  • EXPOSE
  • FROM
  • LABEL
  • STOPSIGNAL
  • USER
  • VOLUME
  • WORKDIR
  • ONBUILD

关于环境变量的应用,可以参考开源项目“jenkins”的Dockerfile文件

卷(volume)

volume是一个数据分割和共享的工具,它有一个与容器无关的范围或生命周期。这使得volume成为了容器化系统设计中关于文件分享或写入最重要的一部分。

存储卷有两种类型,一种是挂载卷,它使用的是主机提供的目录或文件,并将其挂载到容器中使用;另一种是内部卷,它使用的是由Docker守护进程控制的位置,被称为Docker管理空间,该方式有明显的性能优势。

Docker卷
Docker卷图

挂在卷

挂载卷提供了主机和容器两者间相互交互的一个手段,在开发中应用是便利的,我们可以在宿主机上进行开发然后在容器中使用,当然,这同时也将持久化在主机上。挂载方法是在docker run命令中使用-v <host_path_as_src>:<container_path_as_dst>[:ro|rw]选项,将主机的某个路径下的文件或目录挂载到容器下,并可以设置在容器中的权限,是为只读(ro)还是读写(rw)。

如果想要挂载的是文件的话,需要注意,文件必须在创建容器之前就存在于主机上,否则Docker会认为你想用一个目录,于是在主机上创建它并将其挂载到容器中指定的位置。

挂载卷也有问题存在,那就是创造了与其他容器发生冲突的机会。比如启动多个容器,并同时挂载了相同的主机位置。

这里以"homqyy/hengine"镜像为例,我们用以下命令实现“用自己的配置文件覆盖hengine的默认配置文件”,并实现在主机上持久化:

docker run -d -v /path/to/nginx.conf:/usr/local/hengine/conf/nginx.conf:ro homqyy/hengine
  • 这样的好处应该是显而易见的,意味着我们只需要备份好nginx.conf配置文件即可,可以在任何地点任何时间通过该配置文件恢复hengine程序。
  • 我们也可以很方便的在主机上直接修改nginx.conf文件,而不必进入到容器中修改,也不怕容器退出后配置丢失。

再比如持久化jenkins的数据到主机上:

docker run -p 8080:8080 -p 50000:50000 -v /your/home:/var/jenkins_home jenkins
  • 这时候我们可以在主机上操作/your/home目录下的文件,容器也将同步生效。
  • 当然,jenkins的数据也被持久化到了主机的/your/home目录。

内部卷

这是由dockerd在主机文件系统中创建的存储卷,并由其管理。该卷是容器与文件系统位置解耦的方法。而且如果只是为了持久化的话,采用挂载的方式会有明显的性能问题,而内部卷则兼顾了持久化和性能。其创建方法是在命令docker run命令中使用-v [<volume_name:]/path/to/container_path[:ro|rw]选项,创建一个名为volume_name的内部卷,并将其挂载到容器的/path/to/container_path路径下,可以选择性的设置container_path在容器中的的读写权限,是为只读(ro)还是读写(rw)。

比如运行jenkins容器时:

docker run --name myjenkins -p 8080:8080 -p 50000:50000 -v jenkins_home_volume:/var/jenkins_home jenkins
  • 创建一个内部卷jenkins_home_volume,并挂载到/var/jenkins_home

在使用容器一段时间后,可以退出并删除容器,只要保证卷jenkins_home_volume没有删除即可,可以随时用相同的命令重建容器,并恢复其数据。

docker有一个独立的子命令用于管理内部卷:docker volume <create|inspect|ls|prune|rm>

  • create:创建一个内部卷
  • inspect:查看内部卷的元信息
  • ls:列出当前已经创建的内部卷
  • prune:移除所有未被容器使用的内部卷
  • rm:删除一个或多个指定的内部卷

编排主机服务

编排主机服务用的是Docker的组件compose,详情可参阅“官方docker compose手册”。

当服务较多时,利用compose可以很容易的“构建镜像”、“配置容器”、“启停容器”和“更新容器”。使用方法是编写一个Compose文件,其语言是YAML

先上样例,我们在工作目录中编写文件docker-compose.yml

services:
    blog:
        image: wordpress
        restart: always
        environment:
            WORDPRESS_DB_HOST: db
            WORDPRESS_DB_USER: exampleuser
            WORDPRESS_DB_PASSWORD: examplepass
            WORDPRESS_DB_NAME: exampledb
        ports:
            - 8080:80
        networks:
            - blog
        volumes:
            - blog_www:/var/www/html
    db:
        image: mysql:5.7
        restart: always
        environment:
            MYSQL_DATABASE: exampledb
            MYSQL_USER: exampleuser
            MYSQL_PASSWORD: examplepass
            MYSQL_RANDOM_ROOT_PASSWORD: '1'
        networks:
            - blog
        volumes:
            - blog_db:/var/lib/mysql
volumes:
    blog_www:
    blog_db:


networks:
    blog:

在上文样例中,计划创建2个服务,分别是博客blog和数据库dbblog负责提供博客站点,db负责存储blog的数据。

在文件docker-compose.yml所在目录执行命令docker-compose up -d来启动上述所有服务:

docker-compose up -d
启动compose示例图

看到如上信息时,证明服务启动成功了(docker-blog-example是我的目录名称),通过命令docker-compose ps查看一下启动情况:

docker-compoes-ps
查看compose里的服务运行情况示例图

从上面可以看到,blog做了端口映射但db并没没有做映射,blog8080映射到80,而db则是在自己的容器中3306,这是因为db不需要对用户开放,只需要保证blog可以访问即可,这样更有安全性,而blog可以通过访问服务名称db来访问到数据库。

接着我们通过浏览器访问博客:http://localhost:8080

回顾一下配置文件:

services:
    blog:
        ...
    db:
        ...
volumes:
    blog_www:
    blog_db:


networks:
    blog:
  • services:必要字段,用于配置服务,后续二级及以后的字段都是跟服务相关的配置
    • blog: 服务名称,这里是博客服务
    • db:服务名称,这里是数据库服务
  • volumes:可选字段,用于配置卷。格式分为短语法(short syntax)和长语法(long syntax):

    短语法跟命令docker run的可选项-v是一样的: volume_name|host_path:container_path[:ro|rw](权限这里只列出常用的,详细的还请看官网)

    • blog_www:卷名称,创建(不存在的话)或使用内部卷blog_www
    • blog_db:卷名称,创建(不存在)或使用内部卷blog_db
    volumes:
        type: <volume|bind|tmpfs|npipe> # 卷类型
        source: <volume_name|host_path> # 挂载源;当type为bind时,这里指的是主机路径。当type为volume时,之类值的是(内部)卷名称
        target:<container_path>        # 挂载目的;这里指的是在容器中的路径。
        read_only:<true>               # 标识这个卷为只读
        external: <true|false>          # 表明此卷是否为外部创建的,如果为true,则不主动创建
    
  • networks:可选字段,用于配置网络

    • blog:网络名称,创建一个虚拟网络blog

db

wordpress需要持久化数据到mysql数据中,因此我们需要启动一个数据库服务:

...
    db:
        image: mysql:5.7
        restart: always
        environment:
            MYSQL_DATABASE: exampledb
            MYSQL_USER: exampleuser
            MYSQL_PASSWORD: examplepass
            MYSQL_RANDOM_ROOT_PASSWORD: '1'
        networks:
            - blog
        volumes:
            - blog_db:/var/lib/mysql
...
  • image:等效于Dockerfile中的FROM指令,指定基础镜像
  • restart:容器的重启策略;格式为“restart <"no"|always|on-failure|unless-stopped>”:
    • no:默认策略;无论什么情况都不进行重启
    • always:除非容器被删除,否则总是自动重启容器
    • on-failure:如果容器异常退出则重启
    • unless-stopped:除非容器被删除或者停止,否则自动重启容器,无论它的退出是否正常。
  • environment:定义在容器中的环境变量,即此处在容器中将通过环境变量来配置mysql数据库的初始数据库和用户。表达方式可以是“队列(array)”和“映射(map)”

    environment:
        - MYSQL_DATABASE: exampledb
        - MYSQL_USER: exampleuser
        - MYSQL_PASSWORD: examplepass
        - MYSQL_RANDOM_ROOT_PASSWORD: '1'
    
    environment:
        MYSQL_DATABASE: exampledb
        MYSQL_USER: exampleuser
        MYSQL_PASSWORD: examplepass
        MYSQL_RANDOM_ROOT_PASSWORD: '1'
    
  • networks:配置在特定服务下时,指的是将服务加入到某个网络中,这里指的是将db服务加入到名为blog的网络中,注意,这里的blog应该是在顶级的networks中定义过的。这里使用起来跟VMWare中的VMNet有点类似。

  • volumes:定义卷,格式可以是“内部卷”或“挂载卷”,且可配置多项。比如这里指的是,将名为blog_db的内部卷挂载到容器中的/var/lib/mysql中。要配置多项的话只需要按照数组(array)方式继续罗列即可。注意,这里的blog_db应该是在顶级的volumes中定义过的。

blog

此服务为博客服务,由wordpress提供。

...
    blog:
        image: wordpress
        restart: always
        environment:
            WORDPRESS_DB_HOST: db
            WORDPRESS_DB_USER: exampleuser
            WORDPRESS_DB_PASSWORD: examplepass
            WORDPRESS_DB_NAME: exampledb
        ports:
            - 8080:80
        networks:
            - blog
        volumes:
            - blog_www:/var/www/html
...
  • 这里比较重要的就是在enrironment中的WORDPRESS_DB_HOST: db这个配置,该环境变量是用来指定数据库的主机IP或域名的,我们这里填充为db指的肯定是域名,而该域名名称就恰好为db服务的服务名称。在Docker中,会给每个服务一个默认的域名,其值为服务名。其次我们将blog服务和db服务都置于网络blog中,因此他们彼此之间可以通信。反之,我们也可以在db服务中执行命令ping blog,可以发现是可以通的。

  • ports:跟命令docker run的可选配置-p是一样的,用来定义服务中的端口映射关系,其格式为数组(array)。

其他常用配置

links:在特定服务之下,定义一个链接服务的网络;可以指定服务名和别名,或者只指定服务名,格式为service:alias

services:
  web:
    links:
      - db
      - db:database
  • db:仅指定服务名,意思是定义一个将web服务和db服务连接到一起的网络,这样彼此都可以相互通信。
  • db:database:指定服务名和别名,意思是定义一个将web服务和db服务连接到一起的网络,并且设置db服务的别名(域名)为database,意味着在web服务中,可以执行连通测试命令ping database

env_file

env_file:在特定服务之下,设置文件中的内容到容器中作为环境变量;格式为数组或者字典

services:
  web:
    env_file:
      - ./a.env
      - ./b.env
  • 文件按行设置变量,每行的格式为:var[=value]

container_name

container_name:设置容器名称

cap_add

cap_add:添加容器的权限

cap_drop

[cap_drop][docker-compose-file.drop]:删除容器的权限

user

user:设置容器运行时的身份,等效于Dockerfile的USER。该配置会覆盖Dockerfile的配置

build

build:指定构建容器镜像的配置,相当于Dockerfile文件中与build相关的动作在此配置中指定。

services:
  web:
    build:
      context: <ctx_path>           # 设置构建时的上下文
      dockerfile: <Dockerfile_path> # 声明Dockerfile文件的路径
      args:                        # 定义参数,格式可以是数组或字典
        ARG1: v1                    # 定义参数ARG1,且值为v1
        ARG2: v2                    # 定义参数ARG2,且值为v2
      labels:                       # 定义容器的标签,格式可以是数组或字典
        com.example.description: "Test App"
        com.example.department: "Person"
        com.example.label-with-empty-value: ""

分享容器

当我们完成镜像的构建和容器的运行之后,肯定会有分享和存储的需求,这时候就涉及到容器镜像的发布和分发了。

镜像的发布和分发能力由Registry服务提供,Registry服务分为私有、公有和公私有混合三种。目前docker官方提供的就是一个典型的公私有混合,即用户可以选择自己的镜像是仅自己可见还是对外发布。

私有的Registry服务可以使用官方镜像registry自行搭建,详情可参阅官方手册。

因此我们最常见的查找他人发布的镜像的方法就是登录站点,然后搜索自己想了解的镜像。那么我们该如何发布自己的镜像到该站点上呢?由于docker默认关联的Registry服务地址就是https://index.docker.io/v1/,该Registry存储的镜像就是站点呈现出来的内容。

那么如何将自己的镜像发布到,步骤如下:

  1. 构建镜像,并且该镜像的作者名应当为你在站点上的用户名,比如构建一个镜像“homqyy/hengine”前面的“homqyy”就是我在上的用户名,“hengine”则是镜像名称。
  2. 登录Registry:docker login
  3. 发布本地镜像:docker push homqyy/hengine

学会了发布后,那么接着就是分发了,由于推送上去的镜像默认是私有的,因此要想其他人可见,则需要将其转为共有,这时只需要登录站点,对镜像进行设置即可:

设置镜像
设置镜像示例图

最后就可以在任何安装了Docker的主机中通过命令docker pull homqyy/hengine将镜像下载到本地了。

当然也可以直接一个命令完成下载和运行的操作:docker run -d homqyy/hengine

但是有时候我们可能因为在公司等原因无法上网,这时候只想进行本地间的发布和分发该怎么办?这里解决方案有很多,进行简要讲解,详细在后续篇章会详述:

  • 在本地搭建私有的Registry服务;
  • 将镜像或容器从一台主机中导出,在目标主机中导入;
    • 导出镜像和导入:docker savedocker load
    • 导出容器和导入:docker exportdocker import
  • 将镜像或容器从一台主机中导出,放置到本地资源服务器上,比如ftpd服务。

评论