Hello Docker(三)——Docker镜像制作

一、Dockerfile脚本

1、Dockerfile脚本简介

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

##  Dockerfile文件格式
# This dockerfile uses the ubuntu image
# VERSION 2 - EDITION 1
# Author: docker_user
# Command format: Instruction [arguments / command] ..

# 1、第一行必须指定 基础镜像信息
FROM centos

# 2、维护者信息
MAINTAINER docker_user docker_user@email.com

# 3、镜像操作指令
RUN yum install -y nginx

# 4、容器启动执行指令
CMD /usr/sbin/nginx

Dockerfile分为四部分:基础镜像信息、维护者信息、镜像操作指令、容器启动执行指令。第一部分必须指明基础镜像名称;第二部分通常说明维护者信息;第三部分是镜像操作指令,例如RUN指令,每执行一条RUN 指令,镜像添加新的一层,并提交;第四部分是CMD指令,指明运行容器时的操作命令。
Dockerfile官方文档:
https://docs.docker.com/engine/reference/builder/
Dockerfile最佳实践文档:
https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/
Docker官方镜像Dockerfile:
https://github.com/docker-library/docs

2、FROM指令

FROM用于指定基础镜像,因此Dockerfile中FROM是必备指令,并且必须是第一条指令。
Docker Hub中有非常多的高质量的官方镜像,有直接可用的服务类镜像,如nginx、redis、mongo、mysql 、httpd、ph、tomcat 等;有方便开发、构建、运行各种语言应用的镜像,如node、openjdk、 python、ruby、golang等;有基础的操作系统镜像,如ubuntu、debian、centos、fedora、alpine等。
Docker存在一个特殊的scratch镜像,scratch镜像是虚拟的概念,并不实际存在,表示一个空白的镜像。
如果以scratch为基础镜像,不以任何镜像为基础,后续所写的指令将作为镜像第一层开始存在。不以任何系统为基础,直接将可执行文件复制进镜像如swarm、coreos/etcd。Linux下静态编译的程序并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里,因此直接FROM scratch会让镜像体积更加小巧。
FROM语法格式为:

FROM <image>
FROM <image>:<tag>
FROM <image>:<digest>

FROM限制如下:
A、FROM必须是Dockerfile中第一条非注释命令
B、在一个Dockerfile文件中创建多个镜像时,FROM可以多次出现。只需在每个新命令FROM前,记录提交上次的镜像ID。
C、tag或digest是可选的,如果不使用tag值时,会使用latest版本的基础镜像。

3、RUN指令

在镜像的构建过程中执行特定的命令,并生成一个中间镜像。格式:

#shell格式
RUN <command>
#exec格式
RUN ["executable", "param1", "param2"]

RUN命令将在当前image中执行任意合法命令并提交执行结果。命令执行提交后,就会自动执行Dockerfile中的下一个指令。
RUN指令创建的中间镜像会被缓存,并会在下次构建中使用。如果不想使用缓存镜像,可以在构建时指定--no-cache参数,如:docker build --no-cache。

4、COPY指令

COPY指令语法格式:

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

COPY 指令将从构建上下文目录中<源路径>的文件/目录复制到新的一层的镜像内的<目标路径>位置。
<源路径>可以是多个,甚至可以是通配符,其通配符规则要满足Go的filepath.Match规则,如:

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

<目标路径>可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用WORKDIR指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。
使用COPY指令,源文件的各种元数据都会保留,比如读、写、执行权限、文件变更时间等。

5、ADD指令

ADD指令在COPY基础上增加了一些功能,比如<源路径>可以是一个URL,Docker引擎会试图去下载URL链接的文件放到<目标路径>。
在构建镜像时,复制上下文中的文件到镜像内,格式:

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

如果Docker发现文件内容被改变,则后续指令都不会再使用缓存。
ADD指令会将相应文件增加到目标目录,如果源文件是压缩文件会进行解压操作。

6、CMD指令

CMD用于指定在容器启动时所要执行的命令。CMD有三种格式:

CMD ["executable","param1","param2"]
CMD ["param1","param2"]
CMD command param1 param2

