Dockerfile 的一些实践经验
去年一个人把公司内部分老项目和新项目添加了 Docker 支持,重新设计了 CD/CI 流程;同时由于基本是单机服务,编排容器用的 Docker Compose,因此在这个过程中,也积累了一些经验。
一、减少镜像大小
1.1 Alpine
使用 Alpine 或者是带有 alpine 版本的镜像作为基础镜像
下图是这个博客分别用 node:14.13.0 和 node:14.13.0-alpine3.12 构建后的镜像大小对比

具体关于 Alpine 的介绍可以看看这个链接
1.2 多阶段构建
在构建一些运行文件和源代码分离的项目时(Vue.js、React.js、Go 等),可以考虑使用多阶段构建让最后生成的镜像只含有运行文件
1.2.1 Vue.js
FROM node:13.14.0-alpine3.11 as base
# 构建镜像
FROM base as builder
WORKDIR /app
COPY ./package.json ./package.json
RUN npm install -g cross-env
RUN npm install && \
npm install -g cnpm --registry=https://registry.npm.taobao.org && \
cnpm install node-sass sass-loader
COPY . .
RUN npm run build:h5
# 运行镜像
FROM caddy:2.1.1-alpine as runner
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
apk add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo "Asia/Shanghai" > /etc/timezone && \
apk del tzdata
COPY ./Caddyfile /etc/caddy/Caddyfile
COPY --from=builder /app/dist/build/h5 /app/dist
COPY --from=builder /app/public/pc.html /app/dist/public/pc.html
CMD caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
1.3 如果希望所有容器都通过端口访问,可以考虑使用 Caddy
我个人比较偏向于所有的容器之间都通过端口访问,那么就会遇到一些服务本身没有映射功能端口的情——例如 PHP——因此,可以考虑使用 Caddy在容器内部代理端口
Caddyfile:
:8989 {
root * /app/public
file_server
php_fastcgi 127.0.0.1:9000
}
Dockerfile:
COPY ./Caddyfile /etc/caddy/Caddyfile
CMD caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
二、减少 Layer 层的数量
减少层数有利于提高 Push 和 Pull 的速度,提高部署效率;在 Dockerfile中,每一行的命令都会生成一个新层,因此可以通过命令合并的方式降低层数,比如使用 && \ 分隔运行多个命令
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
apk add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo "Asia/Shanghai" > /etc/timezone && \
apk del tzdata
三、提高构建速度
3.1 容易产生变动的命令放在最后执行
虽然 docker build 的过程中可以使用缓存提高速度,但是如果某一行 Dockerfile内容发生了变动,那么它以及之后所有的命令都需要重新执行(不包括多阶段构建中的其他阶段)。
3.2 多阶段构建
多阶段构建不仅能够帮助降低镜像大小,也可以在一定程度上提高构建的速度,比如说在 go build 的时候,运行镜像的基础环境已经完成了;
同时,每个部分之间的缓存判断是独立的,因此就算修改了构建部分的 Dockerfile,运行部分的构建依然可以使用缓存。
3.2.1 Go
# syntax = docker/dockerfile:experimental
FROM golang:1.14.2-alpine3.11 as base
FROM base as module
ENV GOPROXY https://mirrors.aliyun.com/goproxy/
ENV GO111MODULE on
WORKDIR /app
ADD go.mod .
ADD go.sum .
RUN --mount=type=cache,target=/go/pkg/mod,id=go,sharing=locked \
sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
apk update && \
apk add git && \
go mod download -x
FROM module as builder
WORKDIR /app
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod,id=go,sharing=locked \
go build
FROM alpine:3.12.0 as runner
WORKDIR /app
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
apk update && \
apk add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo "Asia/Shanghai" > /etc/timezone && \
apk del tzdata
COPY --from=builder /app/go /app/go
CMD ["./go"]
# syntax = docker/dockerfile:experimental
3.3 使用 BuildKit
BuildKit 中能够使用 cache 解决程序依赖包如果发生变动就需要全部重新下载的问题,具体请参考这份文档
四、成功率和稳定性
基础镜像尽量不使用类似 latest这种非固定镜像的 Tag,尽量使用精确的版本号比如说:node:14.13.0-alpine3.12
五、时区问题
由于大部分镜像的时区都不是东八区,因此在构建成镜像时时间判断、Log 时间标记可能都会出现问题,因此时区也是需要考虑的问题
通过 Dockerfile 解决(Alpine):
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
apk add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo "Asia/Shanghai" > /etc/timezone && \
apk del tzdata
运行时解决(通过同步服务器的宿主机时间):
# docker run
-v /etc/localtime:/etc/localtime
# docker-compose
volumes:
- /etc/localtime:/etc/localtime:ro
