前面咱们已经介绍了如何拉取已经构建好的带有定制内容的Docker镜像,那么如何构建本身的镜像呢?html
构建Docker镜像有如下两种方法:python
在这里并不推荐使用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
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
经过使用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的定义文件和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中的指令。
从上面能够看出,若是你的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指令来帮助将多个容器连接,在后面的学习过程当中咱们会接触到。
执行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
也能够在构建镜像的过程中为镜像设置一个标签:
# 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
查看新镜像:
如今来看一下新构建的镜像,使用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
这将在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有公共的Docker Registry就是Docker Hub。可是有时咱们可能但愿构建和存储包含不想被公开的信息或数据的镜像。这时候咱们有如下两种选择:
从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:/#