findxc / blog

88 stars 5 forks source link

Dockerfile 最佳实践 #58

Open findxc opened 3 years ago

findxc commented 3 years ago

本文主要是 Best practices for writing Dockerfiles | Docker Documentation 的总结。

其它参考资料:

容器的创建和删除应尽量简单

比如创建容器时需要的配置参数尽量少。

扩展阅读:The Twelve-Factor App (简体中文)

build context 和 .dockerignore

比如 docker build -t hello:v1 . 这里的 . 就是 build context ,也可以指定其它本地路径或者一个 git 仓库作为 build context 。

因为一般 Dockerfile 会有一句 COPY . . ,为了避免把不需要的文件拷贝到镜像中增加镜像体积,我们一般会定义一个 .dockerignore

Dockerfile 还支持 stdin 格式

构建镜像时, Dockerfile 这个文件可以不存在,而是通过 stdin 的方式传递给 docker build 命令,比如 Dockerfile 需要动态生成时就很适用。

# 没有 build context 时
docker build -t myimage:latest -<<EOF
FROM busybox
RUN echo "hello world"
EOF

# 使用本地 build context 时
docker build -t myimage:latest -f- . <<EOF
FROM busybox
COPY somefile.txt ./
RUN cat /somefile.txt
EOF

# 使用 git 仓库作为 build context 时
# 这种方式需要主机上有安装 git , docker 会先 git clone
docker build -t myimage:latest -f- https://github.com/docker-library/hello-world.git <<EOF
FROM busybox
COPY hello.c ./
EOF

使用多阶段构建

在 Dockerfile 中可以写多个 FROM ,一个 FROM 是一个阶段,只有最后的 FROM 会构建镜像,其它阶段都是做准备工作的。主要是用来减小镜像体积的。

如下这个例子,我们可以在 FROM golang:1.16 AS builder 阶段做安装依赖和打包工作,然后在 FROM alpine:latest 阶段把 builder 阶段打包好的代码拷贝过来即可。

FROM golang:1.16 AS builder
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go ./
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
# 从 builder 阶段拷贝打包好的代码过来
COPY --from=builder /go/src/github.com/alexellis/href-counter/app ./
CMD ["./app"]  

多阶段构建支持的语法如下,详见 Use multi-stage builds | Docker Documentation

不要安装无用依赖 & 清理依赖的缓存

比如 yarn 装包可以用 yarn install --production 来只安装生产用的依赖。然后如果不需要依赖的缓存的话,可以用 yarn cache clean 来清理一下。

如果是 apk 可以 RUN apk add --no-cache xxx 来不生成缓存。

如果是 apt-get 可以 rm -rf /var/lib/apt/lists/* 来删掉缓存。

解耦你的应用

不要把一堆应用塞进一个镜像里,不利于横向扩展和镜像的复用。

用反斜杠 \ 把比较长的命令换行展示,参数较多时按首字母排序

方便阅读和维护。

RUN apt-get update && apt-get install -y \
  bzr \
  cvs \
  git \
  mercurial \
  subversion \
  && rm -rf /var/lib/apt/lists/*

尽量利用 docker 缓存机制

docker 是怎么判断某一层是否能使用缓存的呢?

FROM 中定义的镜像尽量是官方镜像,尽量是 alpine 版本,尽量用详细的 tag

比如相对于 FROM node:alpine 来说, FROM node:14.16.1-alpine3.13 这种版本更明确的会好一些,避免因为版本问题引起的 bug 。

如果 RUN 中有用到 pipe ,最好设一下 set -o pipefail

比如 RUN wget -O - https://some.site | wc -l > /number 这一句,只要 wc -l 命令成功了,就算 wget 失败了,镜像构建也会成功。

改为 RUN set -o pipefail && wget -O - https://some.site | wc -l > /number 可以保证每条命令都成功才会构建成功。

Docker executes these commands using the /bin/sh -c interpreter, which only evaluates the exit code of the last operation in the pipe to determine success.

用 ADD 还是 COPY

COPY 只是单纯地拷贝文件。 ADD 是在 COPY 基础上,还能解压文件(比如 ADD rootfs.tar.xz /)以及获取某个链接的文件(比如 ADD https://example.com/big.tar.xz /usr/src/things/),这样导致 ADD 命令的效果不够显式,COPY 的效果更透明。

如果 COPY 满足要求优先用 COPY 。只在你确实有这种解压等需求的时候使用 ADD 。

尽量使用非 root 用户

docker 默认启动容器时是 root 用户。如果注重安全问题,改为非 root 用户会比较好。比如 node 镜像默认有 node 用户,可以通过 USER node 进行切换。

From scratch 中的 scratch 是啥

就一个十分小的镜像,适合基于它做一个基础镜像。

This image is most useful in the context of building base images (such as debian and busybox) or super minimal images (that contain only a single binary and whatever it requires, such as hello-world).

docker run 设置内存占用上限

如果一台主机上会运行多个容器,为了防止某个容器占用内存过大影响其它容器,可以设置单个容器内存占用上限,比如 -m "300M" --memory-swap "1G"

docker run 的 --init 参数

有清理僵尸进程等功能,详见 GitHub - krallin/tini: A tiny but valid init for containers