镜像大小其实是衡量我们容器打包技术的重要指标,我们应该在不影响应用正常运行的情况下,尽量让我们的容器镜像变得更小,这样,不管是从安全还是维护效率角度来讲,都是最佳实践。
本文我们从两种情况阐述我们的问题和解决方案,我们从实现我们的application的编程语言角度,按照语言是解释型还是编译型语言来演示如何解决容器镜像体积大的问题。
1 解释型语言
大部分的脚本语言都是解释型语言,像Ruby,Python,PHP等,我们只需要把我们的代码扔给解释器,解释器去运行就好了,但是这里的解释器有大有小,为什么? 首先Docker为一些“懒人”准备了一种Docker Image,比如Python,那么我们可以非常方便的从DockerHub上拉取某一个版本的Python镜像,比如Python3。6我们会看到,同样是3.6.5, alpine linux的docker image就小了非常非常多,是普通的python3.6.5十分之一大小,为什么呢?主要是因为为了照顾“懒人”,Python:3.6.5 image预先安装了大量python的工具和编译的头文件等,包括C的编译环境等等。 而python:3.6.5-alpine 基本上除了Linux系统必须的一些文件以外,基本只包含基本的Python运行环境,例如像 gcc 等工具是不会预先安装的,都是用户需要的时候自行安装。 所以进一步来讲,我们要打包我们的Python应用,使用以上这两种base image的效果就显而易见了,一个打包完的image会上G大小,另一个只有100M左右。 例如下面是一个python flask的Dockerfile
$ docker image ls
python 3.6.5 a5b7afcfdcc8 2 weeks ago 912MB
python 3.6.5-alpine 27e79c0fa4d2 2 months ago 87.4MB
FROM python:3.6-alpine
LABEL maintainer="XYZ <xxx@xxx.com>"
RUN apk add --no-cache gcc musl-dev
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
EXPOSE 8000
CMD []
2 编译型语言
使用编译型语言(例如C,Go等)编写的应用程序打包成Docker镜像,这里面的优化空间就更大了,我们以Go语言为例。 假如我们有一个Go APP,假如使用普通的go image,那么我们构建出来的镜像会很大,例如这个app https://github.com/golang/example/blob/master/outyet/Dockerfiledockerfile
FROM golang:onbuild
EXPOSE 8080
构建完的docker image 700多M。
然后利用前面我们讲的,我们可以替换base image,选择一个alpine的base image,例如:
$ docker images go-demo
REPOSITORY TAG IMAGE ID CREATED SIZE
go-demo latest f562d6efa39c 21 seconds ago 707MB
构建的image,只有不到400M。
FROM golang:alpine
WORKDIR /app
ADD . /app
RUN cd /app && go build -o goapp
EXPOSE 8080
ENTRYPOINT ./goapp
但是,我们还可以让我们的image变得更小。根据我们之前讲的分阶段build,我们可以分阶段build我们的APP,然后最终只需要在一个很小的image里,运行我们程序编译后的结果即可,例如
$ docker images go-demo
REPOSITORY TAG IMAGE ID CREATED SIZE
go-demo latest f562d6efa39c 3 minutes ago 707MB
$ docker images go-demo-alpine
REPOSITORY TAG IMAGE ID CREATED SIZE
go-demo-alpine latest a16d2986dbd1 9 seconds ago 385MB
这样,又能省掉一部分空间。只有13M,惊不惊喜!!!
FROM golang:alpine AS build-env
WORKDIR /app
ADD . /app
RUN cd /app && go build -o goapp
FROM alpine
RUN apk update && \
apk add ca-certificates && \
update-ca-certificates && \
rm -rf /var/cache/apk/*
WORKDIR /app
COPY --from=build-env /app/goapp /app
EXPOSE 8080
ENTRYPOINT ./goapp
$ docker images go-demo-muti-build
REPOSITORY TAG IMAGE ID CREATED SIZE
go-demo latest f562d6efa39c 3 minutes ago 707MB
go-demo-alpine latest a16d2986dbd1 9 seconds ago 385MB
go-demo-muti-build latest 1b1237a8fe0e 20 seconds ago 13.5MB
3 总结
所以,我们每次build自己的docker image的时候,一定要思考一下,怎么才能让我们的docker image变得更加小巧,更小的image其实也是更安全的,因为冗余的软件包少,那么漏洞就相应的少,另外小的docker image方便移动,不管是docker push还是pull,速度都很快。 快看一看自己手头上的docker image,有没有优化的空间吧!4 Docker 镜像缩小神器
4.1 部署
$ wget https://mirror.ghproxy.com/https://github.com/slimtoolkit/slim/releases/download/1.40.11/dist_linux.tar.gz
$ tar -xvf dist_linux.tar.gz
$ mv dist_linux/slim /usr/local/bin/
$ mv dist_linux/slim-sensor /usr/local/bin/
# 后续发布新版本之后直接使用命令进行更新
$ slim update
4.2 使用
# step1:查看镜像id
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest e0c9858e10ed 10 days ago 188MB
# step2:缩小镜像
$ slim build e0c9858e10ed
cmd=build info=param.http.probe message='using default probe'
cmd=build state=started
cmd=build info=params target.image='e0c9858e10ed'continue.mode='probe' rt.as.user='true' keep.perms='true' tags='' image-build-engine='internal' target.type='image'
cmd=build state=image.inspection.start
cmd=build info=image id='sha256:e0c9858e10ed8be697dc2809db78c57357ffc82de88c69a3dee5d148354679ef' size.bytes='187669514' size.human='188 MB'
cmd=build info=image.stack index='0' name='nginx:latest'id='sha256:e0c9858e10ed8be697dc2809db78c57357ffc82de88c69a3dee5d148354679ef'
cmd=build info=image.exposed_ports list='80/tcp'
cmd=build state=image.inspection.done
cmd=build state=container.inspection.start
cmd=build info=container status='created' name='slimk_1215_20240701055220'id='29fdbecafdc938cbbe505e071d7d6ce34a14a0bfeb6c5d463211b59fab4a8162'
cmd=build info=container status='running' name='slimk_1215_20240701055220'id='29fdbecafdc938cbbe505e071d7d6ce34a14a0bfeb6c5d463211b59fab4a8162'
cmd=build info=container message='obtained IP address' ip='172.17.0.2'
time="2024-07-01T05:52:21Z" level=error msg="channel.Client.Read: read error (read tcp 127.0.0.1:57720->127.0.0.1:49154: read: connection reset by peer), exiting..."
time="2024-07-01T05:52:21Z" level=error msg="channel.NewCommandClient: channel verify error = read tcp 127.0.0.1:57720->127.0.0.1:49154: read: connection reset by peer"
cmd=build info=cmd.startmonitor status='sent'
cmd=build info=event.startmonitor.done status='received'
cmd=build info=container name='slimk_1215_20240701055220'id='29fdbecafdc938cbbe505e071d7d6ce34a14a0bfeb6c5d463211b59fab4a8162' target.port.list='49155' target.port.info='80/tcp => 0.0.0.0:49155' message='YOU CAN USE THESE PORTS TO INTERACT WITH THE CONTAINER'
cmd=build state=http.probe.starting message="WAIT FOR HTTP PROBE TO FINISH"
cmd=build info=continue.after mode='probe' message='no input required, execution will resume when HTTP probing is completed'
cmd=build prompt='waiting for the HTTP probe to finish'
cmd=build state=http.probe.running
cmd=build info=http.probe.ports count='1' targets='49155'
cmd=build info=http.probe.commands count='1' commands='GET /'
cmd=build info=http.probe.call target='http://127.0.0.1:49155/' attempt='1' error='none' time='2024-07-01T05:52:35Z' status='200' method='GET'
cmd=build info=http.probe.summary total='1' failures='0' successful='1'
cmd=build state=http.probe.done
cmd=build info=http.probe.crawler url='http://127.0.0.1:49155/' page='0'
cmd=build info=probe.crawler.done addr='http://127.0.0.1:49155/'
cmd=build info=event message='HTTP probe is done'
cmd=build state=container.inspection.finishing
cmd=build state=container.inspection.artifact.processing
cmd=build state=container.inspection.done
cmd=build state=building message="building optimized image" engine=internal
cmd=build state=completed
cmd=build info=results size.optimized='13 MB' status='MINIFIED' by='14.11X' size.original='188 MB'
cmd=build info=results has.data='true' image-build-engine='internal' image.name='nginx.slim' image.size='13 MB' image.id='sha256:ed7eee6e6af9272dbfa18fb7e62b90352d97f4fba2be8def9b4bc8fd6da2bda7' image.digest='sha256:2e94ff7052fbc93de1e141ccf0b4712bf8f9065a9d9dbb2713a03c2fa3f6dff5'
cmd=build info=results artifacts.location='/tmp/slim-state/.slim-state/images/e0c9858e10ed8be697dc2809db78c57357ffc82de88c69a3dee5d148354679ef/artifacts'
cmd=build info=results artifacts.report='creport.json'
cmd=build info=results artifacts.dockerfile.reversed='Dockerfile.reversed'
cmd=build info=results artifacts.seccomp='nginx-seccomp.json'
cmd=build info=results artifacts.apparmor='nginx-apparmor-profile'
cmd=build state=done
cmd=build info=commands message='use the xray command to learn more about the optimize image'
cmd=build info=report file='slim.report.json'
app='slim' message='GitHub Discussions' info='https://github.com/slimtoolkit/slim/discussions'
app='slim' message='Join the CNCF Slack channel to ask questions or to share your feedback' info='https://cloud-native.slack.com/archives/C059QP1RH1S'
app='slim' message='Join the Discord server to ask questions or to share your feedback' info='https://discord.gg/9tDyxYS'
app='slim' message='Join the Gitter channel to ask questions or to share your feedback' info='https://gitter.im/docker-slim/community'
# step3:查看镜像大小
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx.slim latest ed7eee6e6af9 6 seconds ago 13.3MB
nginx latest e0c9858e10ed 10 days ago 188MB
# step4:运行新镜像测试
$ docker run -d -p 8080:80 nginx.slim
3e8226ddeb35563fa14fd5ec9badaf35a0503be367923724497fa5ffbb1a5d18
$ curl -I http://127.0.0.1:8080
HTTP/1.1200 OK
Server: nginx/1.27.0
Date:Mon,01Jul202405:57:32 GMT
Content-Type: text/html
Content-Length:615
Last-Modified:Tue,28May202413:22:30 GMT
Connection: keep-alive
ETag:"6655da96-267"
Accept-Ranges: bytes
4.3 总结
- slim 不会更改 Docker 容器映像中的任何内容并将其缩小多达 30 倍。
- slim 通过使用分析技术了解应用程序来优化您的容器。丢弃不需要的东西,减少容器的攻击面。
5 优化 Docker 镜像大小常见方法
平时我们构建的 Docker 镜像通常比较大,占用大量的磁盘空间,随着容器的大规模部署,同样也会浪费宝贵的带宽资源。本文将介绍几种常用的方法来优化 Docker 镜像大小,这里我们使用 Docker Hub 官方上的 Redis 镜像进行说明。5.1 手动管理
我们能够直接想到的方法就是直接修改官方的 Redis 镜像 Dockerfile 文件,手动删除容器运行后不需要的组件,然后重新构建一个新镜像。这种方法理论上是可行的,但是容易出错,而且效果也不是特别明显。主要是不能和官方的镜像实时同步。5.2 多阶段构建
Docker 在17.05 版本起提供了多阶段构建的功能来解决这个问题,这种方法是通过丢弃中间层来实现的,并通过中间层来提供有关如何创建最终镜像及其内容信息来完成的,只需要保留容器化应用所需的组件即可。在更上层的实现如下所示:- 以一些镜像作为构建的基础
- 和平常一样运行命令来构造你的应用
- 将所需的制品复制到另外一个单独的镜像
5.3 Distroless
在严重依赖容器化技术,尤其是 Docker 之后,谷歌早就意识到了使用臃肿镜像的弊端。所以他们提供了自己的方法来解决这个问题,即 distroless 镜像。与典型的Linux 基础镜像(绑定了很多软件)不同,在 distroless 上对你的应用进行 docker化,最终的镜像只包含应用及其运行时的依赖项,大多数 Linux 发行版中包含的标准软件,如包管理器,甚至 shell 都被会被排除在外。 同样的,要使用 Google 的 distroless 镜像,需要使用上面我们提到的多阶段构建,如下所示:使用redis:latest为基础镜像,然后保留需要的一些二进制文件(redis-server二进制文件以及所有的相关依赖),然后使用 distroless 镜像作为构建的最终镜像的基础,将opt目录内容复制到该镜像目录中来。 然后我们只需要重新构建镜像即可:
FROM redis:latest AS build
ARG TIME_ZONE
RUN mkdir -p /opt/etc && \
cp -a --parents /lib/x86_64-linux-gnu/libm.so.* /opt && \
cp -a --parents /lib/x86_64-linux-gnu/libdl.so.* /opt && \
cp -a --parents /lib/x86_64-linux-gnu/libpthread.so.* /opt && \
cp -a --parents /lib/x86_64-linux-gnu/libc.so.* /opt && \
cp -a --parents /usr/local/bin/redis-server /opt && \
cp -a --parents /usr/local/bin/redis-sentinel /opt && \
cp /usr/share/zoneinfo/${TIME_ZONE:-UTC} /opt/etc/localtime
FROM gcr.io/distroless/base
COPY --from=build /opt /
VOLUME /data
WORKDIR /data
ENTRYPOINT ["redis-server"]
我们可以看到镜像由以前的 104MB 变成了 28.2MB,大大降低了镜像的大小。 注意:在 Linux 下面我们可以使用 ldd 工具来查找指定的二进制文件所需要的依赖,比如 $ ldd $(which redis-server) 。 使用 distroless 镜像来降低 Docker 镜像的大小是一个非常有效的方法,但是这样做也有一个明显的缺点就是最终的镜像中没有 shell 程序了,使得调试 Docker 容器就非常非常困难,当然这样也降低了应用被攻击的危险,使其更加安全,如果我们将应用部署到 Kubernetes 集群的话,我们可以利用 kubectl-debug 这样的工具来辅助调试应用。
$ docker build -t redis:distroless .
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
redis distroless 7d50bd873bea 15 seconds ago 28.2MB
redis latest 1319b1eaa0b7 3 days ago 104MB