构建自已的 Docker 镜像


Docker 镜像(Image)是一种分层结构的文件系统,基于Docker Hub中已构建好的镜像后,我们可以快速构建自己的镜像。还可以将自己构建的镜像免费推送到Docker Hub用户仓库进行管理,然后就可以基于这些镜像创建容器

  1. 构建准备
  2. docker commit创建镜像
  3. 使用Dockerfile构建镜像
  4. 新镜像推送到Docker Hub
  5. 自动构建

1. 构建准备

1.1 创建帐号

构建镜像构建完成后,需要将镜像推送Docker Hub或自已私的有Regitry中。本文使用Docker Hub,因此开始前需要首先注册一个Docker Hub帐号。可以在Docker Hub官网https://hub.docker.com完成帐号的注册。

注册时需要输入用户名、邮箱、帐号密码,注册后会收到一封激活邮件,需要登录邮箱完成帐号的激活。

注册后,可以通过docker login命令登录Docker Hub

$ sudo docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: itbilu
Password: 
Login Succeeded

登录成功后,会收到Login Succeeded提示。登录后,认证信息上会被保存(保存于$HOME/.docker/config.json文件),以便之后使用。退出登录可以使用docker logout命令。


1.2 构建方法

基于现有镜像构建新的Docker 镜像可以使用以下两种方式:

  • 使用docker commit命令
  • 使用Dockerfile文件和docker build命令,即:编写Dockerfile文件后,再通过docker build命令构建

本文将介绍这两种构建方式。相对来说,第二种方法相对更灵活、功能也更强大,更推荐使用第二种方式来构建Docker 镜像。


2. docker commit创建镜像

docker commit可以通过修改容器创建新的镜像。这点类似于git commit的提交代码更新,我们可以首先创建一个容器,然后对容器进行修改,修改完成后像提交代码一样将修改提交为一个新镜像。

docker commit命令格式如下:

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

主要选项(OPTIONS)如下:

  • -a, --author - {string}, 作者(如:"John Hannibal Smith ")
  • -c, --change - {list}, 使用Dockerfile指令来创建镜像(默认 [])
  • -m, --message - {string}, 提交备注信息
  • -p, --pause - {string}, 提交时暂停容器(默认 true)

创建容器

首先创建一个容器,创建容器的镜像依然使用之前使用的ubuntu镜像:

$ sudo docker run -i -t --name itbilu_ubuntu ubuntu /bin/bash
root@864770ace7fb:/# 

安装软件

我们会将这个容器做为一个Web服务器使用,所以需要安装nginxapache

运行容器后,在容器中安装nginx

root@864770ace7fb:/# apt-get update
root@864770ace7fb:/# apt-get install nginx

安装完成后,可以将当前状态保存下来,这样就不用每次都创建容器并重新安装软件了。docker commit提交前,先退出容器:

root@864770ace7fb:/# exit

提交更改

提交时要通过容器名或容器ID指定所要提交的容器,并要指定一个目标仓库和镜像名。docker commit提交时比较轻量,只会提交创建容器的镜像与容器当前状态之间有差异的部分。

如,提要刚才配置的容器itbilu_ubuntu,并指定目标仓库和镜像名为itbilu/nginx

$ sudo docker commit itbilu_ubuntu itbilu/nginx
sha256:548e7912739ad4efefe615baaf1d60c78cb60531fd9cd859332762674e6184dc

提交后,就可以通过docker images命令看到新创建的容器:

$ sudo  docker images itbilu/nginx 
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
itbilu/nginx        latest              548e7912739a        10 hours ago        226 MB

提交镜像时,还可以指定一些提交参数和标签等。如:

$ sudo docker commit -m "一个自定义容器" -a "何民三" itbilu_ubuntu itbilu/nginx:webserver

在这条命令中,我们通过-m参数添加了一些提交备注,通过-a参数添加了镜像作者,并为新镜像添加了webserver标签。

查看镜像:

$ sudo  docker images itbilu/nginx
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
itbilu/nginx        webserver           0efe6ee86866        8 minutes ago       226 MB
itbilu/nginx        latest              548e7912739a        11 hours ago        226 MB

