代码改变世界

Docker 安装、镜像、dockerfile、容器、仓库

2018-05-30 21:10  受匕图灵  阅读(1773)  评论(0编辑  收藏  举报

2018-05-30

 

 参考:

1、《docker从入门到实战》

2、菜鸟教程http://www.runoob.com/docker/docker-command-manual.html、

3、docker官网https://docs.docker.com/install/linux/docker-ce/centos/#uninstall-old-versions

 

1、部署docker

1、卸载旧版本docker
$ sudo yum remove docker \
                  docker-client \
                  docker-client-latest \
                  docker-common \
                  docker-latest \
                  docker-latest-logrotate \
                  docker-logrotate \
                  docker-selinux \
                  docker-engine-selinux \
                  docker-engine
2、安装依赖包
$ sudo yum install -y yum-utils \
           device-mapper-persistent-data \
           lvm2
3、添加国内 yum 软件源
$ sudo yum-config-manager \
    --add-repo \
    https://mirrors.ustc.edu.cn/docker-ce/linux/centos/docker-ce.repo

4、更新 yum 软件源缓存,并安装 docker-ce 。
$ sudo yum makecache fast
$ sudo yum install docker-ce
5、启动 Docker CE:
$ sudo systemctl enable docker
$ sudo systemctl start docker
6、添加用户到docker组,且注消、重新登录。docker安装完毕后会自动创建docker群组、自动设置docker组执行docker相关命令。
$ sudo usermod -aG docker $USER

或者$ sudo gpasswd -a $USER docker

重新登录后验证当前用户已经添加上docker群组:

$ id

另:退出docker群组的命令(不需要执行):gpasswd -d $USER docker

7、测试 Docker 是否安装正确
$ docker run hello-world

报错:Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get http://%2Fvar%2Frun%2Fdocker.sock/v1.37/images/json: dial unix /var/run/docker.sock: connect: permission denied。

  • 原因:没有x执行权限导致。
  • 解决步骤:
    • a、只添加用户到docker组。
    • b、注消、重新登录。

8、配置国内镜像进行加速

新增/etc/docker/daemon.json文件且添加如下内容:

 {

   "registry-mirrors": [

     "https://registry.docker-cn.com"

   ]

 }

重启服务:

$ sudo systemctl daemon-reload

$ sudo systemctl restart docker

检查加速器是否生效:

$ docker info

如果从结果中看到了如下内容,说明配置成功

 Registry Mirrors:

 https://registry.docker-cn.com/

9、添加内核配置参数

默认配置下,如果在 CentOS 使用Docker CE 看到下面的这些警告信息:

WARNING: bridge-nf-call-iptables is disabled

WARNING: bridge-nf-call-ip6tables is disabled

请添加内核配置参数以启用这些功能:

$ sudo tee -a /etc/sysctl.conf <<-EOF
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
EOF

重新加载 sysctl.conf 即可:
$ sudo sysctl -p

10、docker卸载

$ sudo yum remove docker-ce
$ sudo rm -rf /var/lib/docker

 

2安装指定版本

1、卸载旧版本docker
$ sudo yum remove docker \
                  docker-client \
                  docker-client-latest \
                  docker-common \
                  docker-latest \
                  docker-latest-logrotate \
                  docker-logrotate \
                  docker-selinux \
                  docker-engine-selinux \
                  docker-engine
2、安装 docker-latest

$ yum list | grep docker |grep "1."
$ sudo yum install docker-latest.x86_64
5、启动 Docker CE:
$ sudo systemctl enable docker-latest
$ sudo systemctl start docker-latest
3、注消、重新登录。docker安装完毕后会自动创建docker群组、自动设置docker群组执行docker相关命令。

另:退出docker群组的命令(不需要执行):gpasswd -d $USER docker

 

3、Docker镜像命令

1、docker search :从Docker Hub查找镜像
语法

  • docker search [OPTIONS] TERM

OPTIONS说明:

  • --automated :只列出 automated build类型的镜像;
  • --no-trunc :显示完整的镜像描述;
  • -s :列出收藏数不小于指定值的镜像。

2、docker pull : 从镜像仓库中拉取或者更新指定镜像
语法

  • docker pull [OPTIONS] NAME[:TAG|@DIGEST]

OPTIONS说明:

  • -a :拉取所有 tagged 镜像
  • --disable-content-trust :忽略镜像的校验,默认开启

从私有仓库抓取镜像:docker pull <path_to_registry>/<image>

3、docker push : 将本地的镜像上传到镜像仓库,要先登陆到镜像仓库
语法

  • docker push [OPTIONS] NAME[:TAG]

OPTIONS说明:

  • --disable-content-trust :忽略镜像的校验,默认开启

4、docker commit :从容器创建一个新的镜像。
语法

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

OPTIONS说明:

  • -a :提交的镜像作者;
  • -c :使用Dockerfile指令来创建镜像;
  • -m :提交时的说明文字;
  • -p :在commit时,将容器暂停。

5、docker images : 列出本地镜像。
语法

  • docker images [OPTIONS] [REPOSITORY[:TAG]]

OPTIONS说明:

  • -a :列出本地所有的镜像(含中间映像层,默认情况下,过滤掉中间映像层);
  • --digests :显示镜像的摘要信息;
  • -f :显示满足条件的镜像;
  • --format :指定返回值的模板文件;
  • --no-trunc :显示完整的镜像信息;
  • -q :只显示镜像ID。

docker images -f dangling=true            显示虚悬镜像
docker images prune                 删除虚拟镜像
docker images --format "{{.ID}}: {{.Repository}}"        直接列出镜像结果,并且只包含镜像ID和仓库名
docker images --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}"        打算以表格等距显示,并且有标题行,和默认一样,不过自己定义列

6、docker diff : 检查容器里文件结构的更改。
语法

  • docker diff [OPTIONS] CONTAINER

7、docker rmi : 删除本地一个或多少镜像。
语法

  • docker rmi [OPTIONS] IMAGE [IMAGE...]

OPTIONS说明:

  • -f :强制删除;
  • --no-prune :不移除该镜像的过程镜像,默认移除;

8、docker save : 将指定镜像保存成 tar 归档文件。
语法

  • docker save [OPTIONS] IMAGE [IMAGE...]

OPTIONS说明:

  • -o :输出到的文件。

9、docker load : 从.tar压缩文件中加载镜像

语法

  • docker load [ -i name.tar]

10、docker export :将文件系统作为一个tar归档文件导出到STDOUT。
语法

  • docker export [OPTIONS] CONTAINER

OPTIONS说明:

  • -o :将输入内容写到文件。

 

11、docker import : 从归档文件中创建镜像。
语法

  • docker import [OPTIONS] file|URL|- [REPOSITORY[:TAG]]

OPTIONS说明:

    • -c :应用docker 指令创建镜像;
    • -m :提交时的说明文字;

12、tag : 标记镜像
语法

  • docker tag IMAGE[:TAG] [REGISTRY_HOST[:REGISTRY_PORT]/]REPOSITORY[:TAG]

实例:使用docker tag将ubuntu:latest这个镜像标记为127.0.0.1:5000/ubuntu:latest。
$ docker tag ubuntu:latest 127.0.0.1:5000/ubuntu:latest

13、docker history:查看镜像内的历史记录