默认为exec格式,使CMD中的参数当做ENTRYPOINT的默认参数,此时ENTRYPOINT应该是exec格式。
如果CMD是/bin/bash,使用docker run -it ubuntu启动容器时,会直接执行进入bash。docker run -it ubuntu cat /etc/os-release会在启动容器时输出系统版本信息。
在CMD指令格式上,推荐使用exec格式,exec格式在解析时会被解析为JSON数组,因此必须使用双引号,而不要使用单引号。
如果使用shell格式,CMD命令会被包装为sh -c的参数的形式进行执行。比如:
CMD echo $HOME会将其变更为:
CMD ["sh", "-c","echo $HOME"]
exec格式不能使用shell中的环境变量,如果要使用shell中环境变量,需要在exec格式命令中指定使用shell脚本。

7、ENTRYPOINT指令

ENTRYPOINT指令用于给容器配置一个可执行程序。每次使用镜像创建容器时,通过ENTRYPOINT指定的程序都会被设置为默认程序。ENTRYPOINT有两种形式:

ENTRYPOINT ["executable", "param1", "param2"]
ENTRYPOINT command param1 param2

通过docker run执行的命令不会覆盖ENTRYPOINT,而docker run命令中指定的任何参数,都会被当做参数再次传递给ENTRYPOINT。Dockerfile中只允许有一个ENTRYPOINT命令,多指定时会覆盖前面的设置,而只执行最后的ENTRYPOINT指令。
docker run运行容器时指定的参数都会被传递给ENTRYPOINT,且会覆盖 CMD命令指定的参数。执行docker run &lt;image&gt; -d时,-d指定的参数将被传递给入口点,也可以通过docker run --entrypoint重写ENTRYPOINT入口点。
ENTRYPOINT ["/usr/bin/nginx"]

8、ENV指令

ENV指令用于设置环境变量,后续的指令可以直接使用。

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

ENV示例如下:

ENV VERSION=1.0 DEBUG=on \
NAME="Happy Feet"

9、ARG指令

ARG指令用于指定传递给构建运行时的变量。
ARG &lt;name&gt;[=&lt;default value&gt;]
通过ARG指定两个变量:

ARG site
ARG build_user=scorpio

上述指令指定site和build_user两个变量,其中build_user指定了默认值。使用docker build构建镜像时,可以通过--build-arg &lt;varname&gt;=&lt;value&gt; 选项参数来指定或重设置相应变量的值。
docker build --build-arg site=www.baidu.com -t baidu/test .
build_user变量使用默认值scorpio。

10、VOLUME指令

VOLUME指令用于创建挂载点,即向基于所构建镜像创始的容器添加卷:
VOLUME ["/data"]
一个卷可以存在于一个或多个容器的指定目录,该目录可以绕过联合文件系统,并具有以下功能:
A、卷可以容器间共享和重用
B、容器并不一定要和其它容器共享卷
C、修改卷后会立即生效
D、对卷的修改不会对镜像产生影响
E、卷会一直存在,直到没有任何容器在使用它
VOLUME可以将源代码、数据或其它内容添加到镜像中,而不提交到镜像中,并使多个容器间共享数据。

11、EXPOSE指令

EXPOSE指令为构建的镜像设置监听端口,使容器在运行时监听。格式如下:
EXPOSE &lt;port&gt; [&lt;port&gt;...]
EXPOSE指令并不会让容器监听host的端口,如果需要容器监听Host端口,需要在docker run时使用 -p、-P 参数来发布容器端口到host的某个端口上。

12、WORKDIR指令

WORKDIR指令用于在容器内设置一个工作目录。
WORKDIR /path/to/workdir
通过WORKDIR设置工作目录后,Dockerfile中的后续命令RUN、CMD、ENTRYPOINT、ADD、COPY 等命令都会在工作目录下执行。

WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd

pwd最终将会在 /a/b/c 目录中执行。使用docker run运行容器时,可以通过-w参数覆盖构建时所设置的工作目录。
WORKDIR指令推荐使用绝对路径。

13、USER指令

USER指令用于指定运行镜像所使用的用户。
USER daemon
使用USER指定用户时,可以使用用户名、UID 或 GID,或是两者的组合。