每次提交都会创建一个新镜像,在itbilu/nginx仓库下现在有两个不同ID的镜像。现在使用docker inspect命令查看新创建镜像的详细信息:

$ sudo docker inspect itbilu/nginx:webserver
[{
  "Id": "sha256:0efe6ee86866ad59d5ba14ff3c27e039894b010b76d54b2769396eb02bbc2444",
  …
  "Comment": "一个自定义容器",
  "Created": "2017-02-04T12:38:02.01058579Z",
  "Container": "864770ace7fbbc6b7abe13fc2b96e647d0ce5122de0b8c0de41a71a989959f10",
  …
  "DockerVersion": "1.13.0",
  "Author": "何民三",
  …
}]

使用新镜像

镜像提交会,就可以通过使用提交的镜像来创建容器。

$ sudo docker run -i -t itbilu/nginx:webserver /bin/bash
root@5d4f02240a10:/# 


3. 使用Dockerfile构建镜像

使用Dockerfiledocker build命令来构建镜像操作更灵活、过程可重复,因此也更推荐使用这种方式来构建镜像。

Dockerfile基于DSL(Domain Specific Language)语言构建Docker镜像,Dockerfile编写完成后,就可以使用docker build命令来构建一个新镜像。

3.1 创建Dockerfile文件

首先创建一个目录用于初始化Dockerfile文件

$ mkdir static_web_server
$ cd static_web_server
$ touch Dockerfile

如上所示,我们创建static_web_server目录,并在其中创见了Dockerfile文件。这个目录就是我们的构建环境,在Docker中,将这个环境称为上下文(content)或者构建上下文(build content)。构建镜像时,Docker会将构建环境中的文件和目录传递给守护进程,这样守护进程就访问到用户想在镜像中存储的任何代码、文件或其它数据。

接下来,编辑刚创建Dockerfile文件,编写Web服务器的构建代码:

# Version: 0.0.1
FROM ubuntu:16.04
MAINTAINER 何民三 "cn.liuht@gmail.com"
RUN apt-get update
RUN apt-get install -y nginx
RUN echo "Hello World, 我是个容器" \ 
   > /var/www/html/index.html
EXPOSE 80

在以上示例中,我们首先通过FROM指定了一个基础镜像ubuntu:16.04。在Dockerfile中,FROM命令只能有一个,该命令用于指定基础镜像,后续的命令都会基于该镜像进行。接着通过MAINTAINER命令告诉Docker镜像的作者、联系邮箱。

接下来,通过三条RUN语句安装软件环境。在这个示例中,首先通过RUN更新了APT源,然后安装了nginx,最后创建一个文件/usr/share/nginx/html/index.html并在其中添加了一些简单的示例文本。

RUN语句表示要在镜像中运行的命令。默认情况下,RUN指令会在/bin/sh -c。如果不想使用shell执行,可以exec来运行RUN命令,这时需要使用数组来传递指令和参数。如:

RUN ["apt-get", "install", "-y", "nginx"]

Dockerfile文件的最后,通过EXPOSE命令对外开放了80端口。出于安全考虑,Docker默认不会打开任何端口。EXPOSE会告诉Docker容器内应用将要使用的端口(可以指定多个),但这并不意味着会自动打开该端口,还需要在docker run运行容器时,通过--expose参数来指定要打开的端口。

注意:Dockerfile支持使用注释,注释以#开头,如上例中的第一行。

构建镜像时,构建目录下的文件默认都会被传入守护进程,如果有不需要传递守护进程的文件。可以通过.dockerignore文件指定,该文件类似.gitignore文件,如果创建后会对每行进行模式匹配并排除符合条件的文件。


关于Dockerfile文件

Dockerfile是由一系列命令参数组成的一个文件。其中,每条件命令都要大写(如:FROM),且其后都要跟一个参数(如:ubuntu:16.04)。构建镜像时,Dockerfile中的命令会按顺序从上到下执行,在编写Dockerfile文件时应注意各条命令的顺序安排。Dockerfile文件中的每条命令,都会创建一个新的镜像层并会提交镜像。

Docker使用Dockerfile构建镜像流程大致如下:

  • 从基础镜像运行一个容器
  • 执行一条命令,对容器进行修改
  • 执行类似docker commit操作,提交一个新的镜像层
  • 基于刚创建的镜像运行一个新容器
  • 继续执行下一条命令,直到所有命令执行完


