Dockerfile

Dockerfile 是一个文本文件,其内包含了一条条的 指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。

Dockerfile 指令

From 指定基础镜像

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

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

FROM scratch
...

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

MAINTAINER 指定维护者信息

格式为 MAINTAINER <name>,指定维护者信息,注意:MAINTAINER 指令已经被弃用,建议使用 LABEL 指令

LABEL 为镜像添加标签

格式为 LABEL <key>=<value> <key>=<value> <key>=<value> …

可以给镜像添加多个 LABEL,需要注意的是,每条 LABEL 指令都会生成一个新的层。所以最好是把添加的多个 LABEL 合并为一条命令:

LABEL multi.label1="value1" \
      multi.label2="value2" \
      other="value3"

可以通过以下命令查看镜像信息,包括 LABEL

$ docker inspect image_name:tag

WORKDIR 指定工作目录

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

如果 WORKDIR 使用的是相对路径,那么所切换的路径与之前的 WORKDIR 有关,例如

WORKDIR /a
WORKDIR b
WORKDIR c

RUN pwd

RUN pwd 的工作目录为 /a/b/c

RUN 执行命令

RUN 指令是用来执行命令行命令的,由于命令行的强大能力,RUN 指令在定制镜像时是最常用的指令之一

其格式有两种:

  1. shell 格式:RUN <命令>,就像直接在命令行中输入的命令一样
  2. exec 格式:RUN ["可执行文件", "参数1", "参数2"],这更像是函数调用中的格式

COPY 复制文件

格式为 COPY [–chown=<user>:<group>] <源路径>… <目标路径>

COPY 指令将从构建上下文目录中 源路径 的文件/目录复制到新的一层的镜像内的 目标路径 位置,目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录,使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等,如果源路径为文件夹,复制的时候不是直接复制该文件夹,而是将文件夹中的内容复制到目标路径

ENTRYPOINT 入口点

可以指定容器运行时执行的内容,例如执行一个 shell 脚本,如下所示

COPY Dockerstart  /start
RUN chmod +x /start
ENTRYPOINT ["/start"]

VOLUME 定义匿名卷

格式为:VOLUME [“<路径1>”, “<路径2>”…]

为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 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 的端口。

构建镜像

创建名称为 Dockerfile 的文件,内容如下

FROM nginx
LABEL maintainer="ryan" email="zhengjianhong95@gmial.com"
WORKDIR /data/www
COPY nginx.conf /etc/nginx/nginx.conf
RUN echo "<?php phpinfo();?>" > index.php

EXPOSE 80 443

在 Dockerfile 的同级目录下放一个 nginx.conf,配置文件内容如下

server {
    listen 80;
    server_name 127.0.0.1 localhost;

    access_log /home/log/access.log;
    error_log /home/log/error.log;

    root /home/www/;
    index index.php index.html index.htm;

    location / {
        try_files $uri $uri/ /index.php;
    }

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_pass unix:/var/run/php-fpm/www.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_connect_timeout 3000;
        fastcgi_send_timeout 3000;
        fastcgi_read_timeout 3000;
    }
    if (-f $request_filename/index.html){
        rewrite (.*) $1/index.html break;
    }
    if (-f $request_filename/index.php){
        rewrite (.*) $1/index.php;
    }
    if (!-f $request_filename){
        rewrite (.*) /index.php;
    }
}

Dockerfile 文件所在目录执行:

$ docker build -t nginx:v3 .

可以使用以下命令查看 docker build 的详细介绍

$ docker build --help

Docker 还支持直接用 Git repo、tar 压缩包、标准输入种读取 Dockerfile 进行构建。

构建镜像上下文 Context

可以看到我们之前构建镜像 docker build 的最后都有一个 “.”, “.” 表示当前目录,指定上下文路径,当构建的时候,会讲上下文路径下的所有内容打包,然后上传到 Docker 引擎,Docker 引擎会根据这个上下文包,获取到镜像所需的文件,例如使用 COPY 指令复制文件到镜像里面的时候,就用到了上下文路径。

COPY ./package.json /app/

以上命令并不是要复制执行 docker build 命令所在的目录下的 package.json,也不是复制 Dockerfile 所在目录下的 package.json,而是复制 上下文(context) 目录下的 package.json,因此 COPY ../package.json 是无法正常执行成功的,因为这些路径已经超出了上下文的范围,Docker 引擎无法获取到这些位置的文件。同时要注意上下文路径不要太大(按需即可),例如直接将整个 “/” 根目录直接作为上下文路径,会导致整个构建过程极其缓慢,因为在讲整个服务器的内容发送到 Docker 引擎。

Docker Compose

基本介绍

Compose 项目是 Docker 官方的开源项目,负责实现对 Docker 容器集群的快速编排,其代码目前在 https://github.com/docker/compose 上开源

Compose 定位是 「定义和运行多个 Docker 容器的应用(Defining and running multi-container Docker applications)」,其前身是开源项目 Fig。

我们知道使用一个 Dockerfile 模板文件,可以让用户很方便的定义一个单独的应用容器。然而,在日常工作中,经常会碰到需要多个容器相互配合来完成某项任务的情况。例如要实现一个 Web 项目,除了 Web 服务容器本身,往往还需要再加上后端的数据库服务容器,甚至还包括负载均衡容器等,Compose 恰好满足了这样的需求。它允许用户通过一个单独的 docker-compose.yml 模板文件(YAML 格式)来定义一组相关联的应用容器为一个项目(project)

Compose 中有两个重要的概念:

  • 服务 (service):一个应用的容器,实际上可以包括若干运行相同镜像的容器实例
  • 项目 (project):由一组关联的应用容器组成的一个完整业务单元,在 docker-compose.yml 文件中定义

使用实例

下面用 Python 建立一个能够记录页面访问次数的 web 网站

新建文件夹,在该目录中编写 app.py 文件

from flask import Flask
from redis import Redis

app = Flask(__name__)
redis = Redis(host='redis', port=6379)

@app.route('/')
def hello():
    count = redis.incr('hits')
    return 'Hello World! 该页面已被访问 {} 次。\n'.format(count)

if __name__ == "__main__":
    app.run(host="0.0.0.0", debug=True)

编写 Dockerfile 文件,内容为

FROM python:3.6-alpine
ADD . /code
WORKDIR /code
RUN pip install redis flask
CMD ["python", "app.py"]

创建文件 docker-compose.yml,内容如下

version: '3'
services:
    web:
        build: .
        ports:
         - "5000:5000"
    redis:
        image: "redis:alpine"

运行 compose 项目

$ docker-compose up

此时访问本地 5000 端口,链接为 localhost:5000,每次刷新页面,计数就会加 1