4、Dockerfile

FROM 指定基础镜像

所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。就像我们之前运行了一个nginx镜像的容器,再进行修改一样,基础镜像是必须指定的。而FROM就是指定基础镜像,因此一个DockerfileFROM是必备的指令,并且必须是第一条指令。

Docker Store上有非常多的高质量的官方镜像,有可以直接拿来使用的服务类的镜像,nginxredismongomysqlhttpdphptomcat;也有一些方便开发、构建、运行各种语言应用的镜像,nodeopenjdkpythonrubygolang等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。

如果没有找到对应服务的镜像,官方镜像中还提供了一些更为基础的操作系统镜像,ubuntudebiancentosfedoraalpine,这些操作系统的软件库为我们提供了更广阔的扩展空间。

除了选择现有镜像为基础镜像外,Docker还存在一个特殊的镜像,名为scratch。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。

FROM scratch

...

如果你以scratch为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。

不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见,比如swarmcoreos/etcd。对于Linux下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接FROM scratch会让镜像体积更加小巧。使用Go语言开发的应用很多会使用这种方式来制作镜像,这也是为什么有人认为Go是特别适合容器微服务架构的语言的原因之一。

RUN 执行命令行命令

RUN 多条的写法是完全没有意义的,很多运行时不需要的东西都被装进了镜像里,比如编译环境、更新的软件包等等。结果就是产生非常臃肿、非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。这是很多初学Docker的人常犯的一个错误。

Union FS是有最大层数限制的,比如AUFS,曾经是最大不得超过42,现在是不得超过127层。

首先,之前所有的命令只有一个目的,就是编译、安装redis可执行文件。因此没有必要建立很多层,这只是一层的事情。因此,这里没有使用很多个RUN对一一对应不同的命令,而是仅仅使用一个RUN指令,并使用&&将各个所需命令串联起来。将之前的7,简化为了1层。在撰写Dockerfile的时候,要经常提醒自己,这并不是在写Shell脚本,而是在定义每一层该如何构建。

并且,这里为了格式化还进行了换行。Dockerfile支持Shell类的行尾添加\的命令换行方式,以及行首#进行注释的格式。良好的格式,比如换行、缩进、注释等,会让维护、排障更为容易,这是一个比较好的习惯。

此外,还可以看到这一组命令的最后添加了清理工作的命令,删除了为了编译构建所需要的软件,清理了所有下载、展开的文件,并且还清理了apt缓存文件。这是很重要的一步,我们之前说过,镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。因此镜像构建时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉,就是每一层构建的最后一定要清理掉无关文件。

COPY 复制文件

格式:

COPY <源路径>... <目标路径

COPY ["<源路径1>",... "<目标路径>"] 

和 RUN 指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。COPY 指令将从构建上下文目录中 <源路径的文件/目录复制到新的一层的镜像内的 <目标路径位置。比如:

COPY package.json /usr/src/app/

 

<源路径> 可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的 filepath.Match 规则,:

COPY hom* /mydir/

COPY hom?.txt /mydir/

<目标路径> 可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR 指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。

此外,还需要注意一点,使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git进行管理的时候。

 

ADD 更高级的复制文件

ADD 指令和 COPY 的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。

如果 <源路径是一个 URL,这种情况下,Docker 引擎会试图去下载这个链接的文件放到 <目标路径去。下载后的文件权限自动设置为 600,如果这并不是想要的权限,那么还需要增加额外的一层 RUN 进行权限调整;另外,如果下载的是个压缩包,需要解压缩,也一样还需要额外的一层 RUN 指令进行解压缩。所以不如直接使用 RUN 指令,然后使用 wget 或者 curl 工具下载,处理权限、解压缩、然后清理无用文件更合理。因此,这个功能其实并不实用,而且不推荐使用。

如果 <源路径为一个 tar 压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的情况下,ADD 指令将会自动解压缩这个压缩文件到 <目标路径去。

在某些情况下,这个自动解压缩的功能非常有用,比如官方镜像 ubuntu :

FROM scratch

ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /

...

但在某些情况下,如果我们真的是希望复制个压缩文件进去,而不解压缩,这时就不可以使用 ADD 命令了。

在 Docker 官方的 Dockerfile 最佳实践文档 中要求,尽可能的使用 COPY,因为 COPY 的语义很明确,就是复制文件而已,而 ADD 则包含了更复杂的功能,其行为也不一定很清晰。最适合使用 ADD 的场合,就是所提及的需要自动解压缩的场合。

另外需要注意的是,ADD 指令会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。

因此在 COPY 和 ADD 指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用COPY指令,仅在需要自动解压缩的场合使用ADD

 

CMD 容器启动命令

也是两种格式:

l shell格式:CMD <命令>

l exec格式:CMD ["可执行文件", "参数1", "参数2"...]

l 参数列表格式:CMD ["参数1", "参数2"...]。在指定了ENTRYPOINT指令后,CMD指定具体的参数。

之前介绍容器的时候曾经说过,Docker 不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。CMD指令就是用于指定默认的容器主进程的启动命令的。

在运行时可以指定新的命令来替代镜像设置中的这个默认命令,比如,ubuntu镜像默认的CMD/bin/bash,如果我们直接docker run -it ubuntu的话,会直接进入bash。我们也可以在运行时指定运行别的命令,docker run -it ubuntu cat /etc/os-release。这就是用cat /etc/os-release命令替换了默认的/bin/bash命令了,输出了系统版本信息。

在指令格式上,一般推荐使用exec格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号",而不要使用单引号。

如果使用shell格式的话,实际的命令会被包装为sh -c的参数的形式进行执行。比如:

CMD echo $HOME

在实际执行中,会将其变更为:

CMD [ "sh", "-c", "echo $HOME" ]

这就是为什么我们可以使用环境变量的原因,因为这些环境变量会被 shell 进行解析处理。提到CMD就不得不提容器中应用在前台执行和后台执行的问题。这是初学者常出现的一个混淆。

Docker 不是虚拟机,容器中的应用都应该以前台执行,而不是像虚拟机、物理机里面那样,用 upstart/systemd 去启动后台服务,容器内没有后台服务的概念。

一些初学者将CMD写为:

CMD service nginx start

然后发现容器执行后就立即退出了。甚至在容器内去使用 systemctl 命令结果却发现根本执行不了。这就是因为没有搞明白前台、后台的概念,没有区分容器和虚拟机的差异,依旧在以传统虚拟机的角度去理解容器。

对于容器而言,其启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。

而使用 service nginx start 命令,则是希望 upstart 来以后台守护进程形式启动 nginx 服务。而刚才说了 CMD service nginx start 会被理解为 CMD [ "sh", "-c", "service nginx start"] ,因此主进程实际上是 sh 。那么当 service nginx start 命令结束后, sh 也就结束了, sh 作为主进程退出了,自然就会令容器退出。

正确的做法是直接执行 nginx 可执行文件,并且要求以前台形式运行。比如:

CMD ["nginx", "-g", "daemon off;"]

 

ENTRYPOINT 入口点

ENTRYPOINT 的格式和 RUN 指令格式一样,分为 exec 格式和 shell 格式。

ENTRYPOINT 的目的和 CMD 一样,都是在指定容器启动程序及参数。 ENTRYPOINT 在运行时也可以替代,不过比 CMD 要略显繁琐,需要通过 docker run 的参数 --entrypoint 来指定。

