在 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"]
改为 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.
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).
本文主要是 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 需要动态生成时就很适用。使用多阶段构建
在 Dockerfile 中可以写多个 FROM ,一个 FROM 是一个阶段,只有最后的 FROM 会构建镜像,其它阶段都是做准备工作的。主要是用来减小镜像体积的。
如下这个例子,我们可以在
FROM golang:1.16 AS builder
阶段做安装依赖和打包工作,然后在FROM alpine:latest
阶段把 builder 阶段打包好的代码拷贝过来即可。多阶段构建支持的语法如下,详见 Use multi-stage builds | Docker Documentation 。
FROM node:14.16.1-alpine3.13 AS builder
:将该阶段命名为 builderCOPY --from=builder /app ./
:从 builder 阶段拷贝啥啥COPY --from=nginx:latest /etc/nginx/nginx.conf ./
:从一个镜像拷贝啥啥FROM builder AS build1
:FROM 可以使用某个阶段。比如先有阶段 1 ,然后阶段 2 和阶段 3 都基于阶段 1 ,然后阶段 4 拷贝阶段 2 和 3 的内容作为最终镜像不要安装无用依赖 & 清理依赖的缓存
比如 yarn 装包可以用
yarn install --production
来只安装生产用的依赖。然后如果不需要依赖的缓存的话,可以用yarn cache clean
来清理一下。如果是
apk
可以RUN apk add --no-cache xxx
来不生成缓存。如果是
apt-get
可以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
可以保证每条命令都成功才会构建成功。用 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 是啥
就一个十分小的镜像,适合基于它做一个基础镜像。
docker run 设置内存占用上限
如果一台主机上会运行多个容器,为了防止某个容器占用内存过大影响其它容器,可以设置单个容器内存占用上限,比如
-m "300M" --memory-swap "1G"
。docker run 的 --init 参数
有清理僵尸进程等功能,详见 GitHub - krallin/tini: A tiny but valid
init
for containers 。