docker优化
构建安全镜像
权限管理
避免以容器以root
身份运行
在Openshift与k8s环境中默认容器需要以非root身份运行,使用root身份运行的情况很少,
所以不要忘记在dockerfile中包含USER
指令,
以将启动容器时默认有效的UID更改为非root用户。
以非root身份运行需要在Dockerfile中做的两个步骤:
- 确保USER指令中指定的用户存在于容器内。
- 在进程将要读取或写入的位置提供适当的文件系统权限。
FROM alpine #创建目录,添加`myuser`用户,目录所有作为`myuser` RUN mkdir /server && adduser -D myuser && chown -R myuser /server USER myuser WORKDIR /server COPY myapp ./ CMD ["./myapp"]
限制可执行文件的写权限
容器中的每个可执行文件都应该由root
用户拥有,即使它由非root
用户执行,
并且不应该是全局可写的。
通过阻止执行用户修改现有的二进制文件或脚本,可以有效降低攻击,保证容器不变性。 不可变容器不会在运行时自动更新其代码,可以防止正在运行的应用程序被意外或恶意修改。
例,使用COPY时:
COPY --chown=myuser:myuser myapp ./ # 应改为 COPY myapp ./
减少攻击面
避免加载不必要的包、第三方应用或暴露端口以减少攻击面。
采用多阶段构建
采用多阶段构建可以降低构建复杂度,同时有效减小镜像尺寸。
在多阶段构建中,我们创建一个中间容器(阶段), 其中包含编译工具及生成最终可执行文件。 然后,我们只将生成的工件复制到最终镜像中, 而无需额外的开发依赖项、临时构建文件等等。
精心设计的多阶段构建仅包含最终映像中所需的最少二进制文件和依赖项, 而不包含构建工具或中间文件。它更为安全,并且还减小了镜像大小。 可以有效减少了攻击面,减少了漏洞。
使用可信赖的镜像
应该选择来自受信任仓库和经过验证的官方镜像。使用自定义镜像时, 我们应该检查镜像源和构建的 Dockerfile。
更进一步,甚至应该以这个Dockerfile来构建自己的基础镜像。 因为无法保证在公共仓库中发布的映像确实是从指定的Dockerfile构建的。
从头开始构建镜像
假如如果你是从centos镜像开始构建,那么可能将会包含几十个或者上百个漏洞。 在生产中通常会从Scratch空镜像或distroless开始。
distroless镜像仅包含应用程序及其运行时依赖项。
它们不包括在标准 Linux 发行版中发布应用如包管理器、shell 或任何其他程序。
Distroless 镜像非常小。最小的 distroless 图像gcr.io/distroless/static
大约为 650 kB。
只有alpine(约2.5 MB)大小的 四分之一 ,不到debian(50 MB)大小的 1.5% 。
FROM golang:1.13-buster as build WORKDIR /go/src/app ADD . /go/src/app RUN go get -d -v ./... RUN go build -o /go/bin/app # 引用Distroless镜像 FROM gcr.io/distroless/base-debian10 COPY --from=build /go/bin/app / CMD ["/app"]
gcr.io/distroless/base-debian10
只包含一组基本的包,
如包括只需要的库,如glibc
、libssl
和openssl
当然对于像 Go 这样不需要libc
的静态编译应用程序我们就可以替换为如下基镜像
FROM gcr.io/distroless/static-debian10
关于distroless基镜像的更多信息可以参考https://github.com/GoogleContainerTools/distroless
及时更新镜像
使用经常更新的基础镜像,在需要时重构你的镜像。随着新的安全漏洞不断被发现, 坚持使用最新的安全补丁是一种通用的安全最佳实践。
版本控制策略:
坚持使用稳定或长期支持版本,这些版本会迅速提供安全修复程序。提前计划。 准备好在基本镜像版本达到生命周期结束或停止接收更新之前删除旧版本并迁移。 定期重建自己的镜像,从基础发行版、Node、Golang、Python 等获取最新的包。
大多数包或依赖项管理器,如npm或go mod,将提供指定版本最新的安全更新。
端口暴露
仅公开应用程序需要的端口,并且避免公开 SSH (22) 等端口。
我们知道 Dockerfile 提供了EXPOSE
命令有暴露端口,
但是该命令仅用于提供信息和用于文档目的。运行容器时,
容器不会自动允许所有EXPOSE
端口的连接
(除非在启动容器时使用docker run --publish-all
)。
启动容器时,通过-P
暴露的端口应与dockerfile中EXPOSE
命令指定的端口一致,
这样更便于维护。
敏感数据管理
凭证和密钥
禁止在 Dockerfile 指令(环境变量、参数或其他任何命令中)中放入凭据和密钥。
在复制文件到镜像时,即使文件在 Dockerfile 的后续指令中被删除, 它仍然可以在之前的层上访问。因为镜像分层原理,你的文件并没有真正被删除, 只是隐藏在最终文件系统中。因此在构建镜像时,我们应该遵循以下做法:
-
如果应用程序支持通过环境变量进行配置,通过
docker run
中的-e
选项配置, 或者使用Docker secrets、Kubernetes secrets提供值作为环境变量。 - 使用配置文件并在docker 中绑定挂载配置文件,或者使用Kubernetes secret 挂载。
继续研究:docker secrets
ADD、COPY
ADD 和 COPY 指令在 Dockerfile 中提供类似的功能。但是COPY 更为明确。
除非我们确实需要 使用ADD 功能,例如从 URL 或从 tar 文件添加文件。 不然最好使用 COPY,COPY 的结果更具可预测性且不易出错。
在某些情况下,最好使用 RUN 指令而不是 ADD 来下载使用curl或wget的包, 解压缩然后删除原始文件,减少层数。
构建上下文与dockerignore
在构建时我们通常使用.
作为上下文
$ docker build -t images:v1 .
使用.
作为上下文时我们需要谨慎些,
因为docker CLI会将上下文中机密或不必要的文件添加到守护进程,甚至到容器中。
例如配置文件、凭据、备份、锁定文件、临时文件、源、子文件夹、点文件等等。 在比如:
COPY . /server
此时会将目录下所有内容都添加到镜像中,包括Dockfile本身。
所以正确做法是创建一个包含需要在容器内复制文件的文件夹, 将其用作构建上下文,并在可能的情况下明确 COPY 指令(避免使用通配符)。例如:
$ docker build -t images:v1 build_files/
为了排除不必要的文件,也可以创建一个.dockerignore
文件,
在其中明确排除的文件和目录。
精简镜像减少体积
减少镜像层数
指令合并
意味着只有RUN
、COPY
、ADD
三个指令会创建层,其他指令会创建一个中间镜像,
并且不会影响镜像大小。这样我们说的指令合并也就是以这三个指令为主。
我们以如下Dockerfile为例
FROM debian:stable WORKDIR /var/www LABEL version=“v1” RUN apt-get update RUN apt-get -y --no-install-recommends install curl RUN apt-get purge -y curl RUN apt-get autoremove -y RUN apt-get clean RUN rm -rf /var/lib/apt/lists/*
构建镜像
$ docker build -t curl:v1 . # 查看构建历史 $ docker history curl:v1 IMAGE CREATED CREATED BY SIZE COMMENT 29b721c09b67 18 seconds ago /bin/sh -c rm -rf /var/lib/apt/lists/* 0B aa28ae151e59 20 seconds ago /bin/sh -c apt-get clean 0B 4f733781f557 22 seconds ago /bin/sh -c apt-get autoremove -y 989kB f66887372121 29 seconds ago /bin/sh -c apt-get purge -y curl 987kB d458ee0de463 34 seconds ago /bin/sh -c apt-get -y --no-install-recommend… 4.46MB 43fdcf68018c 44 seconds ago /bin/sh -c apt-get update 17.6MB 65631e8bb010 53 seconds ago /bin/sh -c #(nop) LABEL version=“v1” 0B 7ef7c53b019c 53 seconds ago /bin/sh -c #(nop) WORKDIR /var/www 0B 8bfa93572e55 13 days ago /bin/sh -c #(nop) CMD ["bash"] 0B <missing> 13 days ago /bin/sh -c #(nop) ADD file:d78d93eff67b18592… 124MB
- 镜像大小
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE curl v1 29b721c09b67 10 minutes ago 148MB }}}
我们将RUN指令通过类shell操作&&合并后
RUN apt-get update && \ apt-get -y --no-install-recommends install curl && \ apt-get purge -y curl && \ apt-get autoremove -y && \ apt-get clean && \ rm -rf /var/lib/apt/lists/*
查看构建历史与镜像大小
$ docker history curl:v2 IMAGE CREATED CREATED BY SIZE COMMENT 928e12c2f57e About a minute ago /bin/sh -c apt-get update && apt-get -y … 989kB 5a32372025fb About a minute ago /bin/sh -c #(nop) LABEL version=“v2” 0B 7ef7c53b019c 30 minutes ago /bin/sh -c #(nop) WORKDIR /var/www 0B 8bfa93572e55 13 days ago /bin/sh -c #(nop) CMD ["bash"] 0B <missing> 13 days ago /bin/sh -c #(nop) ADD file:d78d93eff67b18592… 124MB $ docker images REPOSITORY TAG IMAGE ID CREATED SIZE curl v2 928e12c2f57e 3 minutes ago 125MB
多阶段构建
在Docker17.05 中引入了多阶段构建,通过多阶段构建可以大大降低构建复杂度,同时使缩小镜像尺寸更为简单。我们来看多阶段构建的Dockerfile
#阶段1 FROM golang:1.16 WORKDIR /go/src COPY app.go ./ RUN go build app.go -o myapp #阶段2 FROM scratch WORKDIR /server COPY --from=0 /go/src/myapp ./ CMD ["./myapp"]
构建镜像
$ docker build --no-cache -t server_app:v2 . # 查看构建好的镜像 $ docker images REPOSITORY TAG IMAGE ID CREATED SIZE server_app v2 20225cb1ea6b 12 seconds ago 1.94MB
启用squash特性
通过启用squash特性(实验性功能):
$ docker build --squash -t curl:v3 .
可以构建的镜像压缩为一层。但是为了充分发挥容器镜像层共享的优越设计,这种方法不被推荐。
缩减容量
选择小的基础镜像
每个linux发行版镜像大小相差很多,甚至相同发行版镜像也存在差异。 我们以debian为例,稳定版和瘦身版相差约40MB:
# docker images debian stable-slim 2aa48a485e3a 13 days ago 80.4MB debian stable 8bfa93572e55 13 days ago 124MB
我们将Dockerfile中基础镜像改为瘦身版debian:stable-slim
FROM debian:stable-slim
构建后的镜像尺寸更小
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE curl v4 1aab5c9bf8b3 17 seconds ago 81.4MB
当前映像基于 Debian,并包含许多二进制文件。Docker 容器应该包含一个进程, 并包含运行它所需的最低限度,不需要整个操作系统。
可以使用基于 Alpine 的镜像 替换Debian 基础镜像。
FROM alpine WORKDIR /var/www LABEL version=“v5” RUN echo -e 'https://mirrors.aliyun.com/alpine/v3.6/main/\nhttps://mirrors.aliyun.com/alpine/v3.6/community/' > /etc/apk/repositories && \ apk update && \ apk upgrade && \ apk add --no-cache curl
查看镜像大小
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE curl v5 7f735bb213be 11 seconds ago 10.1MB
此时我们的镜像来到了10MB。使用alpine镜像包管理工具是apk,
一些软件包名可能不一样。最大的区别在于Alpine 采用的链接库是[musl libc](musl libc)
而不是[glibc](https://www.etalabs.net/compare_libcs.html)
系列。
alpine镜像
Alpine一个基于musl libc和busybox、面向安全的轻量级Linux发行版。 它本身的Docker镜像只有4~5M大小。各开发语言和框架都有基于alpine制作的基础镜像, 在开发自己应用的镜像时,选择这些镜像作为基础镜像,可以大大减小镜像的体积。
例如,Java、Python、Node.js语言对应的基础镜像如下:
- Java(Spring Boot): - openjdk:8-jdk-alpine,openjdk:8-jre-alpine等
- Java(Tomcat) - tomcat:8.5-alpine等
- Nodejs - node:9-alpine, node:8-alpine等
- Python - python:3-alpine, python:2-alpine等
如果你的项目涉及到编译,比如python等涉及编译的项目,要注意,Alpine用的是muslc, 因为它原本是用作嵌入式系统的,所以并没有glibc那么完整的C标准库。
另外如果你要在Alpine中跑一些脚本的话, 那你要注意一些shell和在linux(Ubuntu、CentOS、Debian等)下的还是有所区别的, Alpine是基于busybox的,同样也是设计于嵌入式的,所以很多shell命令做了裁剪, 并不具备Ubuntu、CentOS、Debian等系统中那么完整的功能。
scratch镜像
scratch是一个空镜像,只能用于构建其他镜像, 比如你要运行一个包含所有依赖的二进制文件,如Golang程序, 可以直接使用scratch作为基础镜像。
样例:
FROM scratch ARG ARCH ADD bin/pause-${ARCH} /pause ENTRYPOINT ["/pause"]
busybox镜像
如果你希望镜像里可以包含一些常用的Linux工具,busybox镜像是个不错选择, 它集成了一百多个最常用Linux命令和工具的软件工具箱,镜像本身只有1.16M, 非常便于构建小镜像。
distroless镜像
distroless镜像,它仅包含您的应用程序及其运行时依赖项。 它们不包含您希望在标准 Linux 发行版中找到的包管理器、shell或任何其他程序。
由于Distroless是原始操作系统的精简版本,不包含额外的程序。容器里并没有Shell! 如果黑客入侵了我们的应用程序并获取了容器的访问权限,他也无法造成太大的损害。 也就是说,程序越少则尺寸越小也越安全。不过,代价是调试更麻烦。
需要注意的是,我们不应该在生产环境中,将Shell附加到容器中进行调试,而应依靠正确的日志和监控。
样例:
FROM node:8 as build WORKDIR /app COPY package.json index.js ./ RUN npm install FROM gcr.io/distroless/nodejs COPY --from=build /app / EXPOSE 3000 CMD ["index.js"]
Distroless镜像和Alpine镜像
如果是在生产环境中运行,并且注重安全性, Distroless镜像可能会更合适。
Docker镜像中每增加一个二进制程序,就会给整个应用程序带来一定的风险。 在容器中只安装一个二进制程序即可降低整体风险。
举个例子,如果黑客在运行于Distroless的应用中发现了一个漏洞, 他也无法在容器中创建Shell,因为根本就没有。
如果更在意要是大小,则可以换成Alpine基础镜像。
这两个都很小,代价是兼容性。Alpine用了一个稍稍有点不一样的C标准库——muslc
,
时不时会碰到点兼容性的问题。
说明:
原生基础镜像非常适合用于测试和开发。它的尺寸比较大, 不过用起来就像你主机上安装的Ubuntu一样。 并且你能访问该操作系统里有的所有二进制程序。
上下文管理
我们经常会用到的COPY指令
COPY . /server/dir
COPY会把整个 构建上下文复制到镜像中,并生产新的缓存层。
为了不必要的文件如日志、缓存文件、Git 历史记录被加载到构建上下文,
我们最好添加.dockerignore
用于忽略非必须文件。这也是精简镜像关键一步,
同时能更好的保证我们构建的镜像安全性。
及时清理下载
我们有如下Dockerfile
.. WORKDIR /tmp RUN curl -LO https://docker.com/download.zip && \ tar -xf download.zip -C /var/www RUN rm -f download.zip ...
我们虽然使用了rm删除download.zip
包,由于镜像分层的问题,
download.zip
是在新的一层被删除,上一层仍然存在。
我们要在一层中及时清理下载
RUN curl -LO https://docker.com/download.zip && \ tar -xf download.zip -C /var/www && rm -f download.zip
另外在安装软件时应及时使用包管理工具清除你下载的软件依赖及缓存, 比如在我们dockerfile中使用apt包管理工具做清理。