一、删除缓存
一般的包管理器,比如 apt
, pip
等,下载包的时候,都会下载缓存,下次安装同一个包的时候不必从网络上下载,直接使用缓存即可。
#正例
RUN dnf install -y --setopt=tsflags=nodocs \
httpd vim && \
systemctl enable httpd && \
dnf clean all
#反例
FROM fedora
RUN dnf install -y mariadb
RUN dnf install -y wordpress
RUN dnf clean all
Dockerfile 里面的每一个 RUN 都会创建一层新的 layer,如上所说,这样其实是创建了 3 层 layer,前 2 层带来了缓存,第三层删除了缓存。如同 git 一样,你在一个新的 commit 里面删除了之前的文件,其实文件还是在 git 历史中的,最终的 docker image 其实没有减少。
但是 Docker 有了一个新的功能,docker build --squash。squash 功能会在 Docker 完成构建之后,将所有的 layers 压缩成一个 layer,也就是说,最终构建出来的 Docker image 只有一层。所以,如上在多个 RUN 中写 clean 命令,其实也可以。我不太喜欢这种方式,因为前文提到的,多个 image 共享 base image 以及加速 pull 的 feature 其实就用不到了。
一些常见的包管理器删除缓存的方法:
yum | yum clean all |
---|---|
dnf | dnf clean all |
rvm | rvm cleanup all |
gem | gem cleanup |
cpan | rm -rf ~/.cpan/{build,sources}/* |
pip | rm -rf ~/.cache/pip/* |
apt-get | apt-get clean |
另外,上面这个命令其实还有一个缺点。因为我们在同一个 RUN
中写多行,不容易看出这个 dnf
到底安装了什么。而且,第一行和最后一行不一样,如果修改,diff 看到的会是两行内容,很不友好,容易出错。
可以写成这种形式,比较清晰
RUN true \
&& dnf install -y --setopt=tsflags=nodocs \
httpd vim \
&& systemctl enable httpd \
&& dnf clean all \
&& true
二、改动不频繁的内容往前放
对于一个 Docker image 有 ABCD 四层,B 修改了,那么 BCD 会改变。
根据这个原理,我们在构建的时候可以将系统依赖往前写,因为像 apt
, dnf
这些安装的东西,是很少修改的。然后写应用的库依赖,比如 pip install
,最后 copy 应用。
比如下面这个 Dockerfile,就会在每次代码改变的时候都重新 Build 大部分 layers,即使只改了一个网页的标题。
FROM python:3.7-buster
# copy source
RUN mkdir -p /opt/app
COPY myapp /opt/app/myapp/
WORKDIR /opt/app
# install dependencies nginx
RUN apt-get update && apt-get install nginx
RUN pip install -r requirements.txt
RUN chown -R www-data:www-data /opt/app
# start server
EXPOSE 8020
STOPSIGNAL SIGTERM
CMD ["/opt/app/start-server.sh"]
可以改成,先安装 Nginx,再单独 copy requirements.txt
,然后安装 pip
依赖,最后 copy 应用代码。
FROM python:3.7-buster
# install dependencies nginx
RUN apt-get update && apt-get install nginx
COPY myapp/requirements.txt /opt/app/myapp/requirements.txt
RUN pip install -r requirements.txt
# copy source
RUN mkdir -p /opt/app
COPY myapp /opt/app/myapp/
WORKDIR /opt/app
RUN chown -R www-data:www-data /opt/app
# start server
EXPOSE 8020
STOPSIGNAL SIGTERM
CMD ["/opt/app/start-server.sh"]
三、构建和运行Image分离
我们在编译应用的时候需要很多构建工具,比如 gcc, golang 等。但是在运行的时候不需要。在构建完成之后,去删除那些构建工具是很麻烦的。
我们可以这样:使用一个 Docker 作为 builder,安装所有的构建依赖,进行构建,构建完成后,重新选择一个 Base image,然后将构建的产物复制到新的 base image,这样,最终的 image 只含有运行需要的东西。
FROM golang as build
ENV CGO_ENABLED 0
RUN go install github.com/ericchiang/pup@latest
FROM alpine:3.15.4 as run
COPY --from=build /go/bin/pup /usr/local/bin/pup
四、构建产物
dive 是一个 TUI,命令行的交互式 App,它可以让你看到 docker 每一层里面都有什么。
GitHub - wagoodman/dive: A tool for exploring each layer in a docker image
dive ubuntu:latest
命令可以看到 ubuntu image 里面都有什么文件。内容会显示为两侧,左边显示每一层的信息,右边显示当前层(会包含之前的所有层)的文件内容,本层新添加的文件会用黄色来显示。通过 tab
键可以切换左右的操作。
---------------------------------------------------
国内软件源
使用默认的软件源安装构建时所需的依赖,对于绝大多数基础镜像来说,可以通过修改软件源的方式更换为国内的软件源镜像站。目前国内稳定可靠的镜像站主要有,华为云、阿里云、腾讯云、163 等。
对于 alpine 基础镜像修改软件源
echo "http://mirrors.huaweicloud.com/alpine/latest-stable/main/" > /etc/apk/repositories ;\
echo "http://mirrors.huaweicloud.com/alpine/latest-stable/community/" >> /etc/apk/repositories ;\
apk update ;
debian 基础镜像修改默认软件源
sed -i 's/deb.debian.org/mirrors.huaweicloud.com/g' /etc/apt/sources.list ;\
sed -i 's|security.debian.org/debian-security|mirrors.huaweicloud.com/debian-security|g' /etc/apt/sources.list ;\
apt update ;\
Ubuntu 基础镜像修改默认软件源
sed -i 's/archive.ubuntu.com/mirrors.huaweicloud.com/g' /etc/apt/sources.list
apt update ;\
建议这些命令就放在 RUN 指令的第一条,update 以下软件源,之后再 install 相应的依赖。
时区设置:
由于绝大多数基础镜像都是默认采用 UTC 的时区,与北京时间相差 8 个小时,这将会导致容器内的时间与北京时间不一致,因而会对一些应用造成一些影响,还会影响容器内日志和监控的数据。
因此对于东八区的用户,最好在构建镜像的时候设定一下容器内的时区,以免以后因为时区遇到一些 bug😂。
可以通过环境变量设置容器内的时区。在启动的时候可以通过设置环境变量 -e TZ=Asia/Shanghai 来设定容器内的时区
alpine
但对于 alpine 基础镜像无法通过 TZ 环境变量的方式设定时区,需要安装 tzdata 来配置时区。
docker run --rm -it -e TZ=Asia/Shanghai alpine date
Thu Jan 2 03:37:44 UTC 2022
对于 alpine 基础镜像,可以在 RUN 指令后面追加上以下命令
apk add --no-cache tzdata ;\
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime ;\
echo "Asia/Shanghai" > /etc/timezone ;\
apk del tzdata ;\
通过 tzdate 设定时区
docker build -t alpine:tz2 .
docker run --rm -it alpine:tz2 date
Thu Jan 2 11:12:23 CST 2022
debian
通过启动时设定环境变量指定时区
docker run --rm -it -e TZ=Asia/Shanghai debian date
Thu Jan 2 11:38:56 CST 2022
也可以再构建镜像的时候复制时区文件设定容器内时区
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime ;\
echo "Asia/shanghai" > /etc/timezone ;\
ubuntu
通过启动时设定环境变量指定时区,失败 😂,只能通过时区文件来设定时区了。
docker run --rm -it -e TZ=Asia/Shanghai debian date
Thu Jan 2 11:38:56 CST 2022
root@ubuntu:~/docke/alpine# ^debian^ubuntu
docker run --rm -it -e TZ=Asia/Shanghai ubuntu date
Thu Jan 2 03:44:13 Asia 2022
在这里有个命令执行的小技巧,通过脱字符 ^ 来替换上一条命令中的 debian 为 ubuntu 然后执行相同的命令
通过时区文件来设定时区
apt update ;\
apt install tzdata -y ;\
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime ;\
echo "Asia/shanghai" > /etc/timezone ;\
尽量使用 URL 添加源码
如果不采用分阶段构建,对于一些需要在容器内进行编译的项目,最好通过 git 或者 wegt 的方式将源码打入到镜像内,而非采用 ADD 或者 COPY ,因为源码编译完成之后,源码就不需要可以删掉了,而通过 ADD 或者 COPY 添加进去的源码已经用在下一层镜像中了,是删不掉滴啦。
也就是说 git & wget source
然后 build
,最后 rm -rf source/
这三部放在一条 RUN 指令中,这样就能避免源码添加到镜像中而增大镜像体积啦
下面以 FastDFS 的 Dockerfile 为例
项目官方的 Dockerfile
# centos 7
FROM centos:7
# 添加配置文件
# add profiles
ADD conf/client.conf /etc/fdfs/
ADD conf/http.conf /etc/fdfs/
ADD conf/mime.types /etc/fdfs/
ADD conf/storage.conf /etc/fdfs/
ADD conf/tracker.conf /etc/fdfs/
ADD fastdfs.sh /home
ADD conf/nginx.conf /etc/fdfs/
ADD conf/mod_fastdfs.conf /etc/fdfs
# 添加源文件
# add source code
ADD source/libfastcommon.tar.gz /usr/local/src/
ADD source/fastdfs.tar.gz /usr/local/src/
ADD source/fastdfs-nginx-module.tar.gz /usr/local/src/
ADD source/nginx-1.15.4.tar.gz /usr/local/src/
# Run
RUN yum install git gcc gcc-c++ make automake autoconf libtool pcre pcre-devel zlib zlib-devel openssl-devel wget vim -y \
&& mkdir /home/dfs \
&& cd /usr/local/src/ \
&& cd libfastcommon/ \
&& ./make.sh && ./make.sh install \
&& cd ../ \
&& cd fastdfs/ \
&& ./make.sh && ./make.sh install \
&& cd ../ \
&& cd nginx-1.15.4/ \
&& ./configure --add-module=/usr/local/src/fastdfs-nginx-module/src/ \
&& make && make install \
&& chmod +x /home/fastdfs.sh
# export config
VOLUME /etc/fdfs
EXPOSE 22122 23000 8888 80
ENTRYPOINT ["/home/fastdfs.sh"]
优化后的:
FROM alpine:3.10
RUN set -x \
&& echo "http://mirrors.huaweicloud.com/alpine/latest-stable/main/" > /etc/apk/repositories \
&& echo "http://mirrors.huaweicloud.com/alpine/latest-stable/community/" >> /etc/apk/repositories \
&& apk update \
&& apk add --no-cache --virtual .build-deps gcc libc-dev make perl-dev openssl-dev pcre-dev zlib-dev git \
&& mkdir -p /usr/local/src \
&& cd /usr/local/src \
&& git clone https://github.com/happyfish100/libfastcommon.git --depth 1 \
&& git clone https://github.com/happyfish100/fastdfs.git --depth 1 \
&& git clone https://github.com/happyfish100/fastdfs-nginx-module.git --depth 1 \
&& wget http://nginx.org/download/nginx-1.15.4.tar.gz \
&& tar -xf nginx-1.15.4.tar.gz \
&& cd /usr/local/src/libfastcommon \
&& ./make.sh \
&& ./make.sh install \
&& cd /usr/local/src/fastdfs/ \
&& ./make.sh \
&& ./make.sh install \
&& cd /usr/local/src/nginx-1.15.4/ \
&& ./configure --add-module=/usr/local/src/fastdfs-nginx-module/src/ \
&& make && make install \
&& apk del .build-deps \
&& apk add --no-cache pcre-dev bash \
&& mkdir -p /home/dfs \
&& mv /usr/local/src/fastdfs/docker/dockerfile_network/fastdfs.sh /home \
&& mv /usr/local/src/fastdfs/docker/dockerfile_network/conf/* /etc/fdfs \
&& chmod +x /home/fastdfs.sh \
&& rm -rf /usr/local/src*
VOLUME /home/dfs
EXPOSE 22122 23000 8888 8080
CMD ["/home/fastdfs.sh"]
使用虚拟编译环境
对于只在编译过程中使用到的依赖,我们可以将这些依赖安装在虚拟环境中,编译完成之后可以一并删除这些依赖,比如 alpine 中可以使用
apk add --no-cache --virtual .build-deps
,后面加上需要安装的相关依赖。
apk add --no-cache --virtual .build-deps gcc libc-dev make perl-dev openssl-dev pcre-dev zlib-dev git
构建完成之后可以使用 apk del .build-deps 命令,一并将这些编译依赖全部删除
需要注意的是,.build-deps 后面接的是编译时以来的软件包,并不是所有的编译依赖都可以删除,不要把运行时的依赖包接在后面,最好单独 add 一下。
最小化层数
docker 在 1.10 以后,只有 RUN、COPY 和 ADD
指令会创建层,其他指令会创建临时的中间镜像,但是不会直接增加构建的镜像大小了。
前文提到了建议使用 git 或者 wget 的方式来将文件打入到镜像当中,但如果我们必须要使用 COPY 或者 ADD 指令呢?
FastDFS 为例
# centos 7
FROM centos:7
# 添加配置文件
# add profiles
ADD conf/client.conf /etc/fdfs/
ADD conf/http.conf /etc/fdfs/
ADD conf/mime.types /etc/fdfs/
ADD conf/storage.conf /etc/fdfs/
ADD conf/tracker.conf /etc/fdfs/
ADD fastdfs.sh /home
ADD conf/nginx.conf /etc/fdfs/
ADD conf/mod_fastdfs.conf /etc/fdfs
# 添加源文件
# add source code
ADD source/libfastcommon.tar.gz /usr/local/src/
ADD source/fastdfs.tar.gz /usr/local/src/
ADD source/fastdfs-nginx-module.tar.gz /usr/local/src/
ADD source/nginx-1.15.4.tar.gz /usr/local/src/
多个文件需要添加到容器中不同的路径,每个文件使用一条 ADD 指令的话就会增加一层镜像,这样戏曲就多了 12 层镜像 。
其实大可不必,我们可以将这些文件全部打包为一个文件为 src.tar.gz 然后通过 ADD 的方式把文件添加到当中去,然后在 RUN 指令后使用 mv 命令把文件移动到指定的位置。这样仅仅一条 ADD 和 RUN 指令取代掉了 12 个 ADD 指令
FROM alpine:3.10
COPY src.tar.gz /usr/local/src.tar.gz
RUN set -xe \
&& apk add --no-cache --virtual .build-deps gcc libc-dev make perl-dev openssl-dev pcre-dev zlib-dev tzdata \
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& tar -xvf /usr/local/src.tar.gz -C /usr/local \
&& mv /usr/local/src/conf/fastdfs.sh /home/fastdfs/ \
&& mv /usr/local/src/conf/* /etc/fdfs \
&& chmod +x /home/fastdfs/fastdfs.sh \
&& rm -rf /usr/local/src/* /var/cache/apk/* /tmp/* /var/tmp/* $HOME/.cache
VOLUME /var/fdfs
其他最小化层数无非就是把构建项目的整个步骤弄成一条 RUN 指令,不过多条命令合并可以使用 && 或者 ; 这两者都可以,不过据我在 docker hub 上的所见所闻,使用 ; 的居多,尤其是官方的 Dockerfile。
使用 Docker Squash 减小镜像大小
Docker 在构建镜像时创建了很多层。压缩有助于在逻辑层中组织镜像。我们可以控制镜像的结构,而不是让镜像具有多个不必要的层。
您可以使用以下命令安装 docker-squash。
pip install docker-squash
您可以运行以下命令来减小镜像的大小。docker-squash image:old -t image:new
-----------------------------------docker build --squash -t jdk:test . # 需要开启实验特性
apt 安装中使用 --no-install-recommends 标志
当我们运行 apt install 命令来安装某些包时,它会安装一些不需要的推荐包。使用 --no-install-recommends 标志可以显着减小镜像大小。
FROM ubuntu:latest RUN apt update -y && \ apt install unzip -y --no-install-recommends && \ apt install curl --no-install-recommends -y && \ apt install python3 -y --no-install-recommends
在 apt install 命令后添加 rm -rf /var/lib/apt/lists/*
FROM ubuntu:latest RUN apt update -y && \ apt install unzip -y --no-install-recommends && \ apt install curl --no-install-recommends -y && \ apt install python3 -y --no-install-recommends && \ rm -rf /var/lib/apt/lists/*
在 apt install 之后添加这个命令来减少 docker 镜像的大小
使用 .dockerignore 文件
如果您不想将某些文件复制到 docker 镜像,那么使用 .dockerignore 文件可以为您节省一些空间。
在构建上下文中有一些隐藏的文件/文件夹,您可以使用 ADD 或 COPY 命令(如 .git 等)将其传输到镜像。包含一个 .dockerignore 文件以减小 docker 镜像大小是一个很好的做法。
.dockerignore 文件示例。
ignorethisfile.txt logs/ ignorethisfolder/ .git .cache *.md
在 RUN 之后放置 COPY
在某些情况下,您对代码进行了细微的更改,并且需要反复从 dockerfile 构建镜像。
在这种情况下,将 COPY 命令放在 RUN 命令之后将有助于减小镜像大小,因为在这种情况下 docker 将能够更好地使用缓存功能。它将为安装了依赖项的镜像创建缓存,每次更改代码时,docker 都会使用该缓存并创建镜像。它还将减少 docker 构建时间。
# Dockerfile FROM ubuntu:latest RUN apt update -y && \ apt install unzip -y --no-install-recommends && \ apt install curl --no-install-recommends -y && \ apt install python3 -y --no-install-recommends && \ rm -rf /var/lib/apt/lists/* COPY file /home/ubuntu
#Dockerfile-2 FROM ubuntu:latest COPY file /home/ubuntu RUN apt update -y && \ apt install unzip -y --no-install-recommends && \ apt install curl --no-install-recommends -y && \ apt install python3 -y --no-install-recommends && \ rm -rf /var/lib/apt/lists/*
在上述情况下,dockerfile-1 将能够比 dockerfile-2 表现得更好
安装后删除软件包
如果您需要在 docker 镜像中安装一些包,并且您是从外部下载它们,那么最好在安装后删除这些包。
例如,如果您希望从 zip 文件安装 AWS CLI V2,那么在成功安装后请记住也删除该 zip 文件。
FROM ubuntu:latest
RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && \
unzip awscliv2.zip && \
sudo ./aws/install && \
rm awscliv2.zip
使用 Docker 镜像缩容工具
有几个dockerfile 优化工具可以帮助你减少 docker 镜像的大小。下面列出了其中一些。
Dive:Dive 是一个开源工具,用于探索 Docker 镜像及其层内容,然后发现缩小 Docker/OCI 镜像大小的方法。
https://github.com/wagoodman/divefromlatest.io:此工具将检查您的 Dockerfile 并检查可以执行的更多步骤以减小镜像大小。
https://www.fromlatest.io/Docker Slim:它让你的容器更好、更小、更安全。您可以使用dockerslim 来最小化容器镜像。
https://github.com/slimtoolkit/slim