About-简介

参考文档
官方docker-doc

Docker是使用go语言基于LINUX内核的cgroup,namespace以及AUFS 类的 Union FS 等技术,对进程进行封装隔离的一种操作系统层面的虚拟化技术,由于隔离的进程独立于宿主和其它的隔离的进程,因此也称其为容器。

Docker和传统虚拟化技术的对比

kvm-vs-docker
kvm-vs-docker

为什么要使用 Docker

  • 更高效的利用系统资源
    • 由于Docker工作在进程级别,不需要进行硬件虚拟以及运行完整操作系统等额外开销,所以Docker对系统资源的利用率更高。相比虚拟机技术,一个相同配置的主机,往往可以运行更多数量的应用。
  • 更快速的启动时间
    • 传统的虚拟机技术启动应用服务往往需要数分钟,而 Docker容器应用,由于直接运行于宿主内核,无需启动完整的操作系统,因此可以做到秒级、甚至毫秒级的启动时间。大大的节约了开发、测试、部署的时间。
  • 一致的运行环境
    • 由于开发环境、测试环境、生产环境不一致,导致有些 bug 并未在开发过程中被发现。Docker 的镜像提供了除内核外完整的运行时环境,确保了应用运行环境一致性,从而不会再出现 “这段代码在我机器上没问题啊” 这类问题。
  • 持续交付和部署
    • 一次创建,多次运行。通过定制应用镜像来实现持续集成、持续交付、部署。开发人员可以通过 Dockerfile 来进行镜像构建,并结合持续集成(Continuous Integration)系统进行集成测试,而运维人员则可以直接在生产环境中快速部署该镜像,甚至结合 持续部署(Continuous Delivery/Deployment) 系统进行自动部署。而且使用 Dockerfile 使镜像构建透明化,不仅仅开发团队可以理解应用运行环境,也方便运维团队理解应用运行所需条件,帮助更好的生产环境中部署该镜像。
  • 更轻松的迁移
    • Docker 确保了执行环境的一致性,无论是物理机、虚拟机、公有云、私有云,甚至是笔记本,其运行结果是一致的。因此可以很轻易迁移到任意上,而不用担心运行环境的变化导致应用无法正常运行的情况。
  • 更轻松的维护和扩展
    • Docker 使用的分层存储以及镜像的技术,Docker 团队同各个开源项目团队一起维护了一大批高质量的官方镜像,既可以直接在生产环境使用,又可以作为基础进一步定制。

Docker的基本概念

Docker 镜像

我们都知道,操作系统分为内核和用户空间。对于 Linux 而言,内核启动后,会挂载根文件系统为其提供用户空间支持。而 Docker 镜像(Image),就相当于是一个根文件系统。Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。

分层存储

因为镜像包含操作系统完整的根文件系统,其体积往往是庞大的,因此在 Docker 设计时,就充分利用 Union FS 的技术,将其设计为分层存储的架构。所以严格来说,镜像并非是像一个 ISO 那样的打包文件,镜像只是一个虚拟的概念,其实际体现并非由一个文件组成,而是由一组文件系统组成,或者说,由多层文件系统联合组成。
镜像构建时,会一层层构建,前一层是后一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。比如,删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除。在最终容器运行的时候,虽然不会看到这个文件,但是实际上该文件会一直跟随镜像。因此,在构建镜像的时候,需要额外小心,每一层尽量只包含该层需要添加的东西,任何额外的东西应该在该层构建结束前清理掉。

分层存储的特征还使得镜像的复用、定制变的更为容易。甚至可以用之前构建好的镜像作为基础层,然后进一步添加新的层,以定制自己所需的内容,构建新的镜像。

Docker 容器

镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的类和实例一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。

容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的命名空间。因此容器可以拥有自己的根文件系统、自己的网络配置、自己的进程空间,甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样。这种特性使得容器封装的应用比直接在宿主运行更加安全。

前面讲过镜像使用的是分层存储,容器也是如此。每一个容器运行时,是以镜像为基础层,在其上创建一个当前容器的存储层,我们可以称这个为容器运行时读写而准备的存储层为容器存储层。

容器存储层的生存周期和容器一样,容器消亡时,容器存储层也随之消亡。因此,任何保存于容器存储层的信息都会随容器删除而丢失。

按照 Docker 最佳实践的要求,容器不应该向其存储层内写入任何数据,容器存储层要保持无状态化。所有的文件写入操作,都应该使用 数据卷(Volume)、或者绑定宿主目录,在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高。

数据卷的生存周期独立于容器,容器消亡,数据卷不会消亡。因此,使用数据卷后,容器可以随意删除、重新运行,数据却不会丢失。

Docker Registry

Docker Registry 提供了镜像的集中的存储、分发功能。一个 Docker Registry 中可以包含多个仓库(Repository);每个仓库可以包含多个标签(Tag);通过 <仓库名>:<标签> 的格式来指定具体是这个软件哪个版本的镜像。Docker Registry 分为公有服务和私有服务,我们可以搭建一个基于本地的registry

Docker 操作

安装docker

例如在Ubuntu中,可以直接运行:

1
apt-get install docker

配置镜像加速

由于国内访问docker原站点非常困难,国内的云服务提供商提供了加速功能。我们使用阿里云进行docker加速
地址为: https://dev.aliyun.com/search.html

1
2
3
sed -i "s|ExecStart=/usr/bin/dockerd|ExecStart=/usr/bin/dockerd --registry-mirror=https://fz5yth0r.mirror.aliyuncs.com|g" /usr/lib/systemd/system/docker.service
systemctl daemon-reload
systemctl restart docker

使用镜像

  • 获取镜像

    1
    docker pull [OPTIONS] NAME[:TAG|@DIGEST]
  • 列出镜像

    1
    2
    3
    4
    5
    6
    7
    8
    9
    [root@node1 docker]# docker images
    #仓库名 #标签 #镜像ID #创建时间 #大小
    REPOSITORY TAG IMAGE ID CREATED SIZE
    centos latest 98d35105a391 7 days ago 192 MB
    docker images -a //使用-a参数来显示中间层镜像。
    //按照指定格式输出
    docker images --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}"
  • Docker 运行

    1
    2
    //使用nginx 镜像启动一个容器,命名为 webserver,并且把容器的80端口映射在宿主机的80端口。
    docker run --name webserver -d -p 80:80 nginx
  • Docker commit

    • 我们修改了容器的存储层,使用docker diff 查看文件的改动
    • 定制好页面之后,保存为镜像,当我们运行一个容器的时候(如果不使用卷的话),我们做的任何文件修改都会被记录于容器存储层里。
    • 而 Docker 提供了一个 docker commit 命令,可以将容器的存储层保存下来成为镜像。换句话说,就是在原有镜像的基础上,再叠加上容器的存储层,并构成新的镜像。

      1
      2
      3
      4
      5
      docker commit \
      --author "austin <xxx@163.com>" \
      --message "修改初始网页" \
      webserver \
      nginx:v2
- 还可以用 docker history 具体查看镜像内的历史记录,如果比较 nginx:latest 的历史记录

- 慎用 docker commit

观察之前的 docker diff webserver 的结果,你会发现除了真正想要修改的 /usr/share/nginx/html/index.html 文件外,还有很多文件被改动或添加了。如果是安装软件包、编译构建,那会有大量的无关内容被添加进来,将会导致镜像极为臃肿。

使用 docker commit 意味着除了制作镜像的人知道执行过什么命令、怎么生成的镜像,别人根本无从得知。而且,即使是这个制作镜像的人,过一段时间后也无法记清具体在操作的。

如果使用 docker commit 制作镜像,由于只在当前层操作,后期修改的话,每一次修改都会让镜像更加臃肿一次,所删除的上一层的东西并不会丢失,会一直如影随形的跟着这个镜像,
即使根本无法访问到?。这会让镜像更加臃肿。

docker commit 命令除了学习之外,还有一些特殊的应用场合,比如被入侵后保存现场等。但是,不要使用 docker commit 定制镜像,定制行为应该使用 Dockerfile 来完成。

Dockerfile