USER user
USER user:group
USER uid
USER uid:gid
USER user:gid
USER uid:group

使用USER指定用户后,Dockerfile中的后续命令RUN、CMD、ENTRYPOINT 都将使用该用户。镜像构建完成后,通过docker run 运行容器时,可以通过-u参数来覆盖所指定的用户。

14、HEALTHCHECK指令

HEALTHCHECK [OPTIONS] CMD command  
通过运行一个容器内部的命令来检测容器是否健康
HEALTHCHECK NONE
关闭任何来自基础image的健康检测
options
--interval=DURATION (default: 30s)
--timeout=DURATION (default: 30s)
--retries=N (default: 3)

15、ONBUILD指令

ONBUILD指令用于设置镜像触发器。
ONBUILD [INSTRUCTION]
当所构建的镜像被用作其它镜像的基础镜像,镜像中的触发器将会被触发。当镜像被使用时,可能需要做一些处理:

[...]
ONBUILD ADD . /app/src
ONBUILD RUN /usr/local/bin/python-build --dir /app/src
[...]

16、LABEL

LABEL指令用于为镜像添加元数据,元数以键值对的形式指定。
LABEL &lt;key&gt;=&lt;value&gt; &lt;key&gt;=&lt;value&gt; &lt;key&gt;=&lt;value&gt; ...
使用LABEL指定元数据时,一条LABEL指定可以指定一或多条元数据,指定多条元数据时不同元数据之间通过空格分隔。推荐将所有的元数据通过一条LABEL指令指定,以免生成过多的中间镜像。
LABEL version="1.0" description="hello world" by="scorpio"
LABEL指定的元数据可以通过docker inspect查看。

17、STOPSIGNAL

STOPSIGNAL指令用于设置停止容器所要发送的系统调用信号。
STOPSIGNAL signal
所使用的信号必须是内核系统调用表中的合法的值,如:SIGKILL。

18、SHELL

SHELL指令用于设置执行命令(shell)所使用的的默认shell类型。
SHELL ["executable", "parameters"]
SHELL在Windows环境下比较有用,Windows下通常会有cmd和 powershell两种shell,可以通过SHELL来指定所使用的shell类型。

二、Dockerfile构建镜像

1、Dockerfile构建简介

docker build命令会根据Dockerfile文件及上下文构建新Docker镜像。构建上下文是指Dockerfile所在的本地路径或一个URL(Git仓库地址)。构建上下文环境会被递归处理,所以构建所指定的路径还包括子目录,而URL还包括其中指定的子模块。
构建会在Docker后台守护进程(daemon)中执行,而不是CLI中。构建前,构建进程会将全部内容(递归)发送到守护进程。通常,应该将一个空目录作为构建上下文环境,并放入Dockerfile文件。
在构建上下文中使用的Dockerfile文件是一个构建指令文件。为了提高构建性能,可以通过.dockerignore文件排除上下文目录下不需要的文件和目录。
在Docker构建镜像的第一步,docker CLI会先在上下文目录中寻找.dockerignore文件,根据.dockerignore 文件排除上下文目录中的部分文件和目录,然后把剩下的文件和目录传递给Docker服务。
Dockerfile文件一般位于构建上下文的根目录下,也可以通过-f指定Dockerfile文件的位置:
docker build -f /path/to/a/Dockerfile .
构建时,还可以通过-t参数指定构建成镜像的仓库、标签。如果存在多个仓库下,或使用多个镜像标签,就可以使用多个-t参数:
docker build -t nginx/v3:1.0.2 -t nginx/v3:latest .
在Docker守护进程执行Dockerfile中的指令前,首先会对Dockerfile进行语法检查,有语法错误时会返回错误提示信息。
Dockerfile文件:

FROM ubuntu:14.04  
ADD run.sh /  
VOLUME /data  
CMD ["./run.sh"]  

Dockerfile文件构建的容器如下:
Hello Docker(三)——Docker镜像制作

2、同构镜像构建