3.2 docker build构建新镜像

Dockerfile文件创建完成后,就可以通过docker build命令来构建新镜像。执行docker build命令时,Dockerfile中的命令都会被执行和提交,且每次提交都会创建一个新镜像。

如,在上例中的构建过程如下:

$ sudo docker build -t="itbilu/static_web_server" ./
Sending build context to Docker daemon 2.048 kB
Step 1/6 : FROM ubuntu:16.04
 ---> f49eec89601e
Step 2/6 : MAINTAINER 何民三 "cn.liuht@gmail.com"
 ---> Running in 5bab087b741a
 ---> 3f62a1fdbb79
Removing intermediate container 5bab087b741a
Step 3/6 : RUN apt-get update
 ---> Running in 0efa222098eb
 ...
Reading package lists...
 ---> d426a15eb005
Removing intermediate container 0efa222098eb
Step 4/6 : RUN apt-get install -y nginx
 ---> Running in 322ec20871fc
Reading package lists...
...
Processing triggers for systemd (229-4ubuntu13) ...
 ---> 03df6f8f43d0
Removing intermediate container 322ec20871fc
Step 5/6 : RUN echo "Hello World, 我是个容器"    > /usr/share/nginx/html/index.html
 ---> Running in bc9f189a89d2
 ---> 184542d2035e
Removing intermediate container bc9f189a89d2
Step 6/6 : EXPOSE 80
 ---> Running in 80d82f35cfea
 ---> a404b6967a02
Removing intermediate container 80d82f35cfea
Successfully built a404b6967a02

在使用docker build构建镜像时,我们通过-t参数指定itbilu/static_web_server做为镜像名,其中itbilu表示仓库,static_web_server表示镜像名。

构建镜像时,还可以为镜像设置标签,设置格式为镜像名:标签。如:

$ sudo docker build -t="itbilu/static_web_server:v0.0.1" .

在构建时我们可以看到,构建上下文被传给了Docker的守护进程。在构建过程中,每执行一条命令都会有一次镜像创建提交,和使用上一步生成的镜像运行新容器的过程。如:

# 构建上下文发送至守护进程
Sending build context to Docker daemon 2.048 kB
...
Step 3/6 : RUN apt-get update
# 使用上一步创建的镜像运行新容器
 ---> Running in 0efa222098eb
 ...
Reading package lists...
# 提交新镜像
 ---> d426a15eb005

在命令的最后,通过.(同./)告诉Docker从本地当前工作目录查找Dockerfile文件。除指定本地文件还,还可以使用Git源代码仓库或一个文件URL。类似如下:

$ sudo docker build -t="itbilu/static_web_server:v0.0.1" \ 
git@github.com:itbilu/static_web_server

注意:以上git仓库并不存在。当使用Git地址需要Dockerfile文件位于仓库的根目录下。

另外,Dockerfile文件是docker build默认查找的文件,如果不使用这个文件名,可以通过-f参数指定文件路径。如:

$ sudo docker build -t="itbilu/static_web_server:v0.0.1" -f path/to/file


构建失败情况

构建过程中,每一步都会提交且生成新镜像,如果某一步出错,其上一步生成的镜像还是会被保留且可用。

如,假设安装nginx时出错:

 ---> d426a15eb005
Step 4/6 : RUN apt-get install -y ngin
 ---> Running in fa9a54ab1fa3
Reading package lists...
Building dependency tree...
Reading state information...
E: Unable to locate package ngin
The command '/bin/sh -c apt-get install -y ngin' returned a non-zero code: 100

这时上一步生成的镜像d426a15eb005依然可用,可使用上一步生成的镜像进行调试:

$ sudo docker run -t -t d426a15eb005 /bin/bash
root@953676e604fb:/# 

出错后,可使用上一步生成的镜像运行容器。然后在容器内运行出错的步骤进行调试,找出错误原因后,退出容器,改成Dockerfile文件重新构建即可。


构建缓存

由于构建过程中的每一步都会将结果提交为镜像,Docker 会将这些镜像做为缓存使用。重新构建时,Docker会对比每一步生成的镜像,如果没有变化就不会重新生成镜像,以节约构建时间。如,前面构建出错的情况,重新构建时,Docker并不是从头开始执行,而是直接从上次出错的位置开始。

