dockerfile——镜像构建工具详解及案例

Dockerfile

Dockerfile是⼀个创建镜像所有命令的⽂本⽂件, 包含了⼀条条指令和说明, 每条指令构建⼀层, 通过docker build命令,根据Dockerfile的内容构建镜像,因此每⼀条指令的内容, 就是描述该层如何构建.有了Dockefile, 就可以制定⾃⼰docker镜像规则,只需要在Dockerfile上添加或者修改指令, 就可⽣成docker 镜像.

docker build 流程简介

docker build命令会读取Dockerfile的内容,并将Dockerfile的内容发送给 Docker 引擎,最终Docker 引擎会解析Dockerfile中的每⼀条指令,构建出需要的镜像。

  • 第⼀步,docker build会将 context 中的⽂件打包传给 Docker daemon。如果 context 中有.dockerignore⽂件,则会从上传列表中删除满⾜.dockerignore规则的⽂件。注意:如果上下⽂中有相当多的⽂件,可以明显感受到整个⽂件发送过程
  • 第二步,docker build命令向 Docker server 发送 HTTP 请求,请求 Docker server 构建镜像,请求中包含了需要的 context 信息。
  • 第三步,Docker server 接收到构建请求之后,会执⾏以下流程来构建镜像:
    • 创建⼀个临时⽬录,并将 context 中的⽂件解压到该⽬录下。
    • 读取并解析 Dockerfile,遍历其中的指令,根据命令类型分发到不同的模块去执⾏。
    • Docker 构建引擎为每⼀条指令创建⼀个临时容器,在临时容器中执⾏指令,然后 commit 容器,⽣成⼀个新的镜像层。
    • 最后,将所有指令构建出的镜像层合并,形成 build 的最后结果。最后⼀次 commit ⽣成的镜像 ID就是最终的镜像 ID。

为了提⾼构建效率,docker build默认会缓存已有的镜像层。如果构建镜像时发现某个镜像层已经被缓存,就会直接使⽤该缓存镜像,⽽不⽤重新构建。如果不希望使⽤缓存的镜像,可以在执⾏docker build命令时,指定--no-cache=true参数。

Docker 匹配缓存镜像的规则
遍历缓存中的基础镜像及其⼦镜像,检查这些镜像的构建指令是否和当前指令完全⼀致,如果不⼀样,则说明缓存不匹配。对于ADD、COPY指令,还会根据⽂件的校验和(checksum)来判断添加到镜像中的⽂件是否相同,如果不相同,则说明缓存不匹配。缓存匹配检查不会检查容器中的⽂件。⽐如,当使⽤RUN apt-get -y update命令更新了容器中的⽂件时,缓存策略并不会检查这些⽂件,来判断缓存是否匹配。最后,可以通过docker history命令来查看镜像的构建历史

Dockerfile关键字

FROM 设置镜像使⽤的基础镜像
MAINTAINER 设置镜像的作者
RUN 编译竟像时运⾏的脚步
CMD 设置容器的启动命令
LABEL 设置镜像标签
EXPOSE 设置镜像暴露的端⼝
ENV 设置容器的环境变量
ADD 编译镜像时复制上下⽂中⽂件到镜像中
COPY 编译镜像时复制上下⽂中⽂件到镜像中
ENTRYPOINT 设置容器的⼊⼝程序
VOLUME 设置容器的挂载卷
USER 设置运⾏ RUN CMD ENTRYPOINT的⽤户名
WORKDIR 设置 RUN CMD ENTRYPOINT COPY ADD 指令的⼯作⽬录
ARG 设置编译镜像时加⼊的参数
ONBUILD 设置镜像的ONBUILD 指令
STOPSIGNAL 设置容器的退出信号量

案例

素材

一个简单的http服务器,打印启动参数和一些环境变量

hello目录下

printEnv.go

package hello

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

type EnvParam struct {
}

func (*EnvParam) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	env1 := os.Getenv("env1")
	env2 := os.Getenv("env2")
	fmt.Printf("env list : env1 = %s and env2 = %s", env1, env2)
	fmt.Println()
	fmt.Fprintf(w, "env list : env1 = %s and env2 = %s", env1, env2)
}