当指定了 ENTRYPOINT , CMD 的含义就发生了改变,不再是直接的运行其命令,而是将CMD 的内容作为参数传给 ENTRYPOINT 指令,换句话说实际执行时,将变为:

<ENTRYPOINT> "<CMD>"

那么有了 CMD ,为什么还要有 ENTRYPOINT ?这种 <ENTRYPOINT> "<CMD>" 有什么好处么?让我们来看几个场景。

 

场景一:让镜像变成像命令一样使用

假设我们需要一个得知自己当前公网 IP 的镜像,那么可以先用 CMD 来实现:

FROM ubuntu:16.04

RUN apt-get update \

 && apt-get install -y curl \

 && rm -rf /var/lib/apt/lists/*

CMD [ "curl", "-s", "http://ip.cn" ]

假如我们使用 docker build -t myip . 来构建镜像的话,如果我们需要查询当前公网 IP,只需要执行:

$ docker run myip

当前 IP:61.148.226.66 来自:北京市 联通

,这么看起来好像可以直接把镜像当做命令使用了,不过命令总有参数,如果我们希望加参数呢?比如从上面的 CMD 中可以看到实质的命令是 curl ,那么如果我们希望显示 HTTP头信息,就需要加上 -i 参数。那么我们可以直接加 -i 参数给 docker run myip ?

$ docker run myip -i

docker: Error response from daemon: invalid header field value "oci runtime error: container_linux.go:247: starting container process caused \"exec: \\\"-i\\\": executable file not found in $PATH\"\n".

我们可以看到可执行文件找不到的报错, executable file not found 。之前我们说过,跟在镜像名后面的是 command ,运行时会替换 CMD 的默认值。因此这里的 -i 替换了原来的CMD ,而不是添加在原来的 curl -s http://ip.cn 后面。而 -i 根本不是命令,所以自然找不到。

那么如果我们希望加入 -i 这参数,我们就必须重新完整的输入这个命令:

$ docker run myip curl -s http://ip.cn -i

这显然不是很好的解决方案,而使用 ENTRYPOINT 就可以解决这个问题。现在我们重新用ENTRYPOINT 来实现这个镜像:

FROM ubuntu:16.04

RUN apt-get update \

 && apt-get install -y curl \

 && rm -rf /var/lib/apt/lists/*

ENTRYPOINT [ "curl", "-s", "http://ip.cn" ]

这次我们再来尝试直接使用 docker run myip -i :

$ docker run myip

当前 IP:61.148.226.66 来自:北京市 联通

$ docker run myip -i

HTTP/1.1 200 OK

Server: nginx/1.8.0

Date: Tue, 22 Nov 2016 05:12:40 GMT

Content-Type: text/html; charset=UTF-8

Vary: Accept-Encoding

X-Powered-By: PHP/5.6.24-1~dotdeb+7.1

X-Cache: MISS from cache-2

X-Cache-Lookup: MISS from cache-2:80

X-Cache: MISS from proxy-2_6

Transfer-Encoding: chunked

Via: 1.1 cache-2:80, 1.1 proxy-2_6:8006

Connection: keep-alive

当前 IP:61.148.226.66 来自:北京市 联通

可以看到,这次成功了。这是因为当存在 ENTRYPOINT , CMD 的内容将会作为参数传给ENTRYPOINT ,而这里 -i 就是新的 CMD ,因此会作为参数传给 curl ,从而达到了我们预期的效果。

场景二:应用运行前的准备工作

启动容器就是启动主进程,但有些时候,启动主进程前,需要一些准备工作。

比如 mysql 类的数据库,可能需要一些数据库配置、初始化的工作,这些工作要在最终的mysql 服务器运行之前解决。

此外,可能希望避免使用 root 用户去启动服务,从而提高安全性,而在启动服务前还需要以 root 身份执行一些必要的准备工作,最后切换到服务用户身份启动服务。或者除了服务外,其它命令依旧可以使用 root 身份执行,方便调试等。

这些准备工作是和容器 CMD 无关的,无论 CMD 为什么,都需要事先进行一个预处理的工作。这种情况下,可以写一个脚本,然后放入 ENTRYPOINT 中去执行,而这个脚本会将接到的参数(也就是 <CMD> )作为命令,在脚本最后执行。比如官方镜像 redis 中就是这么做的:

FROM alpine:3.4

...

RUN addgroup -S redis && adduser -S -G redis redis

...

ENTRYPOINT ["docker-entrypoint.sh"]

EXPOSE 6379

CMD [ "redis-server" ]

可以看到其中为了 redis 服务创建了 redis 用户,并在最后指定了 ENTRYPOINT 为 docker-

entrypoint.sh 脚本。

#!/bin/sh

...

# allow the container to be started with `--user`

if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then

 chown -R redis .

 exec su-exec redis "$0" "$@"

fi

exec "$@"

该脚本的内容就是根据 CMD 的内容来判断,如果是 redis-server 的话,则切换到redis用户身份启动服务器,否则依旧使用 root 身份执行。比如:

$ docker run -it redis id

uid=0(root) gid=0(root) groups=0(root)

ENV 设置环境变量

格式有两种:

 ENV <key> <value> 

 ENV <key1>=<value1> <key2>=<value2>... 

这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN ,还是运行时的应用,都可以直接使用这里定义的环境变量。

ENV VERSION=1.0 DEBUG=on \

 NAME="Happy Feet"

这个例子中演示了如何换行,以及对含有空格的值用双引号括起来的办法,这和 Shell 下的行为是一致的。

定义了环境变量,那么在后续的指令中,就可以使用这个环境变量。比如在官方 node 镜像Dockerfile ,就有类似这样的代码:

ENV NODE_VERSION 7.2.0

RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.ta

r.xz" \

 && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \

 && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \

 && grep " node-v$NODE_VERSION-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c - \

 && tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=

1 \

 && rm "node-v$NODE_VERSION-linux-x64.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \

 && ln -s /usr/local/bin/node /usr/local/bin/nodejs

在这里先定义了环境变量 NODE_VERSION ,其后的 RUN 这层里,多次使用 $NODE_VERSION 来进行操作定制。可以看到,将来升级镜像构建版本的时候,只需要更新 7.2.0 即可, Dockerfile 构建维护变得更轻松了。

下列指令可以支持环境变量展开:

 ADD 、 COPY 、 ENV 、 EXPOSE 、 LABEL 、 USER 、 WORKDIR 、 VOLUME 、 STOPSIGNAL 、 ONBUILD 

可以从这个指令列表里感觉到,环境变量可以使用的地方很多,很强大。通过环境变量,我们可以让一份 Dockerfile 制作更多的镜像,只需使用不同的环境变量即可。

ARG 构建参数

格式: ARG <参数名>[=<默认值>] 

构建参数和 ENV 的效果一样,都是设置环境变量。所不同的是, ARG 所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的。但是不要因此就使用 ARG 保存密码之类的信息,因为 docker history 还是可以看到所有值的。

Dockerfile 中的 ARG 指令是定义参数名称,以及定义其默认值。该默认值可以在构建命令

 docker build 中用 --build-arg <参数名>=<来覆盖。

在 1.13 之前的版本,要求 --build-arg 中的参数名,必须在 Dockerfile 中用 ARG 定义过了,换句话说,就是 --build-arg 指定的参数,必须在 Dockerfile 中使用了。如果对应参数没有被使用,则会报错退出构建。从 1.13 开始,这种严格的限制被放开,不再报错退出,而是显示警告信息,并继续构建。这对于使用 CI 系统,用同样的构建流程构建不同的Dockerfile 的时候比较有帮助,避免构建命令必须根据每个 Dockerfile 的内容修改。

VOLUME 定义匿名卷

格式为:

 VOLUME ["<路径1>", "<路径2>"...] 

 VOLUME <路径

之前我们说过,容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume),后面的章节我们会进一步介绍

Docker 卷的概念。为了防止运行时用户忘记将动态文件所保存目录挂载为卷,Dockerfile ,我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂

,其应用也可以正常运行,不会向容器存储层写入大量数据。

VOLUME /data

这里的 /data 目录就会在运行时自动挂载为匿名卷,任何向 /data 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。当然,运行时可以覆盖这个挂载设置。比如:

docker run -d -v mydata:/data xxxx

在这行命令中,就使用了 mydata 这个命名卷挂载到了 /data 这个位置,替代了Dockerfile 中定义的匿名卷的挂载配置。

EXPOSE 声明端口

格式为 EXPOSE <端口1> [<端口2>...]

EXPOSE 指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 docker run -P ,会自动随机映射 EXPOSE 的端口。

此外,在早期 Docker 版本中还有一个特殊的用处。以前所有容器都运行于默认桥接网络中,因此所有容器互相之间都可以直接访问,这样存在一定的安全性问题。于是有了一个 Docker引擎参数 --icc=false,当指定该参数后,容器间将默认无法互访,除非互相间使用了 --links 参数的容器才可以互通,并且只有镜像中 EXPOSE 所声明的端口才可以被访问。这个 --icc=false 的用法,在引入了 docker network 后已经基本不用了,通过自定义网络可以很轻松的实现容器间的互联与隔离。

要将 EXPOSE 和在运行时使用 -p <宿主端口>:<容器端口区分开来。-p,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 EXPOSE 仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。

WORKDIR 指定工作目录

格式为 WORKDIR <工作目录路径>

使用 WORKDIR 指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR 会帮你建立目录。

之前提到一些初学者常犯的错误是把 Dockerfile 等同于 Shell 脚本来书写,这种错误的理解还可能会导致出现下面这样的错误:

RUN cd /app

RUN echo "hello" > world.txt

如果将这个 Dockerfile 进行构建镜像运行后,会发现找不到 /app/world.txt 文件,或者其内容不是 hello。原因其实很简单,在 Shell ,连续两行是同一个进程执行环境,因此前一个命令修改的内存状态,会直接影响后一个命令;而在 Dockerfile ,这两行 RUN 命令的执行环境根本不同,是两个完全不同的容器。这就是对 Dockerfile 构建分层存储的概念不了解所导致的错误。

之前说过每一个 RUN 都是启动一个容器、执行命令、然后提交存储层文件变更。第一层 RUN cd /app 的执行仅仅是当前进程的工作目录变更,一个内存上的变化而已,其结果不会造成任何文件变更。而到第二层的时候,启动的是一个全新的容器,跟第一层的容器更完全没关系,自然不可能继承前一层构建过程中的内存变化。

因此如果需要改变以后各层的工作目录的位置,那么应该使用 WORKDIR 指令。

USER 指定当前用户

格式:USER <用户名

USER 指令和 WORKDIR 相似,都是改变环境状态并影响以后的层。WORKDIR 是改变工作目录,USER 则是改变之后层的执行 RUN, CMD 以及 ENTRYPOINT 这类命令的身份。

当然,和 WORKDIR 一样,USER 只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。

RUN groupadd -r redis && useradd -r -g redis redis

USER redis

RUN [ "redis-server" ]

如果以 root 执行的脚本,在执行期间希望改变身份,比如希望以某个已经建立好的用户来运行某个服务进程,不要使用 su 或者 sudo,这些都需要比较麻烦的配置,而且在 TTY 缺失的环境下经常出错。建议使用 gosu

建立 redis 用户,并使用 gosu 换另一个用户执行命令

RUN groupadd -r redis && useradd -r -g redis redis

下载 gosu

RUN wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.7/

gosu-amd64" \

&& chmod +x /usr/local/bin/gosu \

&& gosu nobody true

设置 CMD,并以另外的用户执行

CMD [ "exec", "gosu", "redis", "redis-server" ]

HEALTHCHECK 健康检查

格式:

 HEALTHCHECK [选项] CMD <命令>:设置检查容器健康状况的命令

 HEALTHCHECK NONE:如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令

HEALTHCHECK 指令是告诉 Docker 应该如何进行判断容器的状态是否正常,这是 Docker 1.12引入的新指令。

在没有 HEALTHCHECK 指令前,Docker 引擎只可以通过容器内主进程是否退出来判断容器是否状态异常。很多情况下这没问题,但是如果程序进入死锁状态,或者死循环状态,应用进程并不退出,但是该容器已经无法提供服务了。在 1.12 以前,Docker 不会检测到容器的这种状态,从而不会重新调度,导致可能会有部分容器已经无法提供服务了却还在接受用户请求。

而自 1.12 之后,Docker 提供了 HEALTHCHECK 指令,通过该指令指定一行命令,用这行命令来判断容器主进程的服务状态是否还正常,从而比较真实的反应容器实际状态。

当在一个镜像指定了 HEALTHCHECK 指令后,用其启动容器,初始状态会为 starting,在 HEALTHCHECK 指令检查成功后变为 healthy,如果连续一定次数失败,则会变为 unhealthy

HEALTHCHECK 支持下列选项:

l  --interval=<间隔>:两次健康检查的间隔,默认为 30 ;

l  --timeout=<时长>:健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认 30 ;

l  --retries=<次数>:当连续失败指定次数后,则将容器状态视为 unhealthy,默认 3次。

和 CMD, ENTRYPOINT 一样,HEALTHCHECK 只可以出现一次,如果写了多个,只有最后一个生效。

在 HEALTHCHECK [选项] CMD 后面的命令,格式和 ENTRYPOINT 一样,分为 shell 格式,exec 格式。命令的返回值决定了该次健康检查的成功与否:0:成功;1:失败;2:保留,不要使用这个值。

假设我们有个镜像是个最简单的 Web 服务,我们希望增加健康检查来判断其 Web 服务是否在正常工作,我们可以用 curl 来帮助判断,其 Dockerfile 的 HEALTHCHECK 可以这么写:

FROM nginx

RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

HEALTHCHECK --interval=5s --timeout=3s \

CMD curl -fs http://localhost/ || exit 1

这里我们设置了每 秒检查一次(这里为了试验所以间隔非常短,实际应该相对较长),如果健康检查命令超过 秒没响应就视为失败,并且使用 curl -fs http://localhost/ || exit1 作为健康检查命令。

使用 docker build 来构建这个镜像:

$ docker build -t myweb:v1 .

构建好了后,我们启动一个容器:

$ docker run -d --name web -p 80:80 myweb:v1

当运行该镜像后,可以通过 docker ps 看到最初的状态为 (health: starting):

$ docker ps

CONTAINER IDIMAGE COMMANDCREATED S

TATUSPORTS NAMES

03e28eb00bd0myweb:v1"nginx -g 'daemon off" 3 seconds ago U

p 2 seconds (health: starting) 80/tcp, 443/tcp web

在等待几秒钟后,再次 docker ps,就会看到健康状态变化为了 (healthy):

$ docker ps

CONTAINER IDIMAGE COMMANDCREATED S

TATUSPORTS NAMES

03e28eb00bd0myweb:v1"nginx -g 'daemon off" 18 seconds agoU

p 16 seconds (healthy) 80/tcp, 443/tcp web

如果健康检查连续失败超过了重试次数,状态就会变为 (unhealthy)

为了帮助排障,健康检查命令的输出(包括 stdout 以及 stderr)都会被存储于健康状态里,可以用 docker inspect 来查看。

$ docker inspect --format '{{json .State.Health}}' web | python -m json.tool

{

"FailingStreak": 0,

"Log": [

{

"End": "2016-11-25T14:35:37.940957051Z",

"ExitCode": 0,

"Output": "<!DOCTYPE html>\n<html>\n<head>\n<title>Welcome to nginx!</titl

e>\n<style>\nbody {\nwidth: 35em;\nmargin: 0 auto;\nfont-f

amily: Tahoma, Verdana, Arial, sans-serif;\n}\n</style>\n</head>\n<body>\n<h1>Welc

ome to nginx!</h1>\n<p>If you see this page, the nginx web server is successfully inst

alled and\nworking. Further configuration is required.</p>\n\n<p>For online documentat

ion and support please refer to\n<a href=\"http://nginx.org/\">nginx.org</a>.<br/>\nCo

mmercial support is available at\n<a href=\"http://nginx.com/\">nginx.com</a>.</p>\n\n

<p><em>Thank you for using nginx.</em></p>\n</body>\n</html>\n",

"Start": "2016-11-25T14:35:37.780192565Z"

}

],

"Status": "healthy"

}

ONBUILD 为他人做嫁衣裳

格式:ONBUILD <其它指令>

ONBUILD 是一个特殊的指令,它后面跟的是其它指令,比如 RUN, COPY ,而这些指令,在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。

Dockerfile 中的其它指令都是为了定制当前镜像而准备的,唯有 ONBUILD 是为了帮助别人定制自己而准备的。

假设我们要制作 Node.js 所写的应用的镜像。我们都知道 Node.js 使用 npm 进行包管理,所有依赖、配置、启动信息等会放到 package.json 文件里。在拿到程序代码后,需要先进行npm install 才可以获得所有需要的依赖。然后就可以通过 npm start 来启动应用。因此,一般来说会这样写 Dockerfile:

FROM node:slim

RUN mkdir /app

WORKDIR /app

COPY ./package.json /app

RUN [ "npm", "install" ]

COPY . /app/

CMD [ "npm", "start" ]

把这个 Dockerfile 放到 Node.js 项目的根目录,构建好镜像后,就可以直接拿来启动容器运行。但是如果我们还有第二个 Node.js 项目也差不多呢?好吧,那就再把这个 Dockerfile 复制到第二个项目里。那如果有第三个项目呢?再复制么?文件的副本越多,版本控制就越困难,让我们继续看这样的场景维护的问题。

如果第一个 Node.js 项目在开发过程中,发现这个 Dockerfile 里存在问题,比如敲错字了、或者需要安装额外的包,然后开发人员修复了这个 Dockerfile,再次构建,问题解决。第一个项目没问题了,但是第二个项目呢?虽然最初 Dockerfile 是复制、粘贴自第一个项目的,但是并不会因为第一个项目修复了他们的 Dockerfile,而第二个项目的 Dockerfile 就会被自动修复。

那么我们可不可以做一个基础镜像,然后各个项目使用这个基础镜像呢?这样基础镜像更新,各个项目不用同步 Dockerfile 的变化,重新构建后就继承了基础镜像的更新?好吧,可以,让我们看看这样的结果。那么上面的这个 Dockerfile 就会变为:

FROM node:slim

RUN mkdir /app

WORKDIR /app

CMD [ "npm", "start" ]

这里我们把项目相关的构建指令拿出来,放到子项目里去。假设这个基础镜像的名字为 my-node 的话,各个项目内的自己的 Dockerfile 就变为:

FROM my-node

COPY ./package.json /app

RUN [ "npm", "install" ]

COPY . /app/

基础镜像变化后,各个项目都用这个 Dockerfile 重新构建镜像,会继承基础镜像的更新。

那么,问题解决了么?没有。准确说,只解决了一半。如果这个 Dockerfile 里面有些东西需要调整呢?比如 npm install 都需要加一些参数,那怎么办?这一行 RUN 是不可能放入基础镜像的,因为涉及到了当前项目的 ./package.json,难道又要一个个修改么?所以说,这样制作基础镜像,只解决了原来的 Dockerfile 的前4条指令的变化问题,而后面三条指令的变化则完全没办法处理。

ONBUILD 可以解决这个问题。让我们用 ONBUILD 重新写一下基础镜像的 Dockerfile:

FROM node:slim

RUN mkdir /app

WORKDIR /app

ONBUILD COPY ./package.json /app

ONBUILD RUN [ "npm", "install" ]

ONBUILD COPY . /app/

CMD [ "npm", "start" ]

这次我们回到原始的 Dockerfile,但是这次将项目相关的指令加上 ONBUILD,这样在构建基础镜像的时候,这三行并不会被执行。然后各个项目的 Dockerfile 就变成了简单地:

FROM my-node

是的,只有这么一行。当在各个项目目录中,用这个只有一行的 Dockerfile 构建镜像时,之前基础镜像的那三行 ONBUILD 就会开始执行,成功的将当前项目的代码复制进镜像、并且针对本项目执行 npm install,生成应用镜像。

5、Docker容器命令

当利用docker run来创建容器时,Docker 在后台运行的标准操作包括:

l 检查本地是否存在指定的镜像,不存在就从公有仓库下载。

l 利用镜像创建并启动一个容器。

l 分配一个文件系统,并在只读的镜像层外面挂载一层可读写层。

l 从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去。

l 从地址池配置一个 ip 地址给容器。

l 执行用户指定的应用程序。

l 执行完毕后容器被终止。

1、docker run
语法:

  • docker run [options] IMAGE [command] [args]

OPTIONS说明:

  • -a stdin: 指定标准输入输出内容类型,可选 STDIN/STDOUT/STDERR 三项;
  • -d: 后台运行容器,并返回容器ID;
  • -i: 以交互模式运行容器,通常与 -t 同时使用;
  • -t: 为容器重新分配一个伪输入终端,通常与 -i 同时使用;
  • --name="nginx-lb": 为容器指定一个名称;
  • --dns 8.8.8.8: 指定容器使用的DNS服务器,默认和宿主一致;
  • --dns-search example.com: 指定容器DNS搜索域名,默认和宿主一致;
  • -h "mars": 指定容器的hostname;
  • -e username="ritchie": 设置环境变量;
  • --env-file=[]: 从指定文件读入环境变量;
  • --cpuset="0-2" or --cpuset="0,1,2": 绑定容器到指定CPU运行;
  • -m :设置容器使用内存最大值;
  • --net="bridge": 指定容器的网络连接类型,支持 bridge(在容器内部、主机上创建一对网络设备,在容器内部为eth0,而外部则是veth*网络设备,这个设备加在了主机上的Docker0网桥之中,从而实现容器与外部的通信)、host(容器和主机共用一个Network Namespace)、none(拥有自己的Network Namespace,但不为容器进行任何网络配置,需要用户自行添加)、container(容器不与主机共享Network Namespace地址空间,而是与一个指定的容器共享): 四种类型。 相关文章:四种网络模式 https://blog.csdn.net/noob_f/article/details/52875664
  • --link=[]: 添加链接到另一个容器;
  • --expose=[]: 开放一个端口或一组端口;
  • -p,--publish=[]:容器内的端口服务在主机OS上是无法访问的,这就需要提前对外发布端口,我们也可以认为是端口映射(ip:hostport:containerport)
  • --rm:在容器运行完毕后自动删除容器(这个选项不能与-d同时使用)
  • -v,--volume=[]:容器中的数据会随着容器生命周期的结束而消失,我们可以通过该选项将外部存储映射到容器内,将外部数据给容器访问,或者将容器的数据保存到外部(/host:/container)
  • --volumes-from=[]:从另外一个容器中mount卷
  • -w,--workdir=" ":设置容器中的工作文件夹
  • --name=" ":分配一个容器名字,如果不指定,则会自动生成
  • -u,--user=" ":指定容器运行后的uid或用户名
  • -c,--cpu-shares=0:cpu-shares是一个权重值,当多个容器运行在相同的CPU资源上时,会依据此权重值进行资源分配
  • --restart:设定容器重起策略。
    • no,默认策略,在容器退出时不重启容器。
    • on-failure,在容器非正常退出时(退出状态非0),才会重启容器。
    • on-failure:3,在容器非正常退出时重启容器,最多重启3次。
    • always,在容器退出时总是重启容器。
    • unless-stopped,在容器退出时总是重启容器,但是不考虑在Docker守护进程启动时就已经停止了的容器。

 

-P参数 外部访问容器

容器中可以运行一些网络应用,要让外部也可以访问这些应用,可以通过-P-p参数来指定端口映射。

当使用-P标记时,Docker会随机映射一个49000~49900的端口到内部容器开放的网络端口。

使用docker ps -a可以看到,本地主机的49155被映射到了容器的5000端口。此时访问本机的49155端口即可访问容器内web应用提供的界面。

-p参数 外部访问容器

-p则可以指定要映射的端口,并且,在一个指定端口上只可以绑定一个容器。支持的格式有ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort

 

使用hostPort:containerPort格式,本地的5000端口映射到容器的5000端口,可以执行:

$ docker run -d -p 5000:5000 training/webapp python app.py

此时默认会绑定本地所有接口上的所有地址。

 

可以使用ip:hostPort:containerPort格式,指定映射使用一个特定地址,比如 localhost 地址127.0.0.1,可以执行:

$ docker run -d -p 127.0.0.1:5000:5000 training/webapp python app.py

 

使用ip::containerPort格式,绑定localhost的任意端口到容器的5000端口,本地主机会自动分配一个端口。,可以执行:

$ docker run -d -p 127.0.0.1::5000 training/webapp python app.py

 

还可以使用udp标记来指定udp端口

$ docker run -d -p 127.0.0.1:5000:5000/udp training/webapp python app.py

 

-p标记可以多次使用来绑定多个端口

 

2、docker start
语法:

  • docker start [-i] [-a] <container(s)>

选项基本与run一样。

3、docker stop

4、docker restart
选项基本与run一样。

5、docker attach : 连接到正在运行中的容器。
选项:

  • --sig-proxy=false,确保CTRL-D或CTRL-C不会关闭容器。

 

6、docker ps : 列出容器
语法:

  • docker ps [OPTIONS]

OPTIONS说明:

  • -a,--all :显示所有的容器,包括未运行的。
  • -f :根据条件过滤显示的内容。
  • --format :指定返回值的模板文件。
  • -l,--latest :显示最近创建的容器。
  • -n :列出最近创建的n个容器。
  • --no-trunc :不截断输出。
  • -q,--quit :静默模式,只显示容器编号。
  • -s,--size :显示总的文件大小。
  • --before=" " :显示在某个容器ID之前启动的所有容器,包括停止的容器
  • --after=" " :显示在某个容器ID之后启动的所有容器,包括停止的容器

 

7、docker inspect : 获取容器/镜像的元数据。
语法:

  • docker inspect [OPTIONS] NAME|ID [NAME|ID...]

OPTIONS说明:

  • -f :指定返回值的模板文件。
  • -s :显示总的文件大小。
  • --type :为指定类型返回JSON。

实例,获取镜像mysql:5.6的元信息:
$ docker inspect mysql:5.6
实例,获取正在运行的容器mymysql的 IP:
$ docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' mymysql

7、docker rm 删除容器

$ docker rm trusting_newton

trusting_newton

如果要删除一个运行中的容器,可以添加 -f 参数。Docker 会发送 SIGKILL 信号给容器。

8、docker exec 进入容器

在使用 -d 参数时,容器启动后会进入后台。某些时候需要进入容器进行操作,此时需要使用docker exec 命令。

docker exec命令只用-i交互式操作参数时,由于没有分配伪终端,界面没有我们熟悉的Linux命令提示符,但命令执行结果仍然可以返回。当-i交互式操作、-t终端参数一起使用时,则可以看到我们熟悉的 Linux 命令提示符。

$ docker run -dit ubuntu

69d137adef7a8a689cbcb059e94da5489d3cddd240ff675c640c8d96e84fe1f6

$ docker ps

CONTAINER IDIMAGE COMMAND CREATED STATUS

PORTS NAMES

69d137adef7aubuntu:latest "/bin/bash" 18 seconds agoUp 17 

seconds zealous_swirles

docker exec -i 69d1 bash

ls

bin

boot

dev

...

docker exec -it 69d1 bash

root@69d137adef7a:/#

9、docker diff 查看容器中具体的改动

我们修改了容器的文件,也就是改动了容器的存储层。我们可以通过docker diff命令看到具体的改动。

$ docker diff webserver

 

10、docker port 查看映射地址:端口配置

$ docker port nostalgic_morse 5000

127.0.0.1:49155.

11、docker logs 获取容器的输出信息

 

12、docker export 导出本地某个容器

$ docker export 7691a814370e > ubuntu.tar

这样将导出容器快照到本地文件。

13、docker commit 将容器保存为镜像

语法格式为:

docker commit [选项] <容器ID或容器名> [<仓库名>[:<标签>]]

我们可以用下面的命令将容器保存为镜像:

$ docker commit --author "Tao Wang <twang2218@gmail.com>" --message "修改了默认网页" webserver nginx:v2

sha256:07e33465974800ce65751acc279adc6ed2dc5ed4e0838f8b86f0c87aa1795214

其中--author是指定修改的作者,--message则是记录本次修改的内容。这点和git版本控制相似,不过这里这些信息可以省略留空。

14、docker import 从容器快照文件中再导入为镜像

$ cat ubuntu.tar | docker import - test/ubuntu:v1.0

此外,也可以通过指定 URL 或者某个目录来导入,例如

$ docker import http://example.com/exampleimage.tgz example/imagerepo

:用户既可以使用 docker load 来导入镜像存储文件到本地镜像库,也可以使用 docker import 来导入一个容器快照到本地镜像库。这两者的区别在于容器快照文件将丢弃所有的历史记录和元数据信息(即仅保存容器当时的快照状态),而镜像存储文件将保存完整记录,体积也要大。此外,从容器快照文件导入时可以重新指定标签等元数据信息。

 

6、仓库 registry

镜像构建完成后,可以很容易的在当前宿主机上运行,但是,如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,Docker Registry 就是这样的服务。

一个 Docker Registry 中可以包含多个仓库(Repository);每个仓库可以包含多个标签(Tag);每个标签对应一个镜像。

通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。我们可以通过<仓库名>:<标签>的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以latest作为默认标签。

以 Ubuntu 镜像 为例,ubuntu是仓库的名字,其内包含有不同的版本标签,,14.04,16.04。我们可以通过ubuntu:14.04,或者ubuntu:16.04来具体指定所需哪个版本的镜像。如果忽略了标签,比如ubuntu,那将视为ubuntu:latest

仓库名经常以 两段式路径 形式出现,比如jwilder/nginx-proxy,前者往往意味着 Docker Registry 多用户环境下的用户名,后者则往往是对应的软件名。但这并非绝对,取决于所使用的具体 Docker Registry 的软件或服务。

公共仓库 Docker Hub

Docker Registry公开服务是开放给用户使用、允许用户管理镜像的 Registry 服务。一般这类公开服务允许用户免费上传、下载公开的镜像,并可能提供收费服务供用户管理私有镜像。

最常使用的 Registry 公开服务是官方的 Docker Hub,这也是默认的 Registry,并拥有大量的高质量的官方镜像。除此以外,还有 CoreOS 的 Quay.io,CoreOS 相关的镜像存储在这里。Google 的 Google Container Registry,Kubernetes 的镜像使用的就是这个服务。

由于某些原因,在国内访问这些服务可能会比较慢。国内的一些云服务商提供了针对Docker Hub的镜像服务(Registry Mirror),这些镜像服务被称为加速器。常见的有 阿里云加速器、DaoCloud 加速器 等。使用加速器会直接从国内的地址下载Docker Hub的镜像,比直接从Docker Hub下载速度会提高很多。在 安装 Docker 一节中有详细的配置方法。

国内也有一些云服务商提供类似于Docker Hub的公开服务。比如 时速云镜像仓库、网易云镜像服务、DaoCloud 镜像市场、阿里云镜像库 等。

目前 Docker 官方维护了一个公共仓库 Docker Hub,其中已经包括了数量超过 15,000 的镜像。大部分需求都可以通过在Docker Hub中直接下载镜像来实现。

注册

可以在 https://cloud.docker.com 免费注册一个 Docker 账号。

1、docker login 登录

可以通过执行docker login命令交互式的输入用户名及密码来完成在命令行界面登录Docker Hub

2、docker logout 退出登录

可以通过docker logout退出登录。

3、docker search 查找官方仓库中的镜像

可以通过docker search命令来查找官方仓库中的镜像。例如以centos为关键词进行搜索:

$ docker search centos

可以看到返回了很多包含关键字的镜像,其中包括镜像名字、描述、收藏数(表示该镜像的受关注程度)、是否官方创建、是否自动创建。

官方的镜像说明是官方项目组创建和维护的,automated 资源允许用户验证镜像的来源和内容。

根据是否是官方提供,可将镜像资源分为两类。

一种是类似centos这样的镜像,被称为基础镜像或根镜像。这些基础镜像由 Docker 公司创建、验证、支持、提供。这样的镜像往往使用单个单词作为名字。

还有一种类型,比如tianon/centos镜像,它是由 Docker 的用户创建并维护的,往往带有用户名称前缀。可以通过前缀username/来指定使用某个用户提供的镜像,比如 tianon 用户。

另外,在查找的时候通过--filter=stars=N参数可以指定仅显示收藏数量为N以上的镜像。

4、docker pul拉取镜像

利用docker pull命令来将它下载到本地。例如下载官方centos镜像到本地

$ docker pull centos

5、docker push 推送镜像

用户也可以在登录后通过docker push命令来将自己的镜像推送到 Docker Hub。以下命令中的username请替换为你的 Docker 账号用户名。

$ docker tag ubuntu:17.10 username/ubuntu:17.10

$ docker images

$ docker push username/ubuntu:17.10

$ docker search username

6、自动创建

自动创建(Automated Builds)功能对于需要经常升级镜像内程序来说,十分方便。

有时候,用户创建了镜像,安装了某个软件,如果软件发布新版本则需要手动更新镜像。

而自动创建允许用户通过Docker Hub指定跟踪一个目标网站(目前支持 GitHub BitBucket)上的项目,一旦项目发生新的提交或者创建新的标签(tag),Docker Hub 会自动构建镜像并推送到Docker Hub中。

要配置自动创建,包括如下的步骤:

l 创建并登录 Docker Hub,以及目标网站;

l 在目标网站中连接帐户到 Docker Hub;

l 在Docker Hub中 配置一个自动创建;

l 选取一个目标网站中的项目(需要含Dockerfile)和分支;

l 指定Dockerfile的位置,并提交创建。

之后,可以在Docker Hub的 自动创建页面 中跟踪每次创建的状态。

私有仓库

除了使用公开服务外,用户还可以在本地搭建私有 Docker RegistryDocker 官方提供了Docker Registry 镜像,可以直接使用做为私有 Registry 服务。

开源的 Docker Registry 镜像只提供了 Docker Registry API 的服务端实现,足以支持docker命令,不影响使用。但不包含图形界面,以及镜像维护、用户管理、访问控制等高级功能。在官方的商业化版本 Docker Trusted Registry ,提供了这些高级功能。

除了官方的 Docker Registry ,还有第三方软件实现了 Docker Registry API,甚至提供了用户界面以及一些高级功能。比如,VMWare Harbor 和 Sonatype Nexus

本文内容基于docker-registryv2.x 版本。

1、运行docker-registry

通过获取官方registry镜像来运行。

$ docker run -d -p 5000:5000 --restart=always -v /opt/data/registry:/var/lib/registry --name registry registry

这将使用官方的registry镜像来启动私有仓库。-v参数,将上传的镜像文件放到指定的本地/opt/data/registry目录,默认情况下,仓库会被创建在容器的/var/lib/registry目录下

2、docker push 上传镜像

创建好私有仓库之后,就可以使用docker tag来标记一个镜像,然后推送它到仓库。例如私有仓库地址为127.0.0.1:5000

先在本机查看已有的镜像。

$ docker images

REPOSITORY    TAG    IMAGE    IDCREATED    VIRTUAL    SIZE

ubuntu    latest   ba5877dc9bec6    weeks ago    192.7 MB

 

使用docker tagubuntu:latest这个镜像标记为127.0.0.1:5000/ubuntu:latest

格式为docker tag IMAGE[:TAG] [REGISTRY_HOST[:REGISTRY_PORT]/]REPOSITORY[:TAG]

docker tag ubuntu:latest 127.0.0.1:5000/ubuntu:latest

docker images

 

使用docker push上传标记的镜像。

docker push 127.0.0.1:5000/ubuntu:latest

 

3、curl 搜索镜像

curl查看仓库中的镜像。可以看到{"repositories":["ubuntu"]},表明镜像已经被成功上传了。或者进入私有仓库容器/var/lib/registry目录下查看。

curl 127.0.0.1:5000/v2/_catalog

{"repositories":["ubuntu"]}

4、docker pull 下载镜像

先删除已有镜像,再尝试从私有仓库中下载这个镜像。

$ docker image rm 127.0.0.1:5000/ubuntu:latest

 

docker pull 127.0.0.1:5000/ubuntu:latest

 

$ docker images

 

5、向其他主机的私有仓库推送

如果你不想使用127.0.0.1:5000作为仓库地址,比如想让本网段的其他主机也能把镜像推送到私有仓库。你就得把例如192.168.199.100:5000这样的内网地址作为私有仓库地址,这时你会发现无法成功推送镜像。

这是因为 Docker 默认不允许非HTTPS方式推送镜像。我们可以通过 Docker 的配置选项来取消这个限制,或者查看下一节配置能够通过HTTPS访问的私有仓库。

Ubuntu 14.04, Debian 7 Wheezy

对于使用upstart的系统而言,编辑/etc/default/docker文件,在其中的DOCKER_OPTS 中增加如下内容:

DOCKER_OPTS="--registry-mirror=https://registry.docker-cn.com --insecure-registries=192.168.199.100:5000"

重新启动服务。

$ sudo service docker restart

Ubuntu 16.04+, Debian 8+, centos 7

对于使用systemd的系统,请在/etc/docker/daemon.json中写入如下内容(如果文件不存在请新建该文件)

{

"registry-mirror": [

 "https://registry.docker-cn.com"

],

"insecure-registries": [

 "192.168.199.100:5000"

]

}

注意:该文件必须符合json规范,否则 Docker 将不能启动。

其他

对于 Docker for Windows 、 Docker for Mac 在设置中编辑daemon.json增加和上边一样的字符串即可。

私有仓库高级配置

上一节我们搭建了一个具有基础功能的私有仓库,本小节我们来使用Docker Compose搭建一个拥有权限认证、TLS 的私有仓库。

新建一个文件夹,以下步骤均在该文件夹中进行。

准备站点证书

如果你拥有一个域名,国内各大云服务商均提供免费的站点证书。你也可以使用openssl自行签发证书。

这里假设我们将要搭建的私有仓库地址为docker.domain.com,下面我们介绍使用openssl自行签发docker.domain.com的站点 SSL 证书。

 

第一步创建CA私钥。

$ openssl genrsa -out "root-ca.key" 4096

 

第二步利用私钥创建CA根证书请求文件。

$ openssl req \

 -new -key "root-ca.key" \

 -out "root-ca.csr" -sha256 \

 -subj '/C=CN/ST=Shanxi/L=Datong/O=Your Company Name/CN=Your Company Name Doc

ker Registry CA'

以上命令中-subj参数里的/C表示国家,CN;/ST表示省;/L表示城市或者地区;/O表示组织名;/CN通用名称。

 

第三步配置CA根证书,新建root-ca.cnf

[root_ca]

basicConstraints = critical,CA:TRUE,pathlen:1

keyUsage = critical, nonRepudiation, cRLSign, keyCertSign

subjectKeyIdentifier=hash

 

第四步签发根证书。

$ openssl x509 -req-days 3650-in "root-ca.csr" \

-signkey "root-ca.key" -sha256 -out "root-ca.crt" \

-extfile "root-ca.cnf" -extensions \

root_ca

 

第五步生成站点SSL私钥。

$ openssl genrsa -out "docker.domain.com.key" 4096

 

第六步使用私钥生成证书请求文件。

$ openssl req -new -key "docker.domain.com.key" -out "site.csr" -sha256 \

 -subj '/C=CN/ST=Shanxi/L=Datong/O=Your Company Name/CN=docker.domain.com'

 

第七步配置证书,新建site.cnf文件。

[server]

authorityKeyIdentifier=keyid,issuer

basicConstraints = critical,CA:FALSE

extendedKeyUsage=serverAuth

keyUsage = critical, digitalSignature, keyEncipherment

subjectAltName = DNS:docker.domain.com, IP:127.0.0.1

subjectKeyIdentifier=hash

 

第八步签署站点SSL证书。

$ openssl x509 -req -days 750 -in "site.csr" -sha256 \

 -CA "root-ca.crt" -CAkey "root-ca.key"-CAcreateserial \

 -out "docker.domain.com.crt" -extfile "site.cnf" -extensions server

 

这样已经拥有了docker.domain.com的网站 SSL 私钥docker.domain.com.key和 SSL 证书 docker.domain.com.crt

新建ssl文件夹并将docker.domain.com.keydocker.domain.com.crt这两个文件移入,删除其他文件。

配置私有仓库

私有仓库默认的配置文件位于/etc/docker/registry/config.yml,我们先在本地编辑config.yml,之后挂载到容器中。

version: 0.1

log:

accesslog:

 disabled: true

level: debug

formatter: text

fields:

 service: registry

 environment: staging

storage:

delete:

 enabled: true

cache:

 blobdescriptor: inmemory

filesystem:

 rootdirectory: /var/lib/registry

auth:

htpasswd:

 realm: basic-realm

 path: /etc/docker/registry/auth/nginx.htpasswd

http:

addr: :443

host: https://docker.domain.com

headers:

 X-Content-Type-Options: [nosniff]

http2:

 disabled: false

tls:

 certificate: /etc/docker/registry/ssl/docker.domain.com.crt

 key: /etc/docker/registry/ssl/docker.domain.com.key

health:

storagedriver:

 enabled: true

 interval: 10s

threshold: 3

生成 http 认证文件

$ mkdir auth

$ docker run --rm \

 --entrypoint htpasswd \

 registry \

 -Bbn username password > auth/nginx.htpasswd

将上面的usernamepassword替换为你自己的用户名和密码。

编辑docker-compose.yml

version: '3'

services:

registry:

 image: registry

 ports:

- "443:443"

 volumes:

- ./:/etc/docker/registry

- registry-data:/var/lib/registry

volumes:

registry-data:

修改 hosts

编辑/etc/hosts 

docker.domain.com 127.0.0.1

启动

$ docker-compose up -d

这样我们就搭建好了一个具有权限认证、TLS 的私有仓库,接下来我们测试其功能是否正常。

测试私有仓库功能

登录到私有仓库。

$ docker login docker.domain.com

 

尝试推送、拉取镜像。

$ docker pull ubuntu:17.10

$ docker tag ubuntu:17.10 docker.domain.com/username/ubuntu:17.10

$ docker push docker.domain.com/username/ubuntu:17.10

$ docker image rm docker.domain.com/username/ubuntu:17.10

$ docker pull docker.domain.com/username/ubuntu:17.10

 

如果我们退出登录,尝试推送镜像。

$ docker logout docker.domain.com

$ docker push docker.domain.com/username/ubuntu:17.10

no basic auth credentials

发现会提示没有登录,不能将镜像推送到私有仓库中。

注意事项

如果你本机占用了443端口,你可以配置 Nginx 代理,这里不再赘述。