同构镜像构建是指镜像构建环境与运行环境兼容。
同构镜像构建一般要求编译环境与镜像所使用的base image是兼容的,比如在Ubuntu 14.04上编译应用,并将应用打入基于ubuntu系列base image的镜像。因为应用的编译环境与其部署运行的环境是兼容的,在Ubuntu 14.04下编译出来的应用,可以基本无缝地在基于ubuntu:14.04及后续版本base image镜像中运行;但在不完全兼容的base image中,比如CentOS中就可能会运行失败。

package main 

import ( 
        "net/http" 
        "log" 
        "fmt" 
) 

func home(w http.ResponseWriter, req *http.Request) { 
        w.Write([]byte("Welcome to this website!\n")) 
} 

func main() { 
        http.HandleFunc("/", home) 
        fmt.Println("Webserver start") 
        fmt.Println("  -> listen on port:1111") 
        err := http.ListenAndServe(":1111", nil) 
        if err != nil { 
                log.Fatal("ListenAndServe:", err) 
        } 
} 

编译:
go build -o httpserver httpserver.go 
Dockerfile文件:

From ubuntu:14.04 

COPY ./httpserver /root/httpserver 
RUN chmod +x /root/httpserver 

WORKDIR /root 
ENTRYPOINT ["/root/httpserver"] 

构建httpserver服务镜像:
docker build -t httpserver:latest . 
启动httpserver服务容器:
docker run httpserver 
基于ubuntu基础镜像构建出的应用镜像太过臃肿,因此有必要基于golang:latest构建自己专用的golang-builder image,Dockerfile.build可以用于build golang-builder image:

FROM golang:latest 
WORKDIR /go/src 
COPY httpserver.go . 
RUN go build -o httpserver ./httpserver.go 

构建golang-builder镜像:
docker build -t golang-builder:latest -f Dockerfile.build . 
从golang-builder创建一个容器appsource
docker create --name appsource golang-builder:latest
从appsource容器中将httpserver拷贝到主机当前目录
docker cp appsource:/go/src/httpserver ./
删除appsource容器
docker rm -f appsource
删除golang-builder镜像
docker rmi golang-builder:latest
从当前目录构建出httpserver镜像
docker build -t httpserver:latest .
httpserver镜像的大小依旧停留在200MB。要想减小httpserver镜像的大小,必须使用更小的base image,即alpine 。 Alpine image的大小不到4M,再加上应用的size,最终应用镜像的大小估计可以缩减到20M以下。
Dockerfile.alpine 文件:

From alpine:latest 

COPY ./httpserver /root/httpserver 
RUN chmod +x /root/httpserver 

WORKDIR /root 
ENTRYPOINT ["/root/httpserver"] 

构建alpine版应用镜像:
docker build -t httpserver-alpine:latest -f Dockerfile.alpine .
启动httpserver-alpine容器会失败,因为alpine image并非ubuntu环境的同构image。

3、异构镜像构建

异构镜像构建是指构建环境与运行环境不兼容。
Go将runtime中的C代码都用Go重写,对libc的依赖已经降到最低,但提供了两个版本的实现:C实现和Go实现。默认情况下,即在CGO_ENABLED=1情况下,程序和预编译的标准库都采用C实现。因此采用不同libc实现的debian系和alpine系自然存在不兼容的情况。考虑异构镜像创建首先对Go程序进行静态构建,然后将静态构建后的Go应用放入alpine image中。
Dockerfile.build文件如下:

FROM golang:alpine 

WORKDIR /go/src 
COPY httpserver.go . 

RUN go build -o httpserver ./httpserver.go 

构建builder镜像:

docker build -t myrepo/golang-static-builder:latest -f Dockerfile.build . 
docker create --name appsource golang-static-builder:latest 
docker cp appsource:/go/src/httpserver ./ 
docker rm -f appsource 
docker rmi golang-static-builder:latest 
docker build -t httpserver-alpine:latest -f Dockerfile.alpine . 

运行httpserver服务容器:
docker run httpserver-alpine:latest 
alpine版golang builder镜像Dockerfile:

FROM golang:alpine 

WORKDIR /go/src 
COPY httpserver.go . 

RUN go build -o httpserver ./httpserver.go 