Dockerfile是一个文本文件,用来来构建、定制镜像。之前使用 docker commit 提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。
使用docker file 定制nginx镜像

1
2
FROM nginx
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
  • FROM

    • 指定一个基础镜像,可以直接拿来使用的服务类的镜像,如ubuntu、debian、centos、fedora、alpine、nginx、redis、mongo、mysql、httpd、php、tomcat 等。

    • 指定一个空白镜像

      1
      2
      FROM scratch
      RUN ...
  • RUN

    • RUN 指令是用来执行命令行命令。其格式有两种。

      • SHELL格式:

        1
        RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
      • exec 格式:

        1
        RUN ["可执行文件", "参数1", "参数2"]
    • 在使用shell模式编写时,不建议每个命令都写一层RUN,每一个 RUN 的行为,就和刚才我们手工建立镜像的过程一样:新建立一层,在其上执行这些命令,执行结束后,commit 这一层的修改,构成新的镜像。Union FS 是有最大层数限制的,比如 AUFS,曾经是最大不得超过 42 层,现在是不得超过 127 层。

    • 正确dockerfile写法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      FROM debian:jessie
      RUN buildDeps='gcc libc6-dev make' \
      && apt-get update \
      && apt-get install -y $buildDeps \
      && wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.tar.gz" \
      && mkdir -p /usr/src/redis \
      && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
      && make -C /usr/src/redis \
      && make -C /usr/src/redis install \
      && rm -rf /var/lib/apt/lists/* \
      && rm redis.tar.gz \
      && rm -r /usr/src/redis \
      && apt-get purge -y --auto-remove $buildDeps
- 不建议的写法:
1
2
3
4
5
6
7
8
9
FROM debian:jessie
RUN apt-get update
RUN apt-get install -y gcc libc6-dev make
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.tar.gz"
RUN mkdir -p /usr/src/redis
RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1
RUN make -C /usr/src/redis
RUN make -C /usr/src/redis install
  • 构建镜像

    1
    docker build -t nginx:v3 .

    镜像构建上下文(Context)

  • COPY

    • SHELL格式:

      1
      COPY <源路径>... <目标路径>
      • exec 格式:
        1
        2
        3
        COPY ["<源路径1>",... "<目标路径>"]
        ```
        - 源路径"可以是多个,也可以是通配符,其通配符规则要满足 Go 的 filepath.Match 规则,如:

      COPY hom* /mydir/
      COPY hom?.txt /mydir/

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      > "目标路径"可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。
      使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候。
      - **ADD**
      "源路径"可以是一个 URL,这种情况下,Docker 引擎会试图去下载这个链接的文件放到"目标路径"。下载后的文件权限自动设置为600。
      如果这并不是想要的权限,那么还需要增加额外的一层 RUN 进行权限调整。
      如果"源路径"为一个 tar.gzip,tar.bzip2,tar.xz的文件,ADD 指令将会自动解压缩这个压缩文件到"目标路径"去。

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

      1
      2
      3
      4
      5
      6
      7
      在 Docker 官方的最佳实践文档中要求,尽可能的使用 COPY,因为 COPY 的语义很明确,就是复制文件而已,而 ADD 则包含了更复杂的功能,其行为也不一定很清晰。最适合使用 ADD 的场合,就是所提及的需要自动解压缩的场合。
      - **CMD**
      - CMD 指令的格式和 RUN 相似,也是两种格式:
      - SHELL格式:

      CMD <命令>

      1
      - exec 格式:

      CMD [“可执行文件”, “参数1”, “参数2”…]

      1
      - 参数列表格式

      CMD [“参数1”, “参数2”…]。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数。
      > 之前介绍容器的时候曾经说过,Docker 不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。CMD 指令就是用于指定默认的容器主进程的启动命令的。
      > 在运行时可以指定新的命令来替代镜像设置中的这个默认命令,镜像默认的 CMD 是 `/bin/bash` ,如果我们直接 `docker run -it centos` 的话,会直接进入 `bash`。我们也可以在运行时指定运行别的命令,如 `docker run -it ubuntu cat /etc/os-release`。这就是用 `cat /etc/os-release`命令替换了默认的 `/bin/bash `命令了,输出了系统版本信息。
      在指令格式上,一般推荐使用 exec 格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 ",而不要使用单引号。
      如果使用 shell 格式的话,实际的命令会被包装为sh -c 的参数的形式进行执行。比如:

      CMD echo $HOME

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

      CMD [ “sh”, “-c”, “echo $HOME” ]

      1
      2
      3
      4
      5
      这就是为什么我们可以使用环境变量的原因,因为这些环境变量会被 shell 进行解析处理。
      Docker 不是虚拟机,容器中的应用都应该以前台执行,而不是像虚拟机、物理机里面那样,用 upstart/systemd 去启动后台服务,容器内没有后台服务的概念。
      一些初学者将 CMD 写为:

      CMD service nginx start

      1
      2
      3
      4
      5
      6
      7
      8
      然后发现容器执行后就立即退出了。甚至在容器内去使用 `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;”]

      1
      2
      3
      4
      5
      - **ENTRYPOINT**
      > `ENTRYPOINT` 的目的和 `CMD` 一样,都是在指定容器启动程序及参数。`ENTRYPOINT` 在运行时也可以替代,不过比 `CMD` 要略显繁琐,需要通过 `docker run` 的参数 `--entrypoint` 来指定。
      当指定了 ENTRYPOINT 后,`CMD` 的含义就发生了改变,不再是直接的运行其命令,而是将 `CMD` 的内容作为参数传给 `ENTRYPOINT` 指令,换句话说实际执行时,将变为:

      1
      2
      3
      场景一:让镜像变成像命令一样使用
      假设我们需要一个得知自己当前公网 IP 的镜像,那么可以先用 CMD 来实现:

      mkdir ~/myip/
      cd ~/myip/
      tee Dockerfile <<’EOF’
      FROM centos
      RUN yum -y install wget \
      && wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo \
      &&wget -O /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo \
      &&yum -y install curl
      CMD [ “curl”, “-s”, “http://ip.cn“ ]
      EOF
      docker build -t myip .

      1
      2
      启动一个容器测试

      [root@node1 myip]# docker run myip
      当前 IP:123.117.85.77 来自:北京市 联通

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

      [root@node1 myip]# docker rum myip -i
      unknown shorthand flag: ‘i’ in -i
      See ‘docker –help’.

      1
      2
      3
      4
      跟在镜像名后面的是 command,运行时会替换 CMD 的默认值。因此这里的 -i 替换了原来的 CMD,而不是添加在原来的 curl -s http://ip.cn 后面。而 -i 根本不是命令,所以自然找不到。
      如果我们希望加入 -i 这参数,我们就必须重新完整的输入这个命令:

      [root@node1 myip]# docker run myip curl -s http://ip.cn -i
      HTTP/1.1 200 OK
      Server: nginx/1.10.0 (Ubuntu)
      Date: Fri, 24 Mar 2017 02:21:43 GMT
      Content-Type: text/html; charset=UTF-8
      Transfer-Encoding: chunked
      Connection: keep-alive

      当前 IP:123.117.85.77 来自:北京市

      1
      2
      而使用 ENTRYPOINT 就可以给 docker 传参,修改docker镜像

      cd ~/myip/
      tee Dockerfile <<’EOF’
      FROM centos
      RUN yum -y install wget \
      && wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo \
      &&wget -O /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo \
      &&yum -y install curl
      ENTRYPOINT [ “curl”, “-s”, “http://ip.cn“ ]
      EOF
      docker build -t myip .

      1
      2
      再次运行

      [root@node1 myip]# docker run myip
      当前 IP:123.117.85.77 来自:北京市

[root@node1 myip]# docker run myip -i
HTTP/1.1 200 OK
Server: nginx/1.10.0 (Ubuntu)
Date: Fri, 24 Mar 2017 02:24:42 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive

当前 IP:123.117.85.77 来自:北京市

1
2
3
场景二:应用运行前的准备工作
[redis的官方dockerfile]( https://github.com/docker-library/redis/blob/master/3.2/alpine/Dockerfile)
FROM alpine:3.5 ... RUN addgroup -S redis && adduser -S -G redis redis ... ENTRYPOINT ["docker-entrypoint.sh"] EXPOSE 6379 CMD [ "redis-server" ]
1
2
可以看到其中为了 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 "$@"
1
2
该脚本的内容就是根据 CMD 的内容来判断,如果是 `redis-server` 的话,则切换到 `redis` 用户身份启动服务器,否则依旧使用 `root `身份执行。比如:
$ docker run -it redis id uid=0(root) gid=0(root) groups=0(root)
1
2
3
4
5
6
- **ENV 设置环境变量**
格式有两种:
- `ENV <key> <value>`
- `ENV <key1>=<value1> <key2>=<value2>...`
这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN,还是运行时的应用,都可以直接使用这里定义的环境变量。
ENV VERSION=1.0 DEBUG=on \ NAME="Happy Feet"
1
2
3
4
5
6
7
- **ARG 构建参数**
- 格式:
`ARG <参数名>[=<默认值>]`
构建参数和 ENV 的效果一样,都是设置环境变量。所不同的是,ARG 所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的。不建议 ARG 保存密码之类的信息,因为 docker history 还是可以看到所有值的。
- **VOLUME 定义匿名卷**
- 格式:
VOLUME ["<路径1>", "<路径2>"...] VOLUME <路径>
1
> 之前我们说过,容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中,后面的章节我们会进一步介绍 Docker 卷的概念。为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 Dockerfile 中,我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。
VOLUME /data
1
也可以运行时覆盖这个挂载设置。比如:
docker run -d -v mydata:/data xxxx
1
2
3
4
在这行命令中,就使用了 mydata 这个命名卷挂载到了 /data 这个位置,替代了 Dockerfile 中定义的匿名卷的挂载配置。
- **EXPOSE 声明端口**
- 格式:
EXPOSE <端口1> [<端口2>...]
1
2
3
4
5
6
7
8
9
10
EXPOSE 指令是声明运行时容器提供服务端口,在运行时并不会因为这个声明应用就会开启这个端口的服务。
- 在 Dockerfile 中写入这样的声明有两个好处:
- 帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射
- 在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口。
> 此外,在早期 Docker 版本中还有一个特殊的用处。以前所有容器都运行于默认桥接网络中,因此所有容器互相之间都可以直接访问,这样存在一定的安全性问题。于是有了一个 Docker 引擎参数 --icc=false,当指定该参数后,容器间将默认无法互访,除非互相间使用了 --links 参数的容器才可以互通,并且只有镜像中 EXPOSE 所声明的端口才可以被访问。这个 --icc=false 的用法,在引入了 docker network 后已经基本不用了,通过自定义网络可以很轻松的实现容器间的互联与隔离。
- **WORKDIR 指定工作目录**
- 格式:
WORKDIR <工作目录路径>
1
2
使用 WORKDIR 指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR 会帮你建立目录。
之前提到一些初学者常犯的错误是把 Dockerfile 等同于 Shell 脚本来书写,这种错误的理解还可能会导致出现下面这样的错误:
RUN cd /app RUN echo "hello" > world.txt
1
2
3
4
5
6
7
> 如果将这个 Dockerfile 进行构建镜像运行后,会发现找不到 /app/world.txt 文件,或者其内容不是hello。原因其实很简单,在 Shell 中,连续两行是同一个进程执行环境,因此前一个命令修改的内存状态,会直接影响后一个命令;而在 Dockerfile 中,这两行 RUN 命令的执行环境根本不同,是两个完全不同的容器。这就是对 Dokerfile 构建分层存储的概念不了解所导致的错误。
> 之前说过每一个 `RUN `都是启动一个容器、执行命令、然后提交存储层文件变更。第一层 `RUN cd /app `的执行仅仅是当前进程的工作目录变更,一个内存上的变化而已,其结果不会造成任何文件变更。而到第二层的时候,启动的是一个全新的容器,跟第一层的容器更完全没关系,自然不可能继承前一层构建过程中的内存变化。
因此如果需要改变以后各层的工作目录的位置,那么应该使用 `WORKDIR` 指令。
- **USER 指定当前用户**
- 格式:
USER <用户名>
1
2
3
USER 指令和 WORKDIR 相似,都是改变环境状态并影响以后的层。WORKDIR 是改变工作目录,USER 则是改变之后层的执行 RUN, CMD 以及 ENTRYPOINT 这类命令的身份。
当然,和 WORKDIR 一样,USER 只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。
RUN groupadd -r redis && useradd -r -g redis redis USER redis RUN [ "redis-server" ]
1
2
3
- **HEALTHCHECK 健康检查**
- 格式:
HEALTHCHECK [选项] CMD <命令>:设置检查容器健康状况的命令 HEALTHCHECK NONE:如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令
1
2
3
4
5
6
7
8
9
HEALTHCHECK 指令是告诉 Docker 应该如何进行判断容器的状态是否正常,这是 Docker 1.12 引入的新指令。
在没有 HEALTHCHECK 指令前,Docker 引擎只可以通过容器内主进程是否退出来判断容器是否状态异常。很多情况下这没问题,但是如果程序进入死锁状态,或者死循环状态,应用进程并不退出,但是该容器已经无法提供服务了。在 1.12 以前,Docker 不会检测到容器的这种状态,从而不会重新调度,导致可能会有部分容器已经无法提供服务了却还在接受用户请求。
而自 1.12 之后,Docker 提供了 HEALTHCHECK 指令,通过该指令指定一行命令,用这行命令来判断容器主进程的服务状态是否还正常,从而比较真实的反应容器实际状态。
当在一个镜像指定了 HEALTHCHECK 指令后,用其启动容器,初始状态会为 starting,在 HEALTHCHECK 指令检查成功后变为 healthy,如果连续一定次数失败,则会变为 unhealthy。
HEALTHCHECK 支持下列选项:
--interval=<间隔>:两次健康检查的间隔,默认为 30 秒; --timeout=<时长>:健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认 30 秒; --retries=<次数>:当连续失败指定次数后,则将容器状态视为 unhealthy,默认 3 次。
1
2
3
4
5
6
> 和 CMD, ENTRYPOINT 一样,HEALTHCHECK 只可以出现一次,如果写了多个,只有最后一个生效。
在 HEALTHCHECK [选项] CMD 后面的命令,格式和 ENTRYPOINT 一样,分为 shell 格式,和 exec 格式。命令的返回值决定了该次健康检查的成功与否:0:成功;1:失败;2:保留,不要使用这个值。
假设我们有个镜像是个最简单的 Web 服务,我们希望增加健康检查来判断其 Web 服务是否在正常工作,我们可以用 curl 来帮助判断,其 Dockerfile 的 HEALTHCHECK 可以这么写:
mkdir ~/nginxcheck cd ~/nginxcheck vim Dockerfile 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 docker build -t myweb:v1 .
1
2
3
4
这里我们设置了每 5 秒检查一次(这里为了试验所以间隔非常短,实际应该相对较长),如果健康检查命令超过 3 秒没响应就视为失败,并且使用 curl -fs http://localhost/ || exit 1 作为健康检查命令。
启动测试
docker run -d --name myweb -p 80:82 myweb:v1
1
2
查看
[root@node1 nginxcheck]# docker ps -f name=myweb CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES beb18dcc15fe myweb:v1 "nginx -g 'daemon ..." 22 seconds ago Up 21 seconds (healthy) 80/tcp, 443/tcp, 0.0.0.0:80->82/tcp myweb
1
2
3
4
5
6
7
如果健康检查连续失败超过了重试次数,状态就会变为 (unhealthy)。
健康检查命令的输出(包括 stdout 以及 stderr)都会被存储于健康状态里,可以用 docker inspect 来查看。
- **ONBUILD 为他人做嫁衣**
ONBUILD 是一个特殊的指令,它后面跟的是其它指令,比如 RUN, COPY 等,而这些指令,在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。
例如:
FROM node:slim RUN "mkdir /app" WORKDIR /app ONBUILD COPY ./package.json /app ONBUILD RUN [ "npm", "install" ] ONBUILD COPY . /app/ CMD [ "npm", "start" ]
1
在构建基础镜像的时候,包含ONBUILD这三行并不会被执行,但是当把这个镜像作为基础镜像构建时,这三行就会执行。
#首先构建基础镜像 docker build -t my-node . #在其他项目需要使用这个Dockerfile制作的镜像作为基础镜像时,直接写Dockerfile,基础镜像的三行会在子Dockerfile中执行 FROM my-node
1
2
3
4
- **其他制作镜像方法**
除了标准的使用 Dockerfile 生成镜像的方法外,由于各种特殊需求和历史原因,还提供了一些其它方法用以生成镜像。
- 从 rootfs 压缩包导入
格式:docker import [选项] <文件>|<URL>|- [<仓库名>[:<标签>]]
1
2
3
压缩包可以是本地文件、远程 Web 文件,甚至是从标准输入中得到。压缩包将会在镜像 / 目录展开,并直接作为镜像第一层提交。
比如我们想要创建一个 OpenVZ 的 Ubuntu 14.04 模板的镜像:
docker import \ http://download.openvz.org/template/precreated/ubuntu-14.04-x86_64-minimal.tar.gz \ openvz/ubuntu:14.04
1
2
3
4
5
6
7
这条命令自动下载了 ubuntu-14.04-x86_64-minimal.tar.gz 文件,并且作为根文件系统展开导入,并保存为镜像 openvz/ubuntu:14.04。
- docker save 和 docker load
Docker 还提供了 docker load 和 docker save 命令,用以将镜像保存为一个 tar 文件,然后传输到另一个位置上,再加载进来。这是在没有 Docker Registry 时的做法,现在已经不推荐,镜像迁移应该直接使用 Docker Registry,无论是直接使用 Docker Hub 还是使用内网私有 Registry 都可以。
使用 docker save 命令可以将镜像保存为归档文件。
docker pull alpine docker save alpine |gzip > alpine.tar.gz
1
2
然后我们将 alpine-latest.tar.gz 文件复制到了到了另一个机器上,可以用下面这个命令加载镜像:
docker load -i alpine-latest.tar.gz
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
## 使用容器
### 启动容器
启动容器有两种方式,一种是基于镜像新建一个容器并启动,另外一个是将在终止状态(stopped)的容器重新启动。
- **容器的启动流程**
当利用 docker run 来创建容器时,Docker 在后台运行的标准操作包括:
1. 检查本地是否存在指定的镜像,不存在就从公有仓库下载
1. 利用镜像创建并启动一个容器
1. 分配一个文件系统,并在只读的镜像层外面挂载一层可读写层
1. 从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去
1. 从地址池配置一个 ip 地址给容器
1. 执行用户指定的应用程序
1. 执行完毕后容器被终止
- **使用docker run启动**
下面的命令输出一个 “Hello World”,之后终止容器。
docker run centos /bin/echo 'Hello world'
1
2
启动一个前台的bash进程,-t 选项让Docker分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上,-i 则让容器的标准输入保持打开。
docker run -t -i centos /bin/bash
1
- **使用docker start启动已终止容器**
docker start 容器名字或ID
1
查看容器的资源占用,只有一个bash进程
[root@94b57792acbf /]# ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.1 11768 1932 ? Ss 07:19 0:00 /bin/bash root 56 0.0 0.1 47440 1672 ? R+ 07:27 0:00 ps aux
1
2
3
4
5
- **让容器以后台进程模式运行**
更多的时候,需要让 Docker在后台运行而不是直接把执行命令的结果输出在当前宿主机下。此时,可以通过添加 -d 参数来实现。
使用例子来体验区别:不使用 -d 参数:
[root@node1 ~]# docker run centos /bin/bash -c "while true; do echo hello world; sleep 1; done" hello world hello world hello world hello world
1
2
3
所有的输出都输出到宿主机。
加-d参数
[root@node1 ~]# docker run -d centos /bin/bash -c "while true; do echo hello world; sleep 1; done" a8e42575a9ff340c65f0023c77926b0f80051f53ade13c38847f0e8a6319ee63
1
此时容器会在后台运行并不会把输出的结果打印到宿主机上,可以使用 docker logs 查看
[root@node1 ~]# docker logs a8e42575a9ff
1
2
3
4
> 注: 容器是否会长久运行,是和docker run指定的命令有关,和 -d 参数无关。
- **终止容器**
docker stop {CONTAINER ID| NAMES}
1
2
3
4
对于上一章节中只启动了一个bash的容器,用户通过 exit 命令或 Ctrl+d 来退出终端时,所创建的容器立刻终止。
- **进入容器**
attach 命令
[root@node1 ~]# docker run -dit centos /bin/bash 14c0b4a935f57317f25111aa55beed0f3329afe60871ca06c44a12acc4172140 [root@node1 ~]# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 14c0b4a935f5 centos "/bin/bash" 25 seconds ago Up 24 seconds dreamy_wiles [root@node1 ~]# docker attach dreamy_wiles
1
2
3
4
> 但是使用 attach 命令有时候并不方便。当多个窗口同时 attach 到同一个容器的时候,所有窗口都会同步显示。当某个窗口因命令阻塞时,其他窗口也无法执行操作了,使用attach命令退出使用ctrl+p+q 退出不影响容器运行。
- **nsenter 命令**
安装命令
yum -y install util-linux
1
获取容器PID
PID=$(docker inspect --format "{{ .State.Pid }}" dreamy_wiles)
1
进入容器
nsenter --target $PID --mount --uts --ipc --net --pid
1
2
3
- **导入导出容器**
导出容器快照
docker export 7691a814370e > centos.tar
1
2
导入容器快照
cat centos.tar | sudo docker import - myimages/centos:v1.0
1
2
删除容器
docker rm {CONTAINER ID| NAMES}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
### Docer 数据管理
Docker 内部管理数据主要有两种方式:
- 数据卷(Data volumes)
- 数据卷容器(Data volume containers)
- **数据卷**
数据卷是一个可供一个或多个容器使用的特殊目录,它绕过 UFS,可以提供很多有用的特性:
- 数据卷可以在容器之间共享和重用
- 对数据卷的修改会立马生效
- 对数据卷的更新,不会影响镜像
- 数据卷默认会一直存在,即使容器被删除
> 注意:数据卷的使用,类似于 Linux 下对目录或文件进行 mount,镜像中的被指定为挂载点的目录中的文件会隐藏掉,能显示看的是挂载的数据卷。
使用 `-v` 标记也可以指定挂载一个本地主机的目录到容器中去。
docker run -dit --name test -v /tmp:/opt:ro centos /bin/bash
1
2
3
- **数据卷容器**
数据卷容器,其实就是一个正常的容器,专门用来提供数据卷供其它容器挂载的。
docker run -dit -v /dbdata --name dbdata centos /bin/bash [root@node1 ~]# docker attach dbdata [root@a9642e6adf7b /]# touch /dbdata/{1..10}.txt
1
在其他容器中使用 --volumes-from 来挂载 dbdata 容器中的数据卷。
docker run -dit --volumes-from dbdata --name db1 centos /bin/bash docker run -dit --volumes-from dbdata --name db2 centos /bin/bash
1
验证
[root@node1 ~]# docker attach db1 [root@87f964ea0f31 /]# ls /dbdata/ 1.txt 10.txt 2.txt 3.txt 4.txt 5.txt 6.txt 7.txt 8.txt 9.txt
1
2
3
4
5
6
7
8
9
> 注意:使用 --volumes-from 参数所挂载数据卷的容器自己并不需要保持在运行状态。
如果删除了挂载的容器(包括 dbdata、db1 和 db2),数据卷并不会被自动删除。如果要删除一个数据卷,必须在删除最后一个还挂载着它的容器时使用 docker rm -v 命令来指定同时删除关联的容器。 这可以让用户在容器之间升级和移动数据卷。
- **利用数据卷容器来备份、恢复、迁移数据卷**
可以利用数据卷对其中的数据进行进行备份、恢复和迁移。
`备份`
首先使用 --volumes-from 标记来创建一个加载 dbdata 容器卷的容器,并从主机挂载/opt/backup到容器的 /backup 目录。命令如下:
docker run --volumes-from dbdata -v /opt/backup:/backup centos tar cvf /backup/backup.tar /dbdata
1
2
3
`恢复`
如果要恢复数据到一个容器,首先创建一个带有空数据卷的容器 dbdata2。
docker run --volumes-from dbdata2 -v /opt/backup:/backup centos tar xvf /backup/backup.tar
1
为了查看/验证恢复的数据,可以再启动一个容器挂载同样的容器卷来查看
docker run --volumes-from dbdata2 centos /bin/ls /dbdata
1
2
3
4
5
6
7
8
## Docker registry
### Registry 和 Repository 的区别
注册服务器(Registry),是管理仓库的具体服务器,每个服务器上可以有多个仓库(Repository),而每个仓库(Repository)下面有多个镜像。
### 配置registry
registry如果想要别人可以使用,需要https才可以,我们可以利用openssl来搭建私有的CA服务器,用以签名、颁发证书,管理已签名证书和已吊销证书等。
### 搭建私有CA
初始化CA环境,在/etc/pki/CA/下建立证书索引数据库文件index.txt和序列号文件serial,并为证书序列号文件提供初始值。
touch /etc/pki/CA/{index.txt,serial} echo 01 > /etc/pki/CA/serial
1
2
生成密钥并保存到/etc/pki/CA/private/cakey.pem
(umask 077;openssl genrsa -out /etc/pki/CA/private/cakey.pem 2048)
1
2
生成根证书
openssl req -new -x509 -key /etc/pki/CA/private/cakey.pem -out /etc/pki/CA/cacert.pem -days 3650
1
2
填写的信息
Country Name (2 letter code) [XX]:CN State or Province Name (full name) []:Beijing Locality Name (eg, city) [Default City]:Beijing Organization Name (eg, company) [Default Company Ltd]:mycompany Organizational Unit Name (eg, section) []:ops Common Name (eg, your name or your server's hostname) []:registry.mycompany.com Email Address []:admin@mycompany.com
1
2
使Linux系统信任根证书
cat /etc/pki/CA/cacert.pem >> /etc/pki/tls/certs/ca-bundle.crt
1
2
签发证书
yum -y install nginx //安装nginx mkdir /etc/nginx/ssl //创建ssl目录用来存放密钥文件和证书申请文件 //创建密钥文件和证书申请文件 (umask 077;openssl genrsa -out /etc/nginx/ssl/docker.key 2048) openssl req -new -key /etc/nginx/ssl/docker.key -out /etc/nginx/ssl/docker.csr
1
2
填写的申请信息前四项要和私有CA的信息一致
Country Name (2 letter code) [XX]:CN State or Province Name (full name) []:Beijing Locality Name (eg, city) [Default City]:Beijing Organization Name (eg, company) [Default Company Ltd]:mycompany Organizational Unit Name (eg, section) []:ops Common Name (eg, your name or your server's hostname) []:registry.mycompany.com Email Address []:admin@mycompany.com #直接回车 A challenge password []: An optional company name []:
1
2
签署,证书
[root@node1 ~]# openssl ca -in /etc/nginx/ssl/docker.csr -out /etc/nginx/ssl/docker.crt -days 3650 Using configuration from /etc/pki/tls/openssl.cnf Check that the request matches the signature Signature ok Certificate Details: Serial Number: 1 (0x1) Validity Not Before: Mar 30 08:12:58 2017 GMT Not After : Mar 28 08:12:58 2027 GMT Subject: countryName = CN stateOrProvinceName = Beijing organizationName = mycompany organizationalUnitName = ops commonName = registry.mycompany.com emailAddress = admin@mycompany.com X509v3 extensions: X509v3 Basic Constraints: CA:FALSE Netscape Comment: OpenSSL Generated Certificate X509v3 Subject Key Identifier: 59:27:56:F3:67:46:4B:6D:A5:1B:66:C0:D8:C7:7D:0F:CA:90:C2:ED X509v3 Authority Key Identifier: keyid:76:4A:E0:BB:91:F5:0C:B2:67:2E:D1:3C:74:2B:05:F6:2C:A9:9B:7B Certificate is to be certified until Mar 28 08:12:58 2027 GMT (3650 days) Sign the certificate? [y/n]:y 1 out of 1 certificate requests certified, commit? [y/n]y Write out database with 1 new entries Data Base Updated
1
2
检查index.txt和serial序列号更新
[root@node1 ~]# cat /etc/pki/CA/index.txt V 270328055023Z 01 unknown /C=CN/ST=Beijing/O=mycompany/OU=ops/CN=registry.mycompany.com/emailAddress=admin@mycompany.com [root@node1 ~]# cat /etc/pki/CA/serial 02
1
2
3
### 基于localhost搭建docker-registry
启动registry
docker run -d -p 5000:5000 --restart=always --name registry -v /opt/registry:/var/lib/registry registry:2
1
2
从dockerhub下载centos镜像,使用docker tag 将 centos 这个镜像标记为 localhost:5000/centos
docker pull centos && docker tag centos localhost:5000/centos
1
2
使用docker push 上传标记的镜像
docker push localhost:5000/centos
1
2
从私有仓库下载镜像
docker pull localhost:5000/centos
1
2
3
4
### 配置nginx反向代理docker registry
- **为nginx添加认证**
yum -y install httpd-tools htpasswd -cb /etc/nginx/conf.d/docker-registry.htpasswd admin admin
1
- **添加nginx的server**
[root@node1 ~]# cat /etc/nginx/conf.d/docker-registry.conf upstream docker-registry { server 127.0.0.1:5000; } server { listen 443; server_name registry.mycompany.com; ssl on; ssl_certificate /etc/nginx/ssl/docker.crt; ssl_certificate_key /etc/nginx/ssl/docker.key; client_max_body_size 0; chunked_transfer_encoding on; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; location / { auth_basic "Docker registry"; auth_basic_user_file /etc/nginx/conf.d/docker-registry.htpasswd; proxy_pass http://docker-registry; } location /_ping{ auth_basic off; proxy_pass http://docker-registry; } location /v1/_ping{ auth_basic off; proxy_pass http://docker-registry; } }
1
2
- **重启nginx**
systemctl restart nginx
1
2
- **修改hosts**
10.0.7.1 registry.mycompany.com
1
2
- **测试**
[root@node1 ~]# docker login registry.mycompany.com Username: admin Password: Login Succeeded
1
2
- **上传镜像**
docker tag centos registry.mycompany.com/centos docker push registry.mycompany.com/centos
1
2
- **查看**
curl --user admin:admin https://registry.mycompany.com/v2/_catalog {"repositories":["centos"]}
1
2
- **配置局域网内其他机器认证**
// 修改hosts vim /etc/hosts 10.0.7.1 registry.mycompany.com //同样的系统版本,直接覆盖ca-bundle.crt, scp -rp /etc/pki/tls/certs/ca-bundle.crt root@10.0.7.2:/etc/pki/tls/certs/ca-bundle.crt
1
2
不同版本把CA的密钥发送到客户机,并添加到ca-bundle.crt
scp -rp /etc/pki/CA/cacert.pem root@10.0.7.2:/etc/pki/CA/cacert.pem cat /etc/pki/CA/cacert.pem >> /etc/pki/tls/certs/ca-bundle.crt
1
2
重启docker,如果不重启会出现docker提示x509证书没有授权
systemctl daemon-reload systemctl restart docker
1
2
验证
[root@node1 ~]# curl --user admin:admin https://registry.mycompany.com/v2/_catalog {"repositories":["centos"]} [root@node1 ~]# docker login registry.mycompany.com Username: admin Password: Login Succeeded [root@node1 ~]# docker pull registry.mycompany.com/centos Using default tag: latest latest: Pulling from centos 4969bbd91a1e: Pull complete Digest: sha256:d7f3db1caf4ea76117abce89709ebfc66c9339e13866016b8b2e4eee3ab4bea0 Status: Downloaded newer image for registry.mycompany.com/centos:latest
1
2
3
4
## Docker 网络
### 查看容器IP
- ** 第一种方法 **
docker run -d --name nginx nginx docker inspect --format '{{ .NetworkSettings.IPAddress }}' nginx
1
- ** 第二种方法 **
docker exec -ti nginx ip add | grep global
1
- ** 第三种方法 **
docker exec -ti nginx cat /etc/hosts
1
2
3
### 端口映射
把Docker的内部端口通过端口映射的方法映射到宿主机的某一个端口,当使用 -P 标记时,Docker 会随机映射一个 `49000~49900` 的端口到内部容器开放的网络端口。
docker run -d -P nginx docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 3a5ec1bc2837 nginx "nginx -g 'daemon ..." About an hour ago Up About an hour 0.0.0.0:2049->80/tcp, 0.0.0.0:2048->443/tcp adoring_pike
1
2
通过docker logs查看应用信息
[root@node1 ~]# docker logs -f adoring_pike 172.17.0.1 - - [30/Mar/2017:15:02:40 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.29.0" "-" 10.0.7.1 - - [30/Mar/2017:15:02:47 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.29.0" "-"
1
2
3
4
-p(小写的)则可以指定要映射的端口,并且,在一个指定端口上只可以绑定一个容器。
支持的格式有`ip:hostPort:containerPort` | `ip::containerPort` | `hostPort:containerPort`。
映射到本机所有IP的 80 端口映射到容器的 80 端口
docker run -d -p 80:80 nginx
1
2
查看映射的端口
docker port adoring_pike
1
2
3
## 容器互联
使用 `--link` 参数可以让容器之间安全的进行交互。
下面先创建一个新的数据库容器。
docker run -d --name db training/postgres
1
2
然后创建一个 web 容器,并将它连接到 db 容器
docker run -d -P --name web --link db:db training/webapp python app.py
1
2
3
4
5
此时,db 容器和 web 容器建立互联关系。
`--link` 参数的格式为 `--link name:alias`,其中 name 是要链接的容器的名称,alias 是这个连接的别名。
使用 `env` 命令来查看 web 容器的环境变量
[root@node1 ~]# docker run --rm --name web2 --link db:db training/webapp env PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin HOSTNAME=f2868b0479d8 DB_PORT=tcp://172.17.0.8:5432 DB_PORT_5432_TCP=tcp://172.17.0.8:5432 DB_PORT_5432_TCP_ADDR=172.17.0.8 DB_PORT_5432_TCP_PORT=5432 DB_PORT_5432_TCP_PROTO=tcp DB_NAME=/web2/db DB_ENV_PG_VERSION=9.3 HOME=/root
1
2
3
4
其中 DB_ 开头的环境变量是供 web 容器连接 db 容器使用,前缀采用大写的连接别名。
除了环境变量,`Docker` 还添加 `host` 信息到父容器的 `/etc/hosts `的文件。下面是父容器 web 的 hosts 文件
[root@node1 ~]# docker run -t -i --rm --link db:db training/webapp /bin/bash root@8299f9685894:/opt/webapp# cat /etc/hosts 127.0.0.1 localhost ::1 localhost ip6-localhost ip6-loopback fe00::0 ip6-localnet ff00::0 ip6-mcastprefix ff02::1 ip6-allnodes ff02::2 ip6-allrouters 172.17.0.8 db d65ebb9124a4 172.17.0.10 8299f9685894
1
2
3
4
5
6
7
8
9
10
11
12
13
14
## docker的网络模式
Docker的网络模式分为四种
### Bridge模式
当Docker进程启动时,会在主机上创建一个名为docker0的虚拟网桥,此主机上启动的Docker容器会连接到这个虚拟网桥上。虚拟网桥的工作方式和物理交换机类似,这样主机上的所有容器就通过交换机连在了一个二层网络中。
从docker0子网中分配一个IP给容器使用,并设置docker0的IP地址为容器的默认网关。在主机上创建一对虚拟网卡veth pair设备,Docker将veth pair设备的一端放在新创建的容器中,并命名为eth0(容器的网卡),另一端放在主机中,以vethxxx这样类似的名字命名,并将这个网络设备加入到docker0网桥中。可以通过brctl show命令查看。
bridge模式是docker的默认网络模式,不写--net参数,就是bridge模式。使用docker run -p时,docker实际是在iptables做了DNAT规则,实现端口转发功能。
可以使用iptables -t nat -vnL查看。
bridge模式如下图所示:
![bridge模式](/images/bridge-mode.png)
演示:
docker run -tid --net=bridge --name docker_bri1 ubuntu-base:v3 docker run -tid --net=bridge --name docker_bri2 ubuntu-base:v3 brctl show docker exec -ti docker_bri1 /bin/bash docker exec -ti docker_bri1 /bin/bash ifconfig –a route –n
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
### Host模式
如果启动容器的时候使用`host`模式,那么这个容器将不会获得一个独立的`Network Namespace`,而是和宿主机共用一个`Network Namespace`。容器将不会虚拟出自己的网卡,配置自己的IP等,而是使用宿主机的IP和端口。但是,容器的其他方面,如文件系统、进程列表等还是和宿主机隔离的。
Host模式如下图所示:
![Host模式](/images/docker-host-mode.png)
### Container模式
这个模式指定新创建的容器和已经存在的一个容器共享一个 Network Namespace,而不是和宿主机共享。新创建的容器不会创建自己的网卡,配置自己的 IP,而是和一个指定的容器共享 IP、端口范围等。同样,两个容器除了网络方面,其他的如文件系统、进程列表等还是隔离的。两个容器的进程可以通过 lo 网卡设备通信。
Container模式示意图:
![Container模式](/images/docker-container-mode.png)
### None模式
使用none模式,Docker容器拥有自己的Network Namespace,但是,并不为Docker容器进行任何网络配置。也就是说,这个Docker容器没有网卡、IP、路由等信息。需要我们自己为Docker容器添加网卡、配置IP等。
![None模式](/images/docker-none-mode.png)
## Docker网络设置
### 配置 DNS
在docker run时使用以下参数:
参数 说明
-h HOSTNAME or --hostname=HOSTNAME 设定容器的主机名,它会被写到容器内的 /etc/hostname 和/etc/hosts。但它在容器外部看不到,既不会在 docker ps 中显示,也不会在其他的容器的/etc/hosts 看到。
--link=CONTAINER_NAME:ALIAS 选项会在创建容器的时候,添加一个其他容器的主机名到 /etc/hosts 文件中,让新容器的进程可以使用主机名 ALIAS 就可以连接它。
--dns=IP_ADDRESS 添加 DNS 服务器到容器的 /etc/resolv.conf 中,让容器用这个服务器来解析所有不在 /etc/hosts 中的主机名。
--dns-search=DOMAIN 设定容器的搜索域,当设定搜索域为 .example.com 时,在搜索一个名为 host 的主机时,DNS 不仅搜索host,还会搜索 host.example.com。 注意:如果没有上述最后 2 个选项,Docker 会默认用主机上的 /etc/resolv.conf 来配置容器。
### 容器访问控制
- ** 容器访问外部网络 **
容器要想访问外部网络,需要本地系统的转发支持。在Linux 系统中,检查转发是否打开。
sysctl net.ipv4.ip_forward net.ipv4.ip_forward = 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
- ** 容器之间访问 **
容器之间相互访问,需要两方面的支持。
- 容器的网络拓扑是否已经互联。默认情况下,所有容器都会被连接到 `docker0` 网桥上。
- 本地系统的防火墙软件 `-- iptables `是否允许通过。
- ** 访问所有端口 **
当启动 Docker 服务时候,默认会添加一条转发策略到 iptables 的 FORWARD 链上。策略为通过(ACCEPT)还是禁止(DROP)取决于配置 --icc=true(缺省值)还是 --icc=false。当然,如果手动指定 --iptables=false 则不会添加 iptables 规则。
可见,默认情况下,不同容器之间是允许网络互通的。如果为了安全考虑,可以在docker服务修改/usr/lib/systemd/system/docker.service启动时添加--icc=false。
- ** 访问指定端口 **
在通过 -icc=false 关闭网络访问后,可以通过 `--link=CONTAINER_NAME:ALIAS` 选项来访问容器的开放端口。
例如,在启动 Docker 服务时,可以同时使用 `--icc=false --iptables=true` 参数来关闭允许相互的网络访问,并让 `Docker` 可以修改系统中的 `iptables` 规则。
此时,系统中的 `iptables` 规则可能是类似
iptables -nL ... Chain FORWARD (policy ACCEPT) target prot opt source destination DROP all -- 0.0.0.0/0 0.0.0.0/0 ...
1
2
3
4
5
6
7
8
## 国内加速
由于国外的docker镜像仓库一般都比较慢,所以一般会使用国内的一些加速仓库,如阿里云
申请:https://cr.console.aliyun.com/#/accelerator
配置
mkdir -p /etc/docker cat /etc/docker/daemon.json { "registry-mirrors": ["https://xxxxxxx.mirror.aliyuncs.com"] } https://xxxxxxx.mirror.aliyuncs.com替换成你申请到的地址。//https://5f2jam6c.mirror.aliyuncs.com sudo systemctl daemon-reload sudo systemctl restart docker
1
2
3
4
5
6
7
8
9
10
11
## Docker Compose使用
### 简介
> Docker Compose是一个用来定义和运行复杂应用的Docker工具。使用Compose,你可以在一个文件中定义一个多容器应用,然后使用一条命令来启动你的应用,完成一切准备工作。
> 一个使用Docker容器的应用,通常由多个容器组成。使用Docker Compose,不再需要使用shell脚本来启动容器。在配置文件中,所有的容器通过services来定义,然后使用docker-compose脚本来启动,停止和重启应用,和应用中的服务以及所有依赖服务的容器。
### 安装
## 安装 docker-compose

curl -L https://get.daocloud.io/docker/compose/releases/download/1.18.0/docker-compose-`uname -s-uname -m` > /usr/local/bin/docker-compose
chmod +x /usr/bin/docker-compose
docker-compose –version

1
2
3
4
### 配置文件
下面为一份 `docker-compose` 参考配置文件:

version: '2'
services:
  web:
    image: dockercloud/hello-world
    ports:
      - 8080
    networks:
      - front-tier
      - back-tier

  redis:
    image: redis
    links:
      - web
    networks:
      - back-tier

  lb:
    image: dockercloud/haproxy
    ports:
      - 80:80
    links:
      - web
    networks:
      - front-tier
      - back-tier
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock 

networks:
  front-tier:
    driver: bridge
  back-tier:
driver: bridge
1
2
3
4
可以看到一份标准配置文件应该包含 version、services、networks 三大部分,其中最关键的就是 services 和 networks 两个部分,下面先来看 services 的书写规则。
### image
services: web: image: hello-world
1
2
3
在 services 标签下的第二级标签是 web,这个名字是用户自己自定义,它就是服务名称。
image 则是指定服务的镜像名称或镜像 ID。如果镜像在本地不存在,Compose 将会尝试拉取这个镜像。
例如下面这些格式都是可以的:
image: redis image: ubuntu:14.04 image: tutum/influxdb image: example-registry.com:4000/postgresql image: a4bc65fd
1
2
3
### build
服务除了可以基于指定的镜像,还可以基于一份 Dockerfile,在使用 up 启动之时执行构建任务,这个构建标签就是 build,它可以指定 Dockerfile 所在文件夹的路径。Compose 将会利用它自动构建这个镜像,然后使用这个镜像启动服务容器。
build: /path/to/build/dir
1
2
也可以是相对路径,只要上下文确定就可以读取到 Dockerfile。
build: ./dir
1
2
设定上下文根目录,然后以该目录为准指定 Dockerfile。
build: context: ../ dockerfile: path/of/Dockerfile
1
2
3
注意 build 都是一个目录,如果你要指定 Dockerfile 文件需要在 build 标签的子级标签中使用 dockerfile 标签指定,如上面的例子。
如果你同时指定了 image 和 build 两个标签,那么 Compose 会构建镜像并且把镜像命名为 image 后面的那个名字。
build: ./dir image: webapp:tag
1
2
既然可以在 docker-compose.yml 中定义构建任务,那么一定少不了 arg 这个标签,就像 Dockerfile 中的 ARG 指令,它可以在构建过程中指定环境变量,但是在构建成功后取消,在 docker-compose.yml 文件中也支持这样的写法:
build: context: . args: buildno: 1 password: secret
1
2
下面这种写法也是支持的,一般来说下面的写法更适合阅读。
build: context: . args: - buildno=1 - password=secret
1
2
与 ENV 不同的是,ARG 是允许空值的。例如:
args: - buildno - password
1
2
3
4
5
6
7
这样构建过程可以向它们赋值。
> 注意:YAML 的布尔值(true, false, yes, no, on, off)必须要使用引号引起来(单引号、双引号均可),否则会当成字符串解析。
### command
使用 command 可以覆盖容器启动后默认执行的命令。
command: bundle exec thin -p 3000
1
2
也可以写成类似 Dockerfile 中的格式:
command: [bundle, exec, thin, -p, 3000]
1
2
3
4
### container_name
前面说过 Compose 的容器名称格式是:<项目名称><服务名称><序号>
虽然可以自定义项目名称、服务名称,但是如果你想完全控制容器的命名,可以使用这个标签指定:
container_name: app
1
2
3
4
5
6
这样容器的名字就指定为 app 了。
### depends_on
在使用 Compose 时,最大的好处就是少打启动命令,但是一般项目容器启动的顺序是有要求的,如果直接从上到下启动容器,必然会因为容器依赖问题而启动失败。
例如在没启动数据库容器的时候启动了应用容器,这时候应用容器会因为找不到数据库而退出,为了避免这种情况我们需要加入一个标签,就是 depends_on,这个标签解决了容器的依赖、启动先后的问题。
例如下面容器会先启动 redis 和 db 两个服务,最后才启动 web 服务:
version: '2' services: web: build: . depends_on: - db - redis redis: image: redis db: image: postgres
1
2
3
4
5
> 注意的是,默认情况下使用 docker-compose up web 这样的方式启动 web 服务时,也会启动 redis 和 db 两个服务,因为在配置文件中定义了依赖关系。
### dns
和 --dns 参数一样用途,格式如下:
dns: 8.8.8.8
1
2
也可以是一个列表:
dns: - 8.8.8.8 - 9.9.9.9
1
2
此外 dns_search 的配置也类似:
dns_search: example.com dns_search: - dc1.example.com - dc2.example.com
1
2
3
### tmpfs
挂载临时目录到容器内部,与 run 的参数一样效果:
tmpfs: /run tmpfs: - /run - /tmp
1
2
3
4
### entrypoint
在 Dockerfile 中有一个指令叫做 ENTRYPOINT 指令,用于指定接入点,第四章有对比过与 CMD 的区别。
在 docker-compose.yml 中可以定义接入点,覆盖 Dockerfile 中的定义:
entrypoint: /code/entrypoint.sh
1
2
格式和 Docker 类似,不过还可以写成这样:
entrypoint: - php - -d - zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20100525/xdebug.so - -d - memory_limit=-1 - vendor/bin/phpunit
1
2
3
4
5
6
7
### env_file
还记得前面提到的 .env 文件吧,这个文件可以设置 Compose 的变量。而在 docker-compose.yml 中可以定义一个专门存放变量的文件。
如果通过 docker-compose -f FILE 指定了配置文件,则 env_file 中路径会使用配置文件路径。
如果有变量名称与 environment 指令冲突,则以后者为准。格式如下:
env_file: .env
1
或者根据 docker-compose.yml 设置多个:
env_file: - ./common.env - ./apps/web.env - /opt/secrets.env
1
2
3
4
5
6
> 注意的是这里所说的环境变量是对宿主机的 Compose 而言的,如果在配置文件中有 build 操作,这些变量并不会进入构建过程中,如果要在构建中使用变量还是首选前面刚讲的 arg 标签。
### environment
与上面的 env_file 标签完全不同,反而和 arg 有几分类似,这个标签的作用是设置镜像变量,它可以保存变量到镜像里面,也就是说启动的容器也会包含这些变量设置,这是与 arg 最大的不同。
一般 arg 标签的变量仅用在构建过程中。而 environment 和 Dockerfile 中的 ENV 指令一样会把变量一直保存在镜像、容器中,类似 docker run -e 的效果。
environment: RACK_ENV: development SHOW: 'true' SESSION_SECRET: environment: - RACK_ENV=development - SHOW=true - SESSION_SECRET
1
2
3
4
### expose
这个标签与Dockerfile中的EXPOSE指令一样,用于指定暴露的端口,但是只是作为一种参考,实际上docker-compose.yml的端口映射还得ports这样的标签。
expose: - "3000" - "8000"
1
2
3
4
5
### external_links
在使用Docker过程中,我们会有许多单独使用docker run启动的容器,为了使Compose能够连接这些不在docker-compose.yml中定义的容器,我们需要一个特殊的标签,就是external_links,它可以让Compose项目里面的容器连接到那些项目配置外部的容器(前提是外部容器中必须至少有一个容器是连接到与项目内的服务的同一个网络里面)。
格式如下:
external_links: - redis_1 - project_db_1:mysql - project_db_1:postgresql
1
2
3
### extra_hosts
添加主机名的标签,就是往/etc/hosts文件中添加一些记录,与Docker client的--add-host类似:
extra_hosts: - "somehost:162.242.195.82" - "otherhost:50.31.209.229"
1
启动之后查看容器内部hosts:
162.242.195.82 somehost 50.31.209.229 otherhost
1
2
3
### labels
向容器添加元数据,和Dockerfile的LABEL指令一个意思,格式如下:
labels: com.example.description: "Accounting webapp" com.example.department: "Finance" com.example.label-with-empty-value: "" labels: - "com.example.description=Accounting webapp" - "com.example.department=Finance" - "com.example.label-with-empty-value"
1
2
3
### links
还记得上面的depends_on吧,那个标签解决的是启动顺序问题,这个标签解决的是容器连接问题,与Docker client的--link一样效果,会连接到其它服务中的容器。
格式如下:
links: - db - db:database - redis
1
使用的别名将会自动在服务容器中的/etc/hosts里创建。例如:
172.12.2.186 db 172.12.2.186 database 172.12.2.187 redis
1
2
3
相应的环境变量也将被创建。
### logging
这个标签用于配置日志服务。格式如下:
logging: driver: syslog options: syslog-address: "tcp://192.168.0.42:123"
1
2
3
4
5
默认的driver是json-file。只有json-file和journald可以通过docker-compose logs显示日志,其他方式有其他日志查看方式,但目前Compose不支持。对于可选值可以使用options指定。
### pid
pid: "host"
1
2
3
4
将PID模式设置为主机PID模式,跟主机系统共享进程命名空间。容器使用这个标签将能够访问和操纵其他容器和宿主机的名称空间。
### ports
映射端口的标签。
使用HOST:CONTAINER格式或者只是指定容器的端口,宿主机会随机映射端口。
ports: - "3000" - "8000:8000" - "49100:22" - "127.0.0.1:8001:8001"
1
2
3
4
5
> 注意:当使用HOST:CONTAINER格式来映射端口时,如果你使用的容器端口小于60你可能会得到错误得结果,因为YAML将会解析xx:yy这种数字格式为60进制。所以建议采用字符串格式。
### security_opt
为每个容器覆盖默认的标签。简单说来就是管理全部服务的标签。比如设置全部服务的user标签值为USER。
security_opt: - label:user:USER - label:role:ROLE
1
2
3
4
### stop_signal
设置另一个信号来停止容器。在默认情况下使用的是SIGTERM停止容器。设置另一个信号可以使用stop_signal标签。
stop_signal: SIGUSR1
1
2
3
4
### volumes
挂载一个目录或者一个已存在的数据卷容器,可以直接使用 [HOST:CONTAINER] 这样的格式,或者使用 [HOST:CONTAINER:ro] 这样的格式,后者对于容器来说,数据卷是只读的,这样可以有效保护宿主机的文件系统。
Compose的数据卷指定路径可以是相对路径,使用 . 或者 .. 来指定相对目录。
数据卷的格式可以是下面多种形式:
volumes: // 只是指定一个路径,Docker 会自动在创建一个数据卷(这个路径是容器内部的)。 - /var/lib/mysql // 使用绝对路径挂载数据卷 - /opt/data:/var/lib/mysql // 以 Compose 配置文件为中心的相对路径作为数据卷挂载到容器。 - ./cache:/tmp/cache // 使用用户的相对路径(~/ 表示的目录是 /home/<用户目录>/ 或者 /root/)。 - ~/configs:/etc/configs/:ro // 已经存在的命名的数据卷。 - datavolume:/var/lib/mysql
1
2
如果你不使用宿主机的路径,你可以指定一个volume_driver。
volume_driver: mydriver
1
2
### volumes_from
从其它容器或者服务挂载数据卷,可选的参数是 :ro或者 :rw,前者表示容器只读,后者表示容器对数据卷是可读可写的。默认情况下是可读可写的。
volumes_from: - service_name - service_name:ro - container:container_name - container:container_name:rw
1
2
3
### cap_add, cap_drop
添加或删除容器的内核功能。详细信息在前面容器章节有讲解,此处不再赘述。
cap_add: - ALL cap_drop: - NET_ADMIN - SYS_ADMIN
1
2
### cgroup_parent
指定一个容器的父级cgroup
cgroup_parent: m-executor-abcd
1
2
### devices
设备映射列表。与Docker client的--device参数类似。
devices: - "/dev/ttyUSB0:/dev/ttyUSB0"
1
2
### extends
这个标签可以扩展另一个服务,扩展内容可以是来自在当前文件,也可以是来自其他文件,相同服务的情况下,后来者会有选择地覆盖原有配置。
extends: file: common.yml service: webapp
1
2
3
4
5
6
用户可以在任何地方使用这个标签,只要标签内容包含file和service两个值就可以了。
file的值可以是相对或者绝对路径,如果不指定file的值,那么Compose会读取当前YML文件的信息。
### network_mode
网络模式,与Docker client的--net参数类似,只是相对多了一个service:[service name] 的格式。
network_mode: "bridge" network_mode: "host" network_mode: "none" network_mode: "service:[service name]" network_mode: "container:[container name/id]"
1
2
3
4
可以指定使用服务或者容器的网络。
### networks
加入指定网络,格式如下:
services: some-service: networks: - some-network - other-network
1
关于这个标签还有一个特别的子标签aliases,这是一个用来设置服务别名的标签,例如:
services: some-service: networks: some-network: aliases: - alias1 - alias3 other-network: aliases: - alias2
1
2
3
4
5
6
相同的服务可以在不同的网络有不同的别名。
## 阿里自定义镜像
[阿里容器HUB控制台](https://cr.console.aliyun.com/)
### 登录

docker login -u 1012202451213731 -p {passwd} https://registry.acs.aliyun.com

1
2
3
### 打标签
https://registry.acs.aliyun.com

docker tag bf46626ea2a8 registry.acs.aliyun.com/1012202451213731/laravel_taobao:v1.0

1
### 配置加速器

echo “DOCKER_OPTS=\”\$DOCKER_OPTS –registry-mirror=https://lqjizur.mirror.acs.aliyun.com\“” | sudo tee -a /etc/default/docker
sudo service docker restart

1
2
### docker push

docker push registry.acs.aliyun.com/1012202451213731/austin_laravel_taobao:v1.0
```

常用操作命令

  • 查看当前启动的容器
    sudo docker ps

  • 启动部分服务在后边加服务名,不加表示启动所有,-d 表示在后台运行
    sudo docker-compose up [nginx|php71|mysql|redis] -d

  • 停止和启动类似
    sudo docker-compose stop [nginx|php71|mysql|redis]

  • 删除所有未运行的容器
    sudo docker rm $(docker ps -qf status=exited)

  • 删除所有镜像,-f 可以强制删除
    sudo docker rmi $(docker images -q)