如果不希望使用缓存,可以为docker build命令指定--no-cache参数。如:

$ sudo docker build --no-cache -t="itbilu/static_web_server:v0.0.1" .


3.3 使用新镜像

查看新镜像

使用docker images命令查看刚构建的镜像。

$ sudo docker images itbilu/static_web_server
REPOSITORY                 TAG                 IMAGE ID            CREATED             SIZE
itbilu/static_web_server   latest              a404b6967a02        4 hours ago         226 MB
itbilu/static_web_server   v0.0.1              a404b6967a02        4 hours ago         226 MB

在前面示例中,我们分别构建完成了itbilu/static_web_server镜像的两个版本v0.0.1latest

如果想查看镜像的构建过程,可以使用docker history命令查看:

$ sudo docker history itbilu/static_web_server
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
a404b6967a02        4 hours ago         /bin/sh -c #(nop)  EXPOSE 80/tcp                0 B                 
184542d2035e        4 hours ago         /bin/sh -c echo "Hello World, 我是个容器"    > ...   29 B                
03df6f8f43d0        4 hours ago         /bin/sh -c apt-get install -y nginx             56.8 MB             
d426a15eb005        4 hours ago         /bin/sh -c apt-get update                       39.7 MB             
3f62a1fdbb79        4 hours ago         /bin/sh -c #(nop)  MAINTAINER 何民三 "cn.liuh...   0 B                 
f49eec89601e        2 weeks ago         /bin/sh -c #(nop)  CMD ["/bin/bash"]            0 B                 
<missing>           2 weeks ago         /bin/sh -c mkdir -p /run/systemd && echo '...   7 B                 
<missing>           2 weeks ago         /bin/sh -c sed -i 's/^#\s*\(deb.*universe\...   1.9 kB              
<missing>           2 weeks ago         /bin/sh -c rm -rf /var/lib/apt/lists/*          0 B                 
<missing>           2 weeks ago         /bin/sh -c set -xe   && echo '#!/bin/sh' >...   745 B               
<missing>           2 weeks ago         /bin/sh -c #(nop) ADD file:68f83d996c38a09...   129 MB     

通过docker history命令可以查看镜像的每一层,及创建这些层的Dockerfile命令。

使用

可以基于这些新构建的镜像启动一个新容器,以检查构建是否成功:

$ sudo docker run -d -p 80 --name static_web_server itbilu/static_web_server \
> nginx -g "daemon off;"
1aa73f0160b5951d07adb9ff6e0ddf4cb6730a127ad79a2a3ab52d4ceaa7203b

在这个示例中,我们通过-d参数告诉Docker在后台运行容器,还通过--name参数为容器指定了名称。而nginx -g "daemon off"是要在容器中运行的命令,表示要以前台的方式启动Nginx

还指定了一个-p参数,这个是告诉Docker应该对宿主机开放的端口。端口开放给宿主机后,宿主机还需要分配一个端口映射到容器。可以通过以下两种方式分配:

  1. 随机分配 - Docker 会在宿主机上随机选择一个位于32768~61000之间的端口映射到容器
  2. 指定端口 - 在启动容器时直接指定映射端口

在前面启动容器时,我们并没有指定宿主机的映射端口,因此docker run会在宿主机上随机分配一个端口,并映射到容器的80端口。这时,可以通过docker ps查看分配的端口:

$ sudo docker ps -l
CONTAINER ID        IMAGE                      COMMAND                  CREATED             STATUS              PORTS                   NAMES
1129bdbb25de        itbilu/static_web_server   "nginx -g 'daemon ..."   6 seconds ago       Up 5 seconds        0.0.0.0:32769->80/tcp   static_web_server

还可以通过docker port命令查看容器的端口映射情况(可以通过容器名或容器ID查看):

$ sudo docker port static_web_server
80/tcp -> 0.0.0.0:32769

如果不想使用随机端口,也可以docker run启动容器时,通过-p参数将容器端口直接映射到Docker宿主机的端口上:

$ sudo docker run -d -p 8080:80 --name static_web_server itbilu/static_web_server \
> nginx -g "daemon off;"

这样,就会将容器内的80端口映射到宿主机的8080端口上。

除一端口绑定外,-p参数还可以绑定宿主机的IP,如:

$ sudo docker run -d -p 127.0.0.1:80:80 --name static_web_server itbilu/static_web_server \
> nginx -g "daemon off;"

这样,我们就将容器的80端口绑定到了宿主机127.0.0.1IP的80端口上。绑定IP时,也支持绑定宿主机的随机端口如:

$ sudo docker run -d -p 127.0.0.1::80 --name static_web_server itbilu/static_web_server \
> nginx -g "daemon off;"

我们在前面创建Dockerfile文件时,曾经通过EXPORT向外公开了80端口。这样,我们可以在运行容器时通过简单的-P参数开放EXPORT导出的所有端口:

$ sudo docker run -d -P --name static_web_server itbilu/static_web_server \
> nginx -g "daemon off;"

使用docker protdocker ps获取宿主机的映射端口后,就可以在浏览器或使用curl命令通过127.0.0.1localhost及端口号,连接到容器中的Web服务器了:

$ sudo docker port static_web_server
80/tcp -> 0.0.0.0:32772
$ sudo curl 127.0.0.1:32772
Hello World, 我是个容器


更多关于Dockerfile介绍:


4. 新镜像推送到Docker Hub

新镜像构建完成后,可以将其推送到Docker Hub,这样就可以在需要的时候轻松获取和使用镜像,其它人也可以使用你构建的镜像。如果不希望镜像被无关人员看到,可以将其推送到私有仓库。

推送镜像使用docker push命令。

如,将前面创建的static_web_server推送到Docker Hub

$ sudo docker push itbilu/static_web_server
The push refers to a repository [docker.io/itbilu/static_web_server]
182a2662aedc: Pushed 
38a8e3073a9e: Pushed 
11b2dd25d9ed: Pushed 
5eb5bd4c5014: Pushed 
d195a7a18c70: Mounted from library/ubuntu 
af605e724c5a: Mounted from library/ubuntu 
59f161c3069d: Mounted from library/ubuntu 
4f03495a4d7d: Mounted from library/ubuntu 
latest: digest: sha256:afbb364ca968e36ffe7ed5784f5412ff2e6cd22f717ef95d35fd9242b95350f9 size: 1988
2b3fd068f542: Pushed 
38a8e3073a9e: Layer already exists 
11b2dd25d9ed: Layer already exists 
5eb5bd4c5014: Layer already exists 
d195a7a18c70: Layer already exists 
af605e724c5a: Layer already exists 
59f161c3069d: Layer already exists 
4f03495a4d7d: Layer already exists 
v0.0.1: digest: sha256:64c80ac28791d32769122e1a640bedb8be8d4608d23c77a6c4fe6f6be52e5154 size: 1988

如果指定的镜像仓库存在,镜像会被添加到仓库中;如果不存在,则会创建仓库,并将镜像推送到仓库中。在前面我们在前面总共创建了两个static_web_server镜像版本,因此会有两个镜像被推送到镜像仓库。而static_web_server仓库,在Docker Hub中并不存在,所以会首先在itbilu用户下创建这个仓库。

可以在Docker Hub看到上传的镜像,如下图:

镜像推送到Docker Hub

注意:推送镜像时,一定要使用用户ID/仓库名的形式。如果仅使用仓库名,Docker会认为这是一个root仓库,会推送失败。

从上面的推送过程可以看出,镜像是一种分层结构的文件系统。镜像推送到镜像仓库时,这些层都会被推送到仓库中。使用docker rmi删除镜像时,这些层也都会被删除。


5. 自动构建

Docker Hub还提供了自动构建功能,这样我们就可以不必手工建立镜像再推送,只需要将包含Dockerfile文件的GitHubBitbucket地址关联到Docker HubDocker Hub就能自动完成构建过程。

使用自动构建前,首先要要将GitHubBitbucket帐号连接到Docker Hub

登录Docker Hub后,点击Create - Create Automated Build,如下:

Docker创建自动构建

创建自动构建还,还需要关联Link GithubLink Bitbucket帐号、选项/配置构建项目等。诸君可自行尝试,本篇不再过多介绍。