三、Docker多阶段构建

1、Dockerfile多阶段构建

2017年5月发布的 Docker 17.05.0-ce 中,Docker官方提供了简便的多阶段构建(multi-stage build)方案。
对于多阶段构建,可以在Dockerfile中使用多个FROM语句。每个FROM指令可以使用不同的基础镜像,作为一个构建阶段,多条 FROM 就是多阶段构建,虽然最后生成的镜像只能是最后一个阶段的结果,但能够将前边阶段中的文件拷贝到后边的阶段中。
多阶段构建最大的使用场景是将编译环境和运行环境分离。

# 编译阶段
FROM golang:1.10.3

COPY server.go /build/

WORKDIR /build

RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOARM=6 go build -ldflags '-w -s' -o server

# 运行阶段
 FROM scratch

# 从编译阶段中拷贝编译结果到当前镜像中
COPY --from=0 /build/server /

ENTRYPOINT ["/server"]

Dockerfile的COPY指令--from=0参数,从前边的阶段中拷贝文件到当前阶段中,多个FROM语句时,0代表第一个阶段。除了使用数字,还可以给阶段命名,比如:

# 编译阶段
FROM golang:1.10.3 as builder

COPY server.go /build/

WORKDIR /build

RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOARM=6 go build -ldflags '-w -s' -o server

# 运行阶段
 FROM scratch

# 从编译阶段的中拷贝编译结果到当前镜像中
COPY --from=builder /build/server /

ENTRYPOINT ["/server"]

COPY –from指令从单独的image中复制,使用本地image名称,本地或Docker镜像仓库中可用的标记或标记ID。 

2、停在特定构建阶段

构建映像时,不一定需要构建整个Dockerfile文件的每个阶段,可以指定目标构建阶段。
docker build --target builder -t builder:latest .
使用Dockerfile构建,在builder阶段停止。
停在特定构建阶段非常适合的场景如下:
A、调试特定的构建阶段
B、在debug阶段,启用所有调试或工具,而在production阶段尽量精简
C、在testing阶段,应用程序将填充测试数据,但在production阶段则使用生产数据

3、Dockerfile多项目构建

利用多阶段构建可以多个项目的二进制文件构建在一个镜像中发布。

from debian as build-essential
arg APT_MIRROR
workdir /src

from build-essential as A
copy srcA .
run make

from build-essential as B
copy srcB .
run make

from alpine
copy --from=A binA .
copy --from=B binB .
cmd ...

四、C++ Docker镜像制作

1、C++应用开发

HelloDocker.cpp文件如下:

#include <iostream>

int main()
{
    std::cout << "Hello, Docker!" << std::endl;
    return 1;
}

2、查找C++镜像

docker search gcc
Hello Docker(三)——Docker镜像制作
包含多种版本的gcc,包括嵌入式版本的gcc-arm-embedded-docker

3、下载C++镜像

docker pull gcc
Hello Docker(三)——Docker镜像制作

4、gcc镜像查看

docker images

5、使用GCC镜像制作镜像

Dockerfile文件编写:

FROM gcc:latest
RUN  mkdir -p /home/user/docker/src/HelloDocker
COPY HelloDocker.cpp /home/user/docker/src/HelloDocker
WORKDIR /home/user/docker/src/HelloDocker
RUN  g++ HelloDocker.cpp -o HelloDocker
CMD ["./HelloDocker"]

使用Dockerfile文件创建镜像:
docker build -t hellodocker:v1 .
Hello Docker(三)——Docker镜像制作
镜像查看:
docker images
启动镜像:
docker run -d hellodocker:v1
容器运行情况查看
docker ps

6、使用可执行程序制作镜像

Dockerfile文件编写:

FROM gcc:latest
RUN  mkdir -p /home/user/docker/HelloDocker
COPY HelloDocker /home/user/docker/HelloDocker
WORKDIR /home/user/docker/HelloDocker
#RUN  g++ HelloDocker.cpp -o HelloDocker
CMD ["./HelloDocker"]

构建镜像:
docker build -t hellodocker:v1 .
启动容器:
docker run -d hellodocker:v1