如何优雅的写Dockerfile

前几天疯狂爆写dockerfile,被折磨成🐕了,写个文章来分享记录下一些坑,以及Docker17.05的新的写法,能够最大程度的节约空间。

Docker镜像层

还是从刚开始说起,经常出题的老哥都知道安恒,i春秋这种收题的时候都是尽可能地建议将RUN这种指令写成一行,通过&&\的方式。简单粗暴的原因就是这种方法能够节约一定的占用空间。这里就涉及到一个问题,就是为什么这么做

当我们在dockerhub查看一个镜像的具体版本的时候,点进去是可以看见image layers的,例如

图片[1]-如何优雅的写Dockerfile-魔法少女雪殇
图片[2]-如何优雅的写Dockerfile-魔法少女雪殇

基本所有的层都会显示在这里,右边则是他的一个大小,当然在本地也是可以查看的

通过

docker history <images name>
图片[3]-如何优雅的写Dockerfile-魔法少女雪殇

效果基本一致

这里就顺势引出,docker他的镜像层就跟git的commit比较像的。

好处他的层是用来保存镜像与上一版本和当前版本的差异,这样当你在下载镜像的时候只会去请求你尚未拥有的层,来节省时间。

当然归根结底,每一层都是张勇额外的容量的,你拥有的层越多,最终的镜像也就越大,虽然大部分情况下我们自己的调试不太需要在意每一层的大小和容量,但是在实际场景的话,能够节约容量还是尽可能地节约较好。

常规的方式就是将多个RUN语句组合在一行中,例如

RUN apt install update
RUN apt install upgrade

写成

RUN apt install update && apt install upgrade

这是非常常见的写法,一般用于在把源码丢入容器路径中然后结合make或者npm install等相关编译操作所需要的前置环境。

Docker通过多阶段构筑来缩小磁盘占用

Docker在17版本的时候新增了一个多阶段构筑的操作(然而现在20版本),当然我也是最近才知道,前段日子在复现国外比赛题目的时候,发现他们的docker写了两个FROM,稍微了解了一下,这下发现新大陆了。

还是按照基础操作演示一遍和对比,这里拿golang举例

首先写个小代码

package main


import "github.com/gin-gonic/gin"


func main() {
	r := gin.Default()
	r.GET("/", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "Hello World",
		})
	})
	r.Run(":8080")
}

那么常规的golang的docker我们一般会这样写。

FROM golang:latest

WORKDIR /usr/src/app

ENV GOPROXY https://goproxy.cn,direct

COPY go.mod .
RUN go mod download && go mod verify

COPY main.go .
COPY go.sum .
RUN go build -v -o /usr/local/bin/app ./...

EXPOSE 8080

CMD ["app"]

可以看到占用是比较大的

图片[4]-如何优雅的写Dockerfile-魔法少女雪殇
图片[5]-如何优雅的写Dockerfile-魔法少女雪殇

并且运行了超多操作,而且golang的本体官方镜像也是非常大的,这是很不ao的。

所以我们可以采取多阶段构筑的方式。

首先,golang编译后的本体是elf文件,是可以直接被其他镜像所运行的,这里我这样写

FROM golang:latest as builder

WORKDIR /usr/src/app

ENV GOPROXY https://goproxy.cn,direct

COPY go.mod .
RUN go mod download && go mod verify

COPY main.go .
COPY go.sum .
RUN go build -v -o /usr/local/bin/app ./...

FROM ubuntu:18.04 as runner

WORKDIR /root/

COPY --from=0 /usr/local/bin/app .

EXPOSE 8080

CMD ["./app"]

运行后是完全一致的。可以看下大小

图片[6]-如何优雅的写Dockerfile-魔法少女雪殇

非常惊人的达到了72m,比原本足足节省了一个g

重点就是在COPY指令的–from参数,我们来看下官方文档

图片[7]-如何优雅的写Dockerfile-魔法少女雪殇

简单来说,就是可以通过从其他镜像进行复制里面的文件出来。上述内容中的–from=0就意味着从第一个from种复制文件出来到当前from,dockerfile运行结束后,第一个from则会被销毁。

当然你可能注意到了上面FROM golang:latest as builder,他就类似把其单独进行了一个标签化,下面运行COPY的时候这么些也可以

COPY --from=builder /usr/local/bin/app .

效果是一致的。

在后面的构筑中也可以单独对其进行构筑,增加–tartget builder参数

docker build --target builder -t username/imagename:tag .

Docker进一步缩小容量

alpine是一个我经常能够在国外的一些题目dockrfile中所遇到的,给我的感觉就是很奇怪,但是同样他很小,它也拥有shell,我稍微百度查了一圈

这东西对比常规的ubuntu等内容,内核是不相同的,也就是说前者基于glibc,后者基于muslc,具体区别这个我就没有深入研究了,只是这个东西作为底包它的容量是比ubuntu还是要更小的。

我们把上面案例中的ubuntu换成alpine试试

FROM golang:latest as builder

WORKDIR /usr/src/app

ENV GOPROXY https://goproxy.cn,direct

COPY go.mod .
RUN go mod download && go mod verify

COPY main.go .
COPY go.sum .
RUN go build -v -o /usr/local/bin/app ./...

FROM alpine:3.14

WORKDIR /root/

COPY --from=0 /usr/local/bin/app .

EXPOSE 8080

CMD ["./app"]
图片[8]-如何优雅的写Dockerfile-魔法少女雪殇

它的容量甚至来到了惊人的14.84mb。。。这太恐怖了。

小结

总之,关于多阶段构筑这件事情多少有点火星,毕竟都20版本了(,但是碍于国内这类文章比较少,我也是翻阅了一些国外的博客来小水了文章。其中关于对cpp相关的编译之类的内容这里作非专业人士就不来写了,欢迎留言写在下面,我觉得这个功能是适合学习的,毕竟节约内存谁不爱呢.jpg

同时他的上限是极高的,不论是其他语言还是生产环境部署,都可以通过该操作进行对其进行修改的。

ubuntu相关镜像快速换源

RUN sed -i 's/http:\/\/archive.ubuntu.com\/ubuntu\//http:\/\/mirrors.163.com\/ubuntu\//g' /etc/apt/sources.list && apt-get update

复制到dockerfile最上面即可

快速安装java环境支持

先去java官网下载需要的版本jdk,放入同目录jre文件就行了

RUN mkdir /usr/local/java


ADD jre/jre-8u111-linux-x64.tar.gz /usr/local/java/


RUN ln -s /usr/local/java/jre1.8.0_111 /usr/local/java/jdk

ENV JAVA_HOME /usr/local/java/jdk
ENV JRE_HOME ${JAVA_HOME}/jre
ENV CLASSPATH .:${JAVA_HOME}/lib:${JRE_HOME}/lib
ENV PATH ${JAVA_HOME}/bin:$PATH

后续有新的坑和新的发现再写。

© 版权声明
THE END
喜欢就支持一下吧
点赞32 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情