docker入门——构建镜像

前面咱们已经介绍了如何拉取已经构建好的带有定制内容的Docker镜像,那么如何构建本身的镜像呢?html

构建Docker镜像有如下两种方法:python

  • 使用docker commit命令。
  • 使用docker build命令和 Dockerfile 文件。

在这里并不推荐使用docker commit来构建镜像,而应该使用更灵活、更强大的Dockerfile来构建Docker镜像。可是为了对Docker有一个更全面的了解,仍是会先介绍如下如何使用docker commit构建Docker镜像。以后将重点介绍Docker所推荐的镜像构建方法:编写Dockerfile以后使用docker build命令。nginx

通常来讲,咱们不是真正的“建立”新镜像,而是基于一个已有的基础镜像,如ubuntu或centos等,构建新镜像而已。若是真的想从零构建一个全新的镜像,也能够参考https://docs.docker.com/engine/userguide/eng-image/baseimages/。git

经过commit命令建立镜像

docker commit 构建镜像能够想象为是在往版本控制系统里提交变动。咱们先建立一个容器,并在容器里作出修改,就像修改代码同样,最后再将修改提交为一个镜像。github

# docker run -i -t ubuntu /bin/bash
root@b437ffe4d630:/# apt-get -yqq update
root@b437ffe4d630:/# apt-get -y install apache2

咱们启动了一个容器,并在里面安装了Apache。咱们会将这个容器做为一个Web服务器来运行,因此咱们想把它的当前状态保存下来。这样咱们就没必要每次都建立一个新容器并再次在里面安装Apache了。为了完成此项工做,须要先使用exit命令从容器里退出,以后再运行docker commit命令:web

# docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                      PORTS               NAMES
b437ffe4d630        ubuntu              "/bin/bash"         45 minutes ago      Exited (0) 10 seconds ago                       clever_pare         
b87f9dde62b0        devopsil/puppet     "/bin/bash"         2 days ago          Up 2 days                                       evil_archimedes     

# docker commit b437ffe4d630 test/apache2
9c30616364f44a519571709690e3c92a5cad4ad01c007d8126eb6d63670d33f4

# docker images test/apache2
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
test/apache2        latest              9c30616364f4        36 seconds ago      254.4 MB

在使用docker commit命令中,指定了要提交的修改过的容器的ID(能够经过docker ps命令获得刚建立的容器ID),以及一个目标镜像仓库和镜像名,这里是test/apahce2。须要注意的是,docker commit提交的只是建立容器的镜像与容器的当前状态之间有差别的部分,这使得该更新很是轻量。经过docker images 能够查看新建立的镜像信息。docker

也能够在提交镜像时指定更多的数据(包括标签)来详细描述所作的修改。shell

# docker commit -m="A new custom image" --author="Bourbon Tian" b437ffe4d630 test/apache2:webserver
27fc508c41d1180b1a421380d755cf00f9dfb6b0d354b9eccaec94ae58a06675

这条命令里,咱们指定了更多的信息选项:apache

  • -m 用来指定建立镜像的提交信息;
  • --author 用来列出该镜像的做者信息;
  • 最后在test/apache2后面增长了一个webserver标签。

经过使用docker inspect命令来查看新建立的镜像的详细信息:ubuntu

