构建自已的 Docker 镜像

 2017年02月06日    2520     声明


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帐号、选项/配置构建项目等。诸君可自行尝试,本篇不再过多介绍。