在docker使用过程中,其实大部分时间都是花在了打镜像上,因为容器本身底层不可写,顶层可读写缺无法持久化性质,我们如果对容器进行了修改,想要进行横向扩容,快速部署时,一般需要重新制作镜像,在分发到其他主机或终端。(虽然也可以将数据储存在NFS和宿主机本地,而不是容器内部来方便的修改配置文件及保存数据等。)
  docker中镜像的制作方式一般手工修改后导出和通过Dockerfile生成两种方式。

手动制作镜像

  因为镜像本身的不可修改性,有时候官方镜像中使用的工具的版本可能不是那么符合我们的生产环境,我们就需要自己制作镜像了。一般来说,我们都是基于官方镜像,作出修改来符合自身实际场景中使用,然后在导出保存为我们自己的镜像。
  以一个tomcat容器为例,我们如果需要tomcat8的容器,可以直接从官网拉取tomcat8的镜像docker pull tomcat:8.5.49-jdk8-openjdk修改完成后,还可以使用命令docker commit将已有容器制作为镜像.

1
2
3
4
5
6
7
8
9
10
11
root@DockerUbuntu:/opt/dockerfile/web/tomcat/tomcat-apps/app1# docker commit --help

Usage: docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]

Create a new image from a container's changes

Options:
-a, --author string Author (e.g., "John Hannibal Smith <hannibal@a-team.com>")
-c, --change list Apply Dockerfile instruction to the created image
-m, --message string Commit message
-p, --pause Pause container during commit (default true)

  -a添加镜像制作人信息,-m添加备注信息,-p选项是默认选项,在制作为镜像时暂停容器,-c使用Dockerfile指令来创建镜像,Dockerfile之后我们会详细讲解。例如

1
docker commit -a "example@163.com" -m "tomcat app1 v1" --change="EXPOSE 8080 8009" f5f8c13d0f9f centos-tomcat-app1:v1

