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关键字

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

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

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

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构建镜像
dockerfile 复制代码
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"]
sh 复制代码
docker build -t httphello:1.0.0 -f Dockerfile .
运行测试
sh 复制代码
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

查看镜像

sh 复制代码
[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

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关键词,为构建阶段指定别名,也提⾼了可读性

dockerfile 复制代码
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"]

构建镜像

sh 复制代码
docker build -t httphello:1.0.0 -f Dockerfile .

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

sh 复制代码
[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
运行测试
sh 复制代码
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语法

dockerfile 复制代码
COPY <src> <dest> //将上下⽂中源⽂件,拷⻉到⽬标⽂件
COPY prefix* /destDir/ //将所有prefix 开头的⽂件拷⻉到 destDir ⽬录下
COPY prefix?.log /destDir/ //⽀持单个占位符,例如 : prefix1.log、
prefix2.log 等
  • 对于⽬录⽽⾔,COPY 和 ADD 命令具有相同的特点:只复制⽬录中的内容⽽不包含⽬录⾃身
dockerfile 复制代码
COPY srcDir /destDir/ //只会将源⽂件夹srcDir下的⽂件拷⻉到 destDir ⽬录下
  • COPY 区别于ADD在于Dockerfile中使⽤multi-stage。可以拷贝不同阶段的文件
dockerfile 复制代码
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语法

dockerfile 复制代码
ADD <src> <dest>

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

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

例如把nginx打包进镜像里

dockerfile 复制代码
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"]

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

sh 复制代码
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)指令⾥⽤双引号的形式标明:

dockerfile 复制代码
ADD "space file.txt" "/tmp/space file.txt"

CMD 和 ENTRYPOINT

CMD
dockerfile 复制代码
# shell 格式
CMD <command>
# 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指令中的命令。

例子,前文也提到

dockerfile 复制代码
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指令有两种格式

dockerfile 复制代码
# shell 格式
ENTRYPOINT <command>
# 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。

dockerfile 复制代码
# 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参数
sh 复制代码
#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一起使用

dockerfile 复制代码
FROM alpine:latest
ENTRYPOINT ["echo"]
CMD ["Hello, World!"]

参数替换

sh 复制代码
docker run --entrypoint "echo" <image-name> "Hello, Docker!"

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

相关推荐
SilentSamsara9 小时前
存储卷体系:EmptyDir/HostPath/PV/PVC/StorageClass 的选型决策树
服务器·微服务·云原生·容器·架构·kubernetes·k8s
王的宝库10 小时前
【K8s】集群安全机制(二):授权(Authorization)详解与实战
学习·云原生·容器·kubernetes
indexsunny10 小时前
互联网大厂Java面试实录:微服务+Spring Boot在电商场景中的应用
java·spring boot·redis·微服务·eureka·kafka·spring security
东北甜妹12 小时前
Docker 容器故障排查
云原生·eureka
Shining059613 小时前
QEMU 编译开发环境搭建
人工智能·语言模型·自然语言处理·云原生·qemu·vllm·华为昇腾
匀泪1 天前
云原生(Kubernetes service微服务)
微服务·云原生·kubernetes
倔强的胖蚂蚁1 天前
Ollama Modelfile 配置文件 全指南
云原生·开源
AutoMQ1 天前
AWS 新发布的 S3 Files 适合作为 Kafka 的存储吗?
云原生·消息队列·云计算
MY_TEUCK2 天前
从零开始:使用Sealos Devbox快速搭建云原生开发环境
人工智能·spring boot·ai·云原生·aigc
没有口袋啦2 天前
《基于 GitOps 理念的企业级自动化 CI/CD 流水线》
阿里云·ci/cd·云原生·自动化·k8s