# docker inspect test/apache2:webserver
[
{
    "Id": "27fc508c41d1180b1a421380d755cf00f9dfb6b0d354b9eccaec94ae58a06675",
    "Parent": "f5bb94a8fac47aaf15fb4e4ceb138d59ac2fcf004cd3f277cebe2174fd7a6c70",
    "Comment": "A new custom image",
    "Created": "2017-05-17T07:29:46.000512241Z",
    "Container": "b437ffe4d63047dd34653f5256bb6eda54acfd3db99f72f2262a9b9af7f31334",
...

 若是想从刚建立的镜像运行一个容器,可使用docker run命令:

# docker run -t -i test/apache2:webserver /bin/bash

建立Dockerfile文件

下面将介绍如何经过Dockerfile的定义文件和docker build命令来构建镜像。

Dockerfile使用基本的基于DSL语法的指令来构建一个Docker镜像,以后使用docker build命令基于该Dockerfile中的指令构建一个新的镜像。

# mkdir /opt/static_web
# cd /opt/static_web/
# vim Dockerfile

首先建立一个名为static_web的目录用来保存Dockerfile,这个目录就是咱们的构建环境(build environment),Docker则称此环境为上下文(context)或者构建上下文(build context)。Docker会在构建镜像时将构建上下文和该上下文中的文件和目录上传到Docker守护进程。这样Docker守护进程就能直接访问你想在镜像中存储的任何代码、文件或者其余数据。这里咱们还建立了一个Dockerfile文件,咱们将用它构建一个能做为Web服务器的Docker镜像。

# Version: 0.0.1
FROM ubuntu:latest
MAINTAINER Bourbon Tian "bourbon@1mcloud.com"
RUN apt-get update
RUN apt-get install -y nginx
RUN echo 'Hi, I am in your container' > /usr/share/nginx/html/index.html
EXPOSE 80

Dockerfile由一系列指令和参数组成。每条指令都必须为大写字母,切后面要跟随一个参数。Dockerfile中的指令会按照顺序从上到下执行,因此应该根据须要合理安排指令的顺序。每条指令都会建立一个新的镜像层并对镜像进行提交。Docker大致上按照以下流程执行Dockerfile中的指令。

  • Docker从基础镜像运行一个容器。
  • 执行第一条指令,对容器进行修改。
  • 执行相似docker commit的操做,提交一个新的镜像层。
  • Docker再基于刚提交的镜像运行一个新的容器。
  • 执行Dockerfile中的下一条命令,直到全部指令都执行完毕。

从上面能够看出,若是你的Dockerfile因为某些缘由(如某条指令失败了)没有正常结束,那你也能够获得一个可使用的镜像。这对调试很是有帮助:能够基于该镜像运行一个具有交互功能的容器,使用最后建立的镜像对为何你的指令会失败进行调试。

Dockerfile也支持注释。以#开头的行都会被认为是注释,# Version: 0.0.1这就是个注释

FROM:

每一个Dockerfile的第一条指令都应该是FROM。FROM指令指定一个已经存在的镜像,后续指令都是将基于该镜像进行,这个镜像被称为基础镜像(base iamge)。在这里ubuntu:latest就是做为新镜像的基础镜像。也就是说Dockerfile构建的新镜像将以ubuntu:latest操做系统为基础。在运行一个容器时,必需要指明是基于哪一个基础镜像在进行构建。

MAINTAINER:

MAINTAINER指令,这条指令会告诉Docker该镜像的做者是谁,以及做者的邮箱地址。这有助于表示镜像的全部者和联系方式

RUN:

在这些命令以后,咱们指定了三条RUN指令。RUN指令会在当前镜像中运行指定的命令。这里咱们经过RUN指令更新了APT仓库,安装nginx包,并建立了一个index.html文件。像前面说的那样,每条RUN指令都会建立一个新的镜像层,若是该指令执行成功,就会将此镜像层提交,以后继续执行Dockerfile中的下一个指令。

默认状况下,RUN指令会在shell里使用命令包装器/bin/sh -c 来执行。若是是在一个不支持shell的平台上运行或者不但愿在shell中运行(好比避免shell字符串篡改),也可使用exec格式的RUN指令,经过一个数组的方式指定要运行的命令和传递给该命令的每一个参数:

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

EXPOSE:

EXPOSE指令是告诉Docker该容器内的应用程序将会使用容器的指定端口。这并不意味着能够自动访问任意容器运行中服务的端口。出于安全的缘由,Docker并不会自动打开该端口,而是须要你在使用docker run运行容器时来指定须要打开哪些端口。

能够指定多个EXPOSE指令来向外部公开多个端口,Docker也使用EXPOSE指令来帮助将多个容器连接,在后面的学习过程当中咱们会接触到。

基于Dockerfile构建新镜像 

执行docker build命令时,Dockerfile中的全部指令都会被执行而且提交,而且在该命令成功结束后返回一个新镜像。

# cd static_web
# docker build -t="test/static_web" .
Sending build context to Docker daemon 2.048 kB
Sending build context to Docker daemon 
...
Successfully built 94728651ce15
  • -t选项为新镜像设置了仓库和名称,这里仓库为test,镜像名为static_web。建议为本身的镜像设置合适的名字方便之后追踪和管理

也能够在构建镜像的过程中为镜像设置一个标签:

# docker build -t="test/static_web:v1" .

上面命令中最后的“.”告诉Docker到当前目录中去找Dockerfile文件。也能够指定一个Git仓库地址来指定Dockerfile的位置,这里Docker假设在Git仓库的根目录下存在Dockerfile文件:

# docker build -t="test/static_web:v1" git@github.com:test/static_web

 再回到docker build过程。能够看到构建上下文已经上传到Docker守护进程:

Sending build context to Docker daemon 2.048 kB
Sending build context to Docker daemon

提示:若是在构建上下文的根目录下存在以.dockerignore命名的文件的话,那么该文件内容会被按行进行分割,每一行都是一条文件过滤匹配模式。这很是像.gitignore文件,该文件用来设置哪些文件不会被上传到构建上下文中去。该文件中模式的匹配规则采用了Go语言中的filepath。

以后,能够看到Dockerfile中的每条指令会被顺序执行,而做为构建过程当中最终结果,返回了新镜像的ID,即94728651ce15。构建的每一步及其对应指令都会独立运行,而且在输出最终镜像ID以前,Docker会提交每步的构建结果。

指令失败时会怎样?

假设咱们将安装的软件包名字弄错,好比写成ngin,再次运行docker build:

# docker build -t="test/static_web" .
Sending build context to Docker daemon 2.048 kB
Sending build context to Docker daemon 
Step 0 : FROM ubuntu:latest
 ---> f5bb94a8fac4
Step 1 : MAINTAINER Bourbon Tian "bourbon@1mcloud.com"
 ---> Using cache
 ---> ce64f2e75a74
Step 2 : RUN apt-get update
 ---> Using cache
 ---> e98d2c152d1d
Step 3 : RUN apt-get install -y ngin
 ---> Running in 2f16c5f11250
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

这时咱们须要调试一下此次失败,咱们能够经过docker run命令来基于此次构建到目前为止已经成功的最后一步建立一个容器,这里它的ID是e98d2c152d1d:

# docker run -t -i e98d2c152d1d /bin/bash 
root@55aee4322f77:/# apt-get install -y ngin
Reading package lists... Done
Building dependency tree       
Reading state information... Done
E: Unable to locate package ngin

再次运行出错的指令apt-get install -y ngin,发现这里没有找到ngin包,咱们执行安装nginx包时,包命输错了。这时退出容器使用正确的包名修改Dockerfile文件,以后再尝试进行构建。

构建缓存:

在上面执行构建镜像的过程当中,咱们发现当执行apt-get update时,返回Using cache。Docker会将以前的镜像层看作缓存,由于在安装nginx前并无作其余的修改,所以Docker会将以前构建时建立的镜像当作缓存并做为新的开始点。而后,有些时候须要确保构建过程不会使用缓存。可使用docker build 的 --no-cache标志。

# docker build --no-cache -t="test/static_web" .

构建缓存带来的一个好处就是,咱们能够实现简单的Dockerfile模板(好比在Dockerfile文件顶部增长包仓库或者更新包,从而尽量确保缓存命中)。

# cat Dockerfile 

# Version: 0.0.1
FROM ubuntu:latest
MAINTAINER Bourbon Tian "bourbon@1mcloud.com"
ENV REFRESHED_AT 2017-05-18
RUN apt-get update
RUN apt-get install -y nginx
RUN echo 'Hi, I am in your container' > /usr/share/nginx/html/index.html
EXPOSE 80
  • ENV 在镜像中设置环境变量,在这里设置了一个名为REFRESHED_AT的环境变量,这个环境变量用来代表该镜像模板最后的更新时间,这样只须要修改ENV指令中的日期,这使Docker在命中ENV指令时开始重置这个缓存,并运行后续指令而无需依赖该缓存。也就是说,RUN apt-get update这条指令将会被再次执行,包缓存也将会被刷新为最新内容。

查看新镜像:

如今来看一下新构建的镜像,使用docker image命令,若是想深刻探求镜像如何构建出来的,可使用docker history命令看到新构建的test/static_web镜像的每一层,以及建立这些层的Dockerfile指令。

# docker images test/static_web
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
test/static_web     latest              94728651ce15        20 hours ago        212.1 MB

# docker history 94728651ce15
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
94728651ce15        20 hours ago        /bin/sh -c #(nop) EXPOSE 80/tcp                 0 B                 
09e999b131f4        20 hours ago        /bin/sh -c echo 'Hi, I am in your container'    27 B                
4af2ef04fb91        20 hours ago        /bin/sh -c apt-get install -y nginx             56.52 MB            
e98d2c152d1d        20 hours ago        /bin/sh -c apt-get update                       38.29 MB      
...

重新镜像启动容器

下面基于新构建的镜像启动一个新容器,来检查以前的构建工做是否一切正常:

# docker run -d -p 80 --name static_web test/static_web nginx -g "daemon off;"
a4ad951b2ef91275bb918d11964d7d60889608efa3958e699030d38a681ba35e
  • -d选项,告诉Docker以分离(detached)的方式在后台运行。这种方式很是适合运行相似Nginx守护进程这样的须要长时间运行的进程。
  • 这里也指定了须要在容器中运行的命令:nginx -g "daemon off;"。这将之前台运行的方式启动Nginx,来做为咱们的Web服务器。
  • -p选项,控制Docker在运行时应该公开哪些网络端口给外部(宿主机)。运行一个容器时,Docker可经过两种方法在宿主机上分配端口。
    • Docker能够在宿主机上经过/proc/sys/net/ipv4/ip_local_port_range文件随机一个端口映射到容器的80端口。
    • 能够在Docker宿主机中指定一个具体的端口号来映射到容器的80端口上。

这将在Docker宿主机上随机打开一个端口,这个端口会链接到容器中的80端口上。可使用docker ps命令查看容得的端口分配状况:

# docker ps -l
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS              PORTS                   NAMES
0b422bbcce10        test/static_web     "nginx -g 'daemon of   5 seconds ago       Up 5 seconds        0.0.0.0:32772->80/tcp   static_web 

若是没有启动成功,则经过交互的方式进入咱们新建立的镜像中,尝试启动nginx,经过分析错误日志查出不能正常启动的缘由,在这里我遇到的问题是:

nginx: [emerg] socket() [::]:80 failed (97: Address family not supported by protocol)

咱们须要删除/etc/nginx/sites-enabled/default 中 listen [::]:80 ipv6only=on default_server;定位到问题,咱们退出容器,从新修改咱们的Dockerfile:

# Version: 0.0.1
FROM ubuntu:latest
MAINTAINER Bourbon Tian "bourbon@1mcloud.com"
ENV REFRESHED_AT 2017-05-18
RUN apt-get update
RUN apt-get install -y nginx
RUN echo 'Hi, I am in your container' > /usr/share/nginx/html/index.html
RUN sed -i '22d' /etc/nginx/sites-enabled/default
EXPOSE 80

从新尝试构建咱们的容器,再次启动咱们新建的容器,经过docker ps -l查看是否正常启动了。

咱们也能够经过docker port 来查看容器的端口映射状况:

# docker port 0b422bbcce10  80
0.0.0.0:32772

在上面的命令中咱们指定了想要查看映射状况的容器ID和容器的端口号,这里是80。该命令返回了宿主机中映射的端口,即32772。

-p选项还让咱们能够灵活地管理容器和宿主机之间的端口映射关系。好比,能够指定将容器中的端口映射到Docker宿主机的某一个特定的端口上:

# docker run -d -p 80:80 --name static_web test/static_web nginx -g "daemon off;"
ee09ef811a9865d9bd50c71b3ddcbd414194031f14145fdbaf339d92e3ccd1bd

 上面的命令会将容器内的80端口绑定到本地宿主机的80端口上。咱们也能够将端口绑定限制在特定的网络接口(即ip地址)上:

# docker run -d -p 127.0.0.1:80:80 --name static_web test/static_web nginx -g "daemon off;"

咱们也可使用相似的方法将容器内的80端口绑定到一个特定网络接口的随机端口上:

# docker run -d -p 127.0.0.1::80 --name static_web test/static_web nginx -g "daemon off;"

 Docker还提供了一个更简单的方式,即-P参数,该参数能够用来对外公开在Dockfile中的EXPOSE指令中设置的全部端口:

# docker run -d -P --name static_web test/static_web nginx -g "daemon off;"
4fd632e975ad5e47a487e5e23790124da0826886dc24b2497a561d274e4e698e

# docker ps -l
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS              PORTS                   NAMES
4fd632e975ad        test/static_web     "nginx -g 'daemon of   4 seconds ago       Up 3 seconds        0.0.0.0:32773->80/tcp   static_web

该命令会将容器内的80端口对本地宿主机公开,而且绑定到宿主机的一个随机端口上。该命令会将用来构建该镜像的Dockerfile文件中EXPOSE指令指定的其余端口也一并公开。

# curl localhost:32773
Hi, I am in your container

到这,就完成了一个很是简单的基于Docker的Web服务器。

删除镜像

咱们能够经过docker rmi命令来删除一个镜像

# docker rmi test/webapp
Untagged: test/webapp:latest
Deleted: 36ae30d2e972f6651b29127266d68783290e3a861b974c5a491e04ae7e9a9d3d
Deleted: 8cecce09465bc0f2679fd96e1c6e1af03af9c4589b62113d319f24ca969d9164
Deleted: 29c803cce363f84801bd8b8c768bba8767c37947e803c8ae58541d163622ccfa
Deleted: 92a79034071552c09f45ffb1afc455150edc438d4c7da48b28ca6a2dba44d15b

这里咱们删除了test/webapp(在附录Dockerfile指令这个章节中构建的)镜像。在这里也能够看到Docker的分层文件系统:每一个Deleted都表明一个镜像层被删除。该操做只会将本地的镜像删除。若是咱们想删除本地的全部镜像能够像这样:

# docker rmi `docker images -a -q`

运行本身的Docker Registry

前面咱们已经介绍了Docker有公共的Docker Registry就是Docker Hub。可是有时咱们可能但愿构建和存储包含不想被公开的信息或数据的镜像。这时候咱们有如下两种选择:

  • 利用Docker Hub上的私有仓库;
  • 在防火墙后面运行本身的Registry。

从Docker容器安装一个Registry很是简单

## 拉去registry镜像
# docker pull registry

## 搭建本地镜像源
# docker run -d -v /opt/registry:/var/lib/registry -p 5000:5000 --restart=always --name registry registry:latest

## 查看容器状态
# docker ps -a
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS                     PORTS                    NAMES
f570fab5d67d        registry:latest     "/entrypoint.sh /etc   3 seconds ago       Up 3 seconds               0.0.0.0:5000->5000/tcp   registry

接下来将咱们的镜像上传到本地的Docker Registry

## 找到咱们要上传的镜像
# docker images test/apache2
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
test/apache2       latest              9c30616364f4        7 days ago          254.4 MB

## 使用新的Registry给该镜像打上标签
# docker tag 9c30616364f4 docker.example.com:5000/test/apache2

## 经过docker push 命令将它推送到新的Registry中去
# docker push docker.example.com:5000/test/apache2
The push refers to a repository [docker.example.com:5000/test/apache2] (len: 1)
9c30616364f4: Image already exists 
f5bb94a8fac4: Image successfully pushed 
2e36b30057ab: Image successfully pushed 
0346cecb4e51: Image successfully pushed 
274da7f89b05: Image successfully pushed 
b5ce920a148c: Image successfully pushed 
576b12d1aa01: Image successfully pushed 
Digest: sha256:0c22a559f8dea881bca046e0ca27a01f73aa5f3c153b08b8bdf3306082e48b72

## 测试咱们上传的镜像
# docker run -it docker.example.com:5000/test/apache2 /bin/bash
root@5088a0fd20e8:/#
相关文章
相关标签/搜索