Dockerfile

  不过如果业务场景要求的配置场景要修改nginx的编译参数或者要求底层是centos7系统这就没法更改了(官方镜像一般都是debian系统),我们只能修改最上层的镜像。此时我们可以通过分层构建的方式来制作镜像(,毕竟docker镜像本来就是分层构建的)。
  DockerfileDockerFile 可以说是一种可以被 Docker 程序解释的脚本, DockerFile 是由一条条的命令组成的,每条命令对应 linux 下面的一条命令, Docker 程序将这些 DockerFile 指令再翻译成真正的 linux 命令,其有自己的书写方式和支持的命令, Docker 程序读取 DockerFile 并根据指令生成 Docker 镜像,相比手动制作镜像的方式, DockerFile 更能直观的展示镜像是怎么产生的,有了写好的各种各样 DockerFile 文件,当后期某个镜像有额外的需求时,只要在之前的DockerFile 添加或者修改相应的操作即可重新生成新的 Docke 镜像,避免了重复手动制作镜像的麻烦。
  Docker中常用到的命令令有FROM(指定基础镜像名称),MAINTAINER(镜像作者署名及联系方式),USER(切换用户身份,初始一般为root),WORKDIR(指定或切换工作目录),ADD(将当前宿主机目录的文件拷贝至容器指定位置,tar包可以自动解压),RUN(运行命令,其实就是shell命令,可执行多条,用&&符号连接),ENV(设置环境变量),CMD(设置默认镜像启动命令,要可以占据前台,否则基于此镜像启动的容器会直接停止),之后我们结合实际例子一一说明。

  例如我们使用Dockerfile来分层构建定制的tomcat镜像来运行app1服务(当然,也可以一步到位),步骤如下:

  1. 构建目录架构
    我们通常将Dockerfile文件都放置在/opt/目录下

    1
    mkdir /opt/dockerfile/{web/{nginx,tomcat,jdk},system/{centos,ubuntu,redhat}} -pv
  2. 构建系统镜像

    1
    2
    3
    4
    cd /opt/dockerfile/system/centos
    mkdir 7.6
    cd 7.6
    docker pull centos:7.6.1810

      创建Dockerfile文件,注意D要大写

    1
    2
    3
    4
    5
    6
    7
    vim Dockerfile
    #CentOS 7.6 镜像
    FROM centos:7.6.1810
    MAINTAINER Mice example@163.com
    RUN rpm -ivh http://mirrors.aliyun.com/epel/epel-release-latest-7.noarch.rpm
    RUN yum install -y vim wget tree lrzsz gcc gcc-c++ automake pcre pcre-devel zlib zlib-devel openssl openssl-devel iproute net-tools iotop
    CMD ["bash"]

      创建制作镜像脚本,来生成镜像。当然,也可以直接使用docker build命令配合-t参数(指定标签名)直接将当前目录的Dockerfile制作为镜像,使用脚本是为了日后修改不至于每次名称都不一样,可以保证每次打的镜像的名称和版本号统一,否则以后镜像多了会乱(脚本中也可以在标签中加上时间)。

    1
    2
    3
    vim build_centos.sh
    #!/bin/bash
    docker build -t centos-base:v7.6.1810 .

      当前目录结构为

    1
    2
    3
    4
    5
    6
    root@DockerUbuntu:/opt/dockerfile/system/centos/7.6# tree
    .
    ├── build_centos.sh
    └── Dockerfile

    0 directories, 2 files

      执行命令bash build_centos.sh来创建第一层镜像centos-base:v7.6.1810

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    root@DockerUbuntu:/opt/dockerfile/system/centos/7.6# bash build_centos.sh 
    Sending build context to Docker daemon 3.072kB
    Step 1/5 : FROM centos:7.6.1810
    ---> f1cb7c7d58b7
    Step 2/5 : MAINTAINER Mice example@163.com
    ---> Using cache
    ---> 3899d2446806
    Step 3/5 : RUN rpm -ivh http://mirrors.aliyun.com/epel/epel-release-latest-7.noarch.rpm
    ---> Using cache
    ---> 5a72857ed63d
    Step 4/5 : RUN yum install -y vim wget tree lrzsz gcc gcc-c++ automake pcre pcre-devel zlib zlib-devel openssl openssl-devel iproute net-tools iotop
    ---> Using cache
    ---> 705fed38cb94
    Step 5/5 : CMD ["bash"]
    ---> Running in aea451be0461
    Removing intermediate container aea451be0461
    ---> 160b9544f121
    Successfully built 160b9544f121
    Successfully tagged centos-base:v7.6.1810

      有时到yum那步会提示报错,无法解析IP。多执行几次脚本,多试几次就可以了。他会有缓存自动保存镜像,已经写好的层数会自动缓存的,如上面的---> Using cache

  3. 构建适合版本的jdk镜像

    1
    2
    3
    cd /opt/dockerfile/web/jdk/
    mkdir 8u212
    cd 8u212

      将准备好的jdk压缩包jdk-8u212-linux-x64.tar.gz放入此目录,然后还是编写Dockerfile以及build_jdk.sh脚本

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    vim Dockerfile
    #JDK 8u212
    FROM centos-base:v7.6.1810
    MAINTAINER Mice example@163.com
    ADD jdk-8u212-linux-x64.tar.gz /usr/local/src/
    ADD env.sh /etc/profile.d/
    RUN ln -sv /usr/local/src/jdk1.8.0_212 /usr/local/jdk && groupadd www -g 2019 && useradd www -u 2019 -g www

    ENV JAVA_HOME /usr/java/default
    ENV PATH $JAVA_HOME/bin:$PATH
    ENV JRE_HOME $JAVA_HOME/jre
    ENV CLASSPATH $JAVA_HOME/lib/:$JRE_HOME/lib/

    RUN rm -rf /etc/localtime && ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo "Asia/Shanghai" > /etc/timezone
    1
    2
    3
    vim build_jdk.sh
    #!/bin/bash
    docker build -t jdk-base:v1.8.0_212 .

      需要注意的是,因为ENV环境变量是当前用户(当前终端)有效,也就是说对于容器本身来说,他运行的环境变量是已经通过ENV可以设置好,可是如果发生故障,我们需要连接进入容器时,这个ENV就对我们当前终端无效了,我们可能就无法使用那些ENV设置了的PATH变量了,所以我们需要添加一个环境变量配置文件/etc/profile.d目录(也可直接替换profile文件),以便这些环境变量在我们连接至容器后也可以生效。

    1
    2
    3
    4
    5
    vim env.sh
    JAVA_HOME=/usr/local/jdk
    JRE_HOME=$JAVA_HOME/jre
    CLASSPATH=$JAVA_HOME/lib/:$JRE_HOME/lib/
    PATH=$PATH:$JAVA_HOME/bin

      目录结构如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    root@DockerUbuntu:/opt/dockerfile/web/jdk/8u212# tree
    .
    ├── build_jdk.sh
    ├── Dockerfile
    ├── env.sh
    └── jdk-8u212-linux-x64.tar.gz

    0 directories, 4 files

      还是通过build脚本,构建jdk容器。

  4. 构建适合版本的tomcat镜像
    先创建版本目录(,为日后可能需要不同版本的tomcat弄好框架)。

    1
    2
    3
    cd /opt/dockerfile/web/tomcat/
    mkdir 8.5.47
    cd 8

      将准备好的tomcat源码包拷到这个目录,或者wget下载

    1
    wget http://mirrors.tuna.tsinghua.edu.cn/apache/tomcat/tomcat-8/v8.5.47/bin/apache-tomcat-8.5.47.tar.gz

      制作Dockerfile

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    vim Dockerfile
    #tomcat 8-jdk 1.8.0_212-centos 7.6
    FROM jdk-base:v1.8.0_212
    MAINTAINER Mice example@163.com

    ENV TZ "Asia/Shanghai"
    ENV LANG en_US.UTF-8
    ENV TERM xterm
    ENV TOMCAT_MAJOR_VERSION 8
    ENV TOMCAT_MINOR_VERSION 8.5.47
    ENV CATALINA_HOME /apps/tomcat
    ENV APP_DIR ${CATALINA_HOME}/webapps

    RUN mkdir /apps
    ADD apache-tomcat-8.5.47.tar.gz /apps/
    RUN ln -sv /apps/apache-tomcat-8.5.47 /apps/tomcat

      老规矩,创建build脚本

    1
    2
    3
    vim build_tomcat.sh
    #!/bin/bash
    docker build -t tomcat-base:v8.5.47 .

      然后执行脚本打镜像~

  5. 放置项目app1至指定目录构建镜像
      到现在,tomcat容器的环境都配置好了,不过里面还没有跑服务,所以可以针对不同的业务,创建不同的镜像,而他们底层都是共用的一个基础镜像tomcat-base:v8.5.47,所有底层镜像因为都是只读的,所以可以复用而互不干扰,也不会重复占用多余的空间。
    还是先创建业务APP目录。

    1
    2
    mkdir -pv /opt/dockerfile/web/tomcat/myapps/app1/
    cd /opt/dockerfile/web/tomcat/myapps/app1/

      因为当app1有变化时,tomcat服务可能会需要重启才会生效变化,而如果tomcat服务是容器的启动进程(即PID=1的进程)时,重启tomcat会导致容器终止,容器里面在运行的数据及session都会丢失。所以我们使用tail -f命令来作为这个容器的守护进程来启动容器。我们可以通过构建一个脚本run_tomcat.sh来实现,启动tomcat并让tail -f 最为前台进程。

    1
    2
    3
    4
    vim run_tomcat.sh
    #!/bin/bash
    su - www -c "/apps/tomcat/bin/catalina.sh start"
    su - www -c "tail -f /etc/hosts"

      且为了安全考虑,我们打算让tomcat服务以www用户身份启动,所以在构建容器时,需要注意权限问题,Dockerfile如下:

    1
    2
    3
    4
    5
    6
    7
    8
    vim Dockerfile
    # tomcat-apps
    FROM tomcat-base:v8.5.47
    ADD run_tomcat.sh /apps/tomcat/bin/run_tomcat.sh
    ADD app1/* /apps/tomcat/webapps/myapp/
    RUN chown www.www /apps/ -R
    EXPOSE 8080 8009
    CMD ["/apps/tomcat/bin/run_tomcat.sh"]

      而且我们把run_tomcat.sh传进去后,要以脚本启动的话,要对脚本加执行权限,这样传进去的时候也是有执行权限的。

    1
    chmod +x run_tomcat.sh

      然后构建build脚本

    1
    2
    3
    vim build_app1.sh
    #!/bin/bash
    docker build -t tomcat-app1:v1 .

      然后将APP1的程序代码拷贝至当前目录,结构示意图如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    root@DockerUbuntu:/opt/dockerfile/web/tomcat/myapps/app1# tree
    .
    ├── build_app1.sh
    ├── Dockerfile
    ├── app1
    │ └── index.html
    └── run_tomcat.sh

    1 directory, 4 files

      就可以执行build脚本打镜像啦。

    1
    bash build_app1.sh

      需要注意的是,启动镜像时记得加端口映射,命令如下

    1
    docker run -it -d -p 8080:8080 -p 8009:8009 tomcat-app1:v1

      此时通过ss -tanl命令就可以看到8080、80009端口已经被docker proxy监听了。说明服务启动成功。
    如果宿主机为centos,或者redhat(ubuntu默认是开启的),可能会需要先打开内核参数ip_forward选项,否则会报错网络不可用:

    1
    WARNING: IPv4 forwarding is disabled. Networking will not work.

      那就开启IP转发。

    1
    2
    vim /etc/sysctl.conf
    net.ipv4.ip_forward=1

      然后sysctl -p生效就可以正常使用容器网络了。

    注意事项

      需要注意的是,RUN命令是类似启动一个新的进程或者是shell,来执行每一次命令,执行完毕后此次RUN进程结束,下一次是一个全新的RUN进程了,相互之间不会联系。这么说可能大家无法理解,举个最简单的例子吧,就是当我们想要编译安装haproxy的时候,需要进入编译目录然后执行make命令,编译完后还要在编译目录执行make install命令。所以可以写成

    1
    RUN cd /usr/localk/haproxy-2.0.5 && make --xxx参数选项省略xxxx  && make install

      但是如果我们像写脚本那样,写成

    1
    2
    3
    RUN cd /usr/localk/haproxy-2.0.5
    RUN make --xxx参数选项省略xxxx
    RUN make install

      或者

    1
    2
    RUN cd /usr/localk/haproxy-2.0.5 && make --xxx参数选项省略xxxx
    RUN make install

      就会报错找不到路径,因为没一条命令都是一层独立的镜像,每层镜像开始都会在默认的初始目录,所以如果打算将makemake install分开写,则两次都加cd /usr/local/haproxy-2.0.5,或者直接切换工作目录,如下所示。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    WORKDIR /usr/local/src/haproxy-2.0.5
    RUN make ARCH=x86_64 \
    TARGET=linux-glibc USE_PCRE=1 \
    USE_OPENSSL=1 \
    USE_ZLIB=1 \
    USE_CPU_AFFINITY=1 \
    USE_LUA=1 \
    LUA_INC=../lua-5.3.5/src/ \
    LUA_LIB=../lua-5.3.5/src/ \
    PREFIX=/apps/haproxy
    RUN make install PREFIX=/apps/haproxy

      WORKDIRUSER很类似,都是修改后,对后续Dockerfile指令有效。且WORKDIR还支持相对路径,例如

    1
    2
    3
    WORKDIR /a
    WORKDIR b
    WORKDIR c

      则最终的工作目录为/a/b/c


一个低调的男人