printStartParam.go

package hello

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

const (
	defaultStartUpParam = "default"
)

var (
	param1 = flag.String("param1", defaultStartUpParam, "param1 to hello world")
	param2 = flag.String("param2", defaultStartUpParam, "param2 to hello world")
)

func init() {
	flag.Parse()
}

type PrintStartParam struct {
}

func (*PrintStartParam) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Printf("start up params:     param1 = %s and param2 = %s ", *param1, *param2)
	fmt.Println()
	fmt.Fprintf(w, "start up params:     param1 = %s and param2 = %s ", *param1, *param2)
}

main.go

package main

import (
	"fmt"
	"httpServer/hello"
	"net/http"
)

func main() {
	fmt.Println("into main")
	http.Handle("/print/env", new(hello.EnvParam))
	http.Handle("/print/startup", new(hello.PrintStartParam))
	http.ListenAndServe(":80", nil)
	//	http.ListenAndServeTLS()
}

docker build构建镜像
FROM golang:1.18
ENV env1=env1value
ENV env2=env2value
MAINTAINER dongya
LABEL hello 1.0.0
#也可以用git拉取代码 RUN git clone https://gitee.com/dongyademo/helloworld.git
#这里从上下文中拷贝文件
COPY ./httpServer /go/src/httpServer
WORKDIR /go/src/httpServer
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app .
EXPOSE 80
CMD ["./app","--param1=p1","--param2=p2"]

docker build -t httphello:1.0.0 -f Dockerfile .
运行测试
docker run -p 8081:80 -d --name testServer httphello:1.0.0

[root@localhost example3]# curl http://localhost:8081/print/env
env list : env1 = env1value and env2 = env2value

查看镜像

[root@localhost example3]# docker images
REPOSITORY                        TAG       IMAGE ID       CREATED         SIZE
httphello                         1.0.0     8ae621256ce2   23 hours ago    988MB
alpine                            latest    f8c20f8bbcb6   2 weeks ago     7.38MB

这破镜像居然有988MB。最终运行的时候就运行一个二进制文件,所以编译时候的环境我们最终运行的时候是基本不需要的。如何缩小打出来的镜像呢呢,可以考虑用多阶段构建

多阶段构建

Docker 17.05版本以后,新增了Dockerfile多阶段构建。所谓多阶段构建,实际上是允许⼀个Dockerfile 中出现多个 FROM 指令。这样做有什么意义呢?

多个 FROM 指令的意义

多个 FROM 指令并不是为了⽣成多根的层关系,最后⽣成的镜像,仍以最后⼀条 FROM 为准,之前的 FROM 会被抛弃,那么之前的FROM ⼜有什么意义呢?

每⼀条 FROM 指令都是⼀个构建阶段,多条 FROM 就是多阶段构建,虽然最后⽣成的镜像只能是最后⼀个阶段的结果,但是,能够将前置阶段中的⽂件拷⻉到后边的阶段中,这就是多阶段构建的最⼤意义。

最⼤的使⽤场景是将编译环境和运⾏环境分离,⽐如,之前我们需要构建⼀个Go语⾔程序,那么就需要⽤到go命令等编译环境

Dockerfile

FROM golang:1.18
ADD ./httpServer /go/src/httpServer/
WORKDIR /go/src/httpServer
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app .

FROM alpine:latest
ENV env1=env1value
ENV env2=env2value
MAINTAINER dongya
LABEL hello 1.0.0 # 这个label 不是打包出来的镜像tag,只是config里的字段
WORKDIR /app/
#--from=0代表从阶段0进行操作
COPY --from=0 /go/src/httpServer/app ./
EXPOSE 80
CMD ["./app","--param1=p1","--param2=p2"]

除了用--from=0指定阶段,还可以通过as关键词,为构建阶段指定别名,也提⾼了可读性

FROM golang:1.18 as stage0
ADD ./httpServer /go/src/httpServer/
WORKDIR /go/src/httpServer
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app .

