Dockerfile 的一些实践经验

date
Mar 23, 2021
slug
practice-experience-for-dockerfile
status
Published
summary
去年一个人把公司内部分老项目和新项目添加了 Docker 支持,重新设计了 CD/CI 流程;同时由于基本是单机服务,编排容器用的 Docker Compose,因此在这个过程中,也积累了一些经验。
tags
Docker
type
Post
去年一个人把公司内部分老项目和新项目添加了 Docker 支持,重新设计了 CD/CI 流程;同时由于基本是单机服务,编排容器用的 Docker Compose,因此在这个过程中,也积累了一些经验。

一、减少镜像大小

1.1 Alpine

使用 Alpine 或者是带有 alpine 版本的镜像作为基础镜像
下图是这个博客分别用 node:14.13.0 node:14.13.0-alpine3.12 构建后的镜像大小对比
notion image
具体关于 Alpine 的介绍可以看看这个链接

1.2 多阶段构建

在构建一些运行文件和源代码分离的项目时(Vue.jsReact.jsGo 等),可以考虑使用多阶段构建让最后生成的镜像只含有运行文件

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