FROM alpine:latest
ENV env1=env1value
ENV env2=env2value
MAINTAINER dongya
LABEL hello 1.0.0
WORKDIR /app/
#--from=stage0代表从阶段stage0进行操作
COPY --from=stage0 /go/src/httpServer/app ./
EXPOSE 80
CMD ["./app","--param1=p1","--param2=p2"]

构建镜像

docker build -t httphello:1.0.0 -f Dockerfile .

查看镜像,这个时候我们发现打出来的镜像就小很多了,因为是基于一个轻量级linux alpine打包出来的。多阶段构建基于最后一个from来构建

[root@localhost example3]# docker images
REPOSITORY                        TAG       IMAGE ID       CREATED         SIZE
helloserver                       1.0.2     3eaa5b73a5b4   4 hours ago     13.9MB
httphello                         1.0.0     8ae621256ce2   23 hours ago    988MB
alpine                            latest    f8c20f8bbcb6   2 weeks ago     7.38MB

运行测试
docker run -p 8081:80 -d --name testServer helloserver:1.0.2

[root@localhost example3]# curl http://localhost:8081/print/env
env list : env1 = env1value and env2 = env2value

ADD和COPY

  • ADD 与 COPY 不能拷⻉上下⽂以外的⽂件
  • COPY 命令语法格式

COPY语法

COPY   //将上下⽂中源⽂件,拷⻉到⽬标⽂件
COPY prefix* /destDir/ //将所有prefix 开头的⽂件拷⻉到 destDir ⽬录下
COPY prefix?.log /destDir/ //⽀持单个占位符,例如 : prefix1.log、
prefix2.log 等
  • 对于⽬录⽽⾔,COPY 和 ADD 命令具有相同的特点:只复制⽬录中的内容⽽不包含⽬录⾃身
COPY srcDir /destDir/ //只会将源⽂件夹srcDir下的⽂件拷⻉到 destDir ⽬录下
  • COPY 区别于ADD在于Dockerfile中使⽤multi-stage。可以拷贝不同阶段的文件
FROM golang:1.18 as stage0
ADD ./httpServer /go/src/httpServer/
WORKDIR /go/src/httpServer
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app .

FROM alpine:latest
ENV env1=env1value
ENV env2=env2value
MAINTAINER dongya
LABEL hello 1.0.0
WORKDIR /app/
#--from=stage0代表从阶段stage0进行操作
COPY --from=stage0 /go/src/httpServer/app ./
EXPOSE 80
CMD ["./app","--param1=p1","--param2=p2"]

ADD语法

ADD  

ADD 命令除了不能⽤在 multistage 的场景下,ADD 命令可以完成 COPY 命令的所有功能,并且还可以完成两类的功能:

  • 解压压缩⽂件并把它们添加到镜像中,对于宿主机本地压缩⽂件,ADD命令会⾃动解压并添加到镜像
  • 从 url 拷⻉⽂件到镜像中,需要注意:url 所在⽂件如果是压缩包,ADD 命令不会⾃动解压缩

例如把nginx打包进镜像里

FROM golang:1.18 as stage0
ADD ./httpServer /go/src/httpServer/
WORKDIR /go/src/httpServer
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app .

FROM alpine:latest
ENV env1=env1value
ENV env2=env2value
MAINTAINER dongya
LABEL hello 1.0.0
ADD https://nginx.org/download/nginx-1.21.6.tar.gz /soft/
COPY nginx-1.21.6.tar.gz /soft/copy/
ADD nginx-1.21.6.tar.gz /soft/add/
WORKDIR /app/
#--from=stage0代表从阶段stage0进行操作
COPY --from=stage0 /go/src/httpServer/app ./
EXPOSE 80
CMD ["./app","--param1=p1","--param2=p2"]

启动进容器里查看对应目录即可

docker build --no-cache -t helloserver:1.0.3 -f Dockerfile .
docker run -p 8081:80 -d --name helloserver helloserver:1.0.3
docker exec -it d873894c0493 /bin/sh

注意:ADD⽬标⽂件位置要注意路径后⾯是否带 “/” ,带斜杠表示⽬录,不带斜杠表示⽂件名⽂件名⾥带有空格,需要再 ADD(或COPY)指令⾥⽤双引号的形式标明:

ADD "space file.txt" "/tmp/space file.txt"

CMD 和 ENTRYPOINT

CMD
# shell 格式
CMD 
# exec格式,推荐格式
CMD ["executable","param1","param2"]
# 为ENTRYPOINT 指令提供参数
CMD ["param1","param2"]

CMD 指令提供容器运⾏时的默认值,这些默认值可以是⼀条指令,也可以是⼀些参数。⼀个dockerfile中可以有多条CMD指令,但只有最后⼀条CMD指令有效。CMD参数格式是在CMD指令与ENTRYPOINT指令配合时使⽤,CMD指令中的参数会添加到ENTRYPOINT指令中。使⽤shell 和exec 格式时,命令在容器中的运⾏⽅式与RUN 指令相同。不同在于,RUN指令在构建镜像时执⾏命令,并⽣成新的镜像。CMD指令在构建镜像时并不执⾏任何命令,⽽是在容器启动时默认将CMD指令作为第⼀条执⾏的命令。如果在命令⾏界⾯运⾏docker run 命令时指定命令参数,则会覆盖CMD指令中的命令。

例子,前文也提到

FROM golang:1.18 as stage0
ADD ./httpServer /go/src/httpServer/
WORKDIR /go/src/httpServer
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app .

FROM alpine:latest
ENV env1=env1value
ENV env2=env2value
MAINTAINER dongya
LABEL hello 1.0.0
WORKDIR /app/
#--from=stage0代表从阶段stage0进行操作
COPY --from=stage0 /go/src/httpServer/app ./
EXPOSE 80
CMD ["./app","--param1=p1","--param2=p2"]

ENTRYPOINT

ENTRYPOINT指令有两种格式

# shell 格式
ENTRYPOINT 
# exec 格式,推荐格式
ENTRYPOINT ["executable","param1","param2"]

ENTRYPOINT指令和CMD指令类似,都可以让容器每次启动时执⾏相同的命令,但它们之间⼜有不同。⼀个Dockerfile中可以有多条ENTRYPOINT指令,但只有最后⼀条ENTRYPOINT指令有效。当使⽤shell格式时,ENTRYPOINT指令会忽略任何CMD指令和docker run 命令的参数,并且会运⾏在bin/sh -c。推荐使⽤exec格式,使⽤此格式,docker run 传⼊的命令参数将会覆盖CMD指令的内容并且附加到ENTRYPOINT指令的参数中。

CMD可以是参数,也可以是指令,ENTRYPOINT只能是命令;docker run 命令提供的运⾏命令参数可以覆盖CMD,但不能覆盖ENTRYPOINT。

# syntax=docker/dockerfile:1
FROM golang:1.18
ENV env1=env1value
ENV env2=env2value
MAINTAINER nick
LABEL hello 1.0.0
RUN git clone https://gitee.com/nickdemo/helloworld.git
WORKDIR helloworld
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app .
EXPOSE 80
#ENTRYPOINT ["./app"]
#ENTRYPOINT ./app --param1=p11.0.3 --param2=p2
ENTRYPOINT ["./app","--param1=p1","--param2=p2"]
#CMD ["./app","--param1=p1","--param2=p2"]
#CMD ./app --param1=p1 --param2=p2
CMD ["--param1=p1","--param2=p2"]
替换CMD和ENTRYPOINT参数
#shell形式
docker run <image-name> --cmd "param1 param2 param3"
docker run --entrypoint "command" <image-name> "param1" "param2" "param3"
#exec命令形式
docker run --entrypoint '["command", "param1", "param2", "param3"]' <image-name>
docker run <image-name> --cmd '["command", "param1", "param2", "param3"]'

CMD和ENTRYPOINT一起使用

FROM alpine:latest
ENTRYPOINT ["echo"]
CMD ["Hello, World!"]

参数替换

docker run --entrypoint "echo" <image-name> "Hello, Docker!"

--entrypoint 参数必须在最前面,紧随其后的是镜像名称,然后是要传递的命令及参数

你可能感兴趣的:(eureka,云原生)