Docker镜像优化
Table of Contents
注:本文章基于docker-ce版本:Client 19.03.8,Server 19.03.8
Dockerfile简介⌗
Dockerfile
主要有这几个指令,每个指令都会添加新的层,但是镜像大小不一定增长:
指令 | 用途 | 用法 | 简单示例 |
---|---|---|---|
FROM | 指定基础镜像 | FROM <image> 或 FROM <image>:<tag> |
FROM ubuntu:16.04 |
MAINTAINER | 维护者信息 | MAINTAINER <name> |
MAINTAINER HuangXianDong |
ADD | 复制指定的文件到容器中包括tar,URL等 | ADD <src> <dest> |
ADD conf/jail.local /etc/fail2ban/jail.local |
COPY | 复制host上下文环境的文件或者前一阶段镜像的文件到容器 | COPY <src> <dest> 有两个标志 –from= –chown= |
COPY /usr/local/app /usr/local/app |
USER | 指定用户 | USER <username> |
USER root |
WORKDIR | 指定工作目录 | WORKDIR /path/to/workdir |
WORKDIR /root |
RUN | 终端执行sh或者可执行程序 | RUN <command> 或 RUN [“executable”, “param1”, “param2”] |
RUN apt-get update |
ENV | 指定一个环境变量 | ENV <key> <value> 或ENV <key>=<value> |
ENV TZ=Asia/Shanghai |
ONBUILD | 配置当所创建的镜像作为其它新创建镜像的基础镜像时,所执行的操作指令 | ONBUILD [INSTRUCTION] |
ONBUILD ADD . /app/src |
VOLUME | 创建一个挂载点 | VOLUME [“/data”] |
VOLUME [“/data”] |
EXPOSE | 打算暴露的端口号 | EXPOSE <port> [<port>…] |
EXPOSE 5060/tcp 5060/udp |
ENTRYPOINT | 容器入口点,不可被docker run 覆盖 |
ENTRYPOIN T [“executable”, “param1”, “param2”]或ENTRYPOINT command param1 param2 |
ENTRYPOINT entrypoint.sh |
CMD | 指定启动容器时执行的命令,可以被docker run 覆盖 |
CMD [“executable”,”param1″,”param2″] 或CMD command param1 param2 或CMD [“param1″,”param2”] 提供给 ENTRYPOINT 的默认参数 |
CMD python /app/app.py |
常用镜像缩小方法⌗
在哪层引入,就在哪层清理⌗
众所周知,Dockerfile
每个指令都会叠加新的一层文件系统,由于docker
镜像这种叠加的文件系统,也就是说在构建镜像时,前面一层的文件对于本层来说是只读的,从而不能够在本层文件系统去清理上一层引入的文件,虽然可以删除掉对应的文件或文件夹,但是镜像体积并没有减少:
WORKDIR /usr/src
RUN git clone http://xxx.git \
&& cd ./xxx \
&& ./bootstrap.sh \
&& ./configure --enable-core-pgsql-support \
&& make \
&& make install #这一层引入文件
RUN rm -rf /usr/src/* #在这一层清理,达不到体积减小的目的,反而增加层数
应该这样:
WORKDIR /usr/src
RUN git clone http://xxx.git \
&& cd ./xxx \
&& ./bootstrap.sh \
&& ./configure --enable-core-pgsql-support \
&& make \
&& make install \
&& rm -rf /usr/src/* # 在哪层引入,就在哪层清理
此外,如果在ADD
,COPY
指令中添加了多余的文件,此后在其他层删除掉改文件或文件夹虽然文件删除了,但是镜像大小是不变或增加的:
FROM busybox
ADD . /root # busytest0 157MB
RUN rm -rf /root # busytest1 157MB
另外,在使用包管理工具的时候经常要安装各种工具,安装时应该将所有要安装的工具写在一起并且加以清理,甚至可以卸载掉:
RUN apt-get update && apt-get install -y autoconf automake bison \
build-essential fail2ban gawk git-core groff groff-base \
&& apt-get autoclean \
&& rm -rf /var/lib/apt/lists/*
合并RUN中的命令⌗
多个RUN
指令会增加很多层镜像,应该尽量把能合在一起写的shell
写在同一个RUN
指令里:
RUN apt-get update \
&& ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone \
&& apt-get install tzdata \
&& apt-get clean \
&& apt-get autoclean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
而不是:
RUN apt-get update
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime
RUN echo $TZ > /etc/timezone
RUN apt-get install tzdata
RUN apt-get clean
RUN apt-get autoclean
分阶段构建⌗
有些应用,如go
构建的可执行文件,可以不需要依赖构建他的系统而运行的程序,就适合进行分步构建,也就是一个阶段编译,一个阶段用于执行程序。分步构建很大程度上能减少镜像的体积。
FROM golang
COPY hello.go .
RUN go build hello.go
FROM scratch
COPY --from=0 /go/hello .
CMD ["./hello"]
分阶段构建有时候对一些C
程序和大型的Go
程序来说,凡是有依赖动态运行库的程序进行分阶段构建就很头疼,因为复制过来的可执行文件可能需要某个动态库的支持。
与之相对的解决办法是:静态编译或者复制动态库。
静态编译是一个不错的方法,但是如果某种原因不适合静态编译,比如分阶段编译等等,这就需要拷贝库文件了。
就像某些程序,编译出来的可执行文件只有十几kb
,其他的都是以动态库的形势存在,那么怎么寻找应用对应的依赖库呢?
使用ldd
命令,该命令可以查询对应的库文件:
# ldd /usr/local/freeswitch/bin/freeswitch
linux-vdso.so.1 => (0x00007ffd0bb61000)
libfreeswitch.so.1 => /usr/local/freeswitch/lib/libfreeswitch.so.1 (0x00007f0fa2a1d000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f0fa27f3000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0fa2429000)
libuuid.so.1 => /lib/x86_64-linux-gnu/libuuid.so.1 (0x00007f0fa2223000)
...
将这些对应的库文件复制到镜像之后需要执行ldconfig
命令,任何改动库文件的操作都应该执行该命令。
复制动态库文件可能会导致后续维护变得困难,并且可能存在未知的隐患。
制作基础镜像包⌗
构建镜像时有些主要是下载依赖包导致的时间变长,可以考虑专门做一个环境编译的基础镜像,后续编译就可以基于该镜像编译,大大减少构建时间。
一些容易增大镜像体积的操作⌗
慎用chown
命令:chown
命令在有些文件系统在实现多层联合文件系统的原理不同,可能会导致无端增加文件夹的体积。
原因是在docker
实现的某些文件系统如overlayfs2
,chown
只更改 metadata
数据, 而 metadata
是作用在 inode
节点上的, 一个硬链接文件的属性被修改, 同步的, 所有指向这个 inode
节点的文件的属性都会变化,而overlayfs2
每层都是独立的,即使文件属性的变化,也会导致整个文件被拷贝, 所以在 overlayfs2
下, 会产生多余的空间浪费。
常用的工具⌗
docker history命令⌗
docker
自带了简单的镜像分析命令,直接执行docker history <IMAGE ID>
或者 docker history REPOSITORY:TAG
就行:
[root@RqInternTest3 freeswitch-container]# docker history dda43e8b9e21
IMAGE CREATED CREATED BY SIZE COMMENT
dda43e8b9e21 9 hours ago /bin/sh -c #(nop) CMD ["/usr/local/freeswit… 0B
76f11898568d 9 hours ago /bin/sh -c #(nop) EXPOSE 64535-65535/udp 0B
124e52a5fdb8 9 hours ago /bin/sh -c #(nop) EXPOSE 8021/tcp 0B
d05fa7854fa3 9 hours ago /bin/sh -c #(nop) EXPOSE 5066/tcp 7443/tcp 0B
fae9a2737d1b 9 hours ago /bin/sh -c #(nop) EXPOSE 5060/tcp 5060/udp … 0B
d817769a9e83 9 hours ago /bin/sh -c ldconfig 81.5kB
e4347f4a3f7f 9 hours ago /bin/sh -c #(nop) ADD file:305fb1821e6c9f4f7… 19.5kB
7fa9aac435b0 9 hours ago /bin/sh -c #(nop) ADD file:ef5aa87255557ffc9… 340B
2872fc32f715 9 hours ago /bin/sh -c #(nop) ADD file:aa24cd419ee5ea6ca… 249B
681ea1a83b9b 9 hours ago /bin/sh -c chmod +x /etc/init.d/freeswitch … 7.28kB
b3af584e7595 9 hours ago /bin/sh -c #(nop) ADD file:e450d50f8d60a1861… 5.76kB
8c2e2a5f2de4 9 hours ago /bin/sh -c #(nop) COPY dir:8ecfbdecc9f674459… 417MB
9a2938106462 9 hours ago /bin/sh -c #(nop) COPY dir:1ddadc455338c149f… 112MB
d03606f65563 9 hours ago /bin/sh -c #(nop) COPY dir:cd305b7f15031b530… 20.4MB
3e21911eea87 9 hours ago /bin/sh -c #(nop) COPY --chown=freeswitch:da… 410MB
6ce315566979 9 hours ago /bin/sh -c useradd -r -g daemon freeswitch 329kB
b7c170867677 9 hours ago /bin/sh -c apt-get update && ln -snf /us… 2.76MB
9a800e72f239 10 hours ago /bin/sh -c #(nop) ENV TZ=Asia/Shanghai 0B
77be327e4b63 4 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
<missing> 4 weeks ago /bin/sh -c mkdir -p /run/systemd && echo 'do… 7B
<missing> 4 weeks ago /bin/sh -c set -xe && echo '#!/bin/sh' > /… 745B
<missing> 4 weeks ago /bin/sh -c rm -rf /var/lib/apt/lists/* 0B
<missing> 4 weeks ago /bin/sh -c #(nop) ADD file:1f70668251e2e58ce… 124MB
dive分析镜像工具⌗
可以看出每一层所添加的大小,并针对大的层进行优化。
分析镜像还有一个很有用的工具dive
:
下载安装dive
:
# wget https://github.com/wagoodman/dive/releases/download/v0.9.2/dive_0.9.2_linux_amd64.tar.gz
# tar -zvxf dive_0.9.2_linux_amd64.tar.gz
# cp dive /usr/bin
使用:dive <IMAGE ID>
或者 dive REPOSITORY:TAG
┃ ● Layers ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ Current Layer Contents ├──────────────────────────────────
Cmp Size Command Permission UID:GID Size Filetree
5.2 MB FROM d970df9dd8bab4b drwxr-xr-x 0:0 1.0 MB ├── bin
-rwxr-xr-x 0:0 1.0 MB │ ├── [
│ Layer Details ├─────────────────────────────────────────── -rwxr-xr-x 0:0 0 B │ ├── [[ → bin/[
-rwxr-xr-x 0:0 0 B │ ├── acpid → bin/[
Tags: (unavailable) -rwxr-xr-x 0:0 0 B │ ├── add-shell → bin/[
Id: d970df9dd8bab4b7a604d73763aee9e732d107cf3b02000ca193 -rwxr-xr-x 0:0 0 B │ ├── addgroup → bin/[
712fe48a460f -rwxr-xr-x 0:0 0 B │ ├── adduser → bin/[
Digest: sha256:8a9aa5c55905476e9412fcf0e18942123b239333aafc4 -rwxr-xr-x 0:0 0 B │ ├── adjtimex → bin/[
d7b3656faf67b980b41 -rwxr-xr-x 0:0 0 B │ ├── ar → bin/[
Command: -rwxr-xr-x 0:0 0 B │ ├── arch → bin/[
#(nop) ADD file:d5fc1686f667af49c2cb01970701b62eeb9c818d2d4e -rwxr-xr-x 0:0 0 B │ ├── arp → bin/[
8f967993dab1de737f28 in / -rwxr-xr-x 0:0 0 B │ ├── arping → bin/[
-rwxr-xr-x 0:0 0 B │ ├── ash → bin/[
│ Image Details ├─────────────────────────────────────────── -rwxr-xr-x 0:0 0 B │ ├── awk → bin/[
-rwxr-xr-x 0:0 0 B │ ├── base64 → bin/[
-rwxr-xr-x 0:0 0 B │ ├── basename → bin/[
Total Image size: 5.2 MB -rwxr-xr-x 0:0 0 B │ ├── bc → bin/[
Potential wasted space: 0 B -rwxr-xr-x 0:0 0 B │ ├── beep → bin/[
Image efficiency score: 100 % -rwxr-xr-x 0:0 0 B │ ├── blkdiscard → bin/
-rwxr-xr-x 0:0 0 B │ ├── blkid → bin/[
Count Total Space Path -rwxr-xr-x 0:0 0 B │ ├── blockdev → bin/[
-rwxr-xr-x 0:0 0 B │ ├── bootchartd → bin/
-rwxr-xr-x 0:0 0 B │ ├── brctl → bin/[
-rwxr-xr-x 0:0 0 B │ ├── bunzip2 → bin/[
-rwxr-xr-x 0:0 0 B │ ├── busybox → bin/[
-rwxr-xr-x 0:0 0 B │ ├── bzcat → bin/[
-rwxr-xr-x 0:0 0 B │ ├── bzip2 → bin/[
-rwxr-xr-x 0:0 0 B │ ├── cal → bin/[
可以非常清晰的看到每一层添加的镜像大小以及文件的变化,通过右边的Content
可以观察到文件夹的变化。
各种基础镜像的构建⌗
各类基础镜像分析⌗
常用的基础镜像CentOS,Debian,Fedora,Ubuntu,Alpine
镜像:
类别 | 版本 | 镜像大小(MB) | 描述 | 包管理 |
---|---|---|---|---|
CentOS | latest | 69.84 | 适用于正式的生产环境镜像制作,但是构建出来的镜像大小会很大,可以来考虑分步构建 | yum,rpm |
- | centos8 | 69.84 | - | - |
- | centos7 | 72.15 | - | - |
- | centos6 | 66.83 | - | - |
Debian | buster-slim | 26.46 | 当前稳定版本的体积更小版本(slim) | apt-get, dpkg |
- | stretch | 43.69 | 旧的稳定版 | - |
- | jessie | 52.08 | 测试版本 | - |
- | stable | 48.78 | 稳定版本 | - |
- | sid | 50.54 | 不稳定版本 | - |
- | wheezy | 38.66 | Debian 7,被淘汰的稳定版 | - |
Fedora | 33 | 67.21 | 稳定版本 | dpkg/apt,pacman |
- | rawhide | 67.21 | 永远不会被冻结, 也永远不会被正式发布. 对稳定性没有任何保证. 主要用于最新代码的初步测试 | - |
Ubuntu | latest | 25.9 | 适用于正式的生产环境镜像制作,但是构建出来的镜像大小会很大,可以来考虑分步构建 | apt-get |
- | 18.04 | 25.9 | - | - |
- | 16.04 | 42.13 | - | - |
Alpine | latest | 2.68 | 面向安全的、轻量级的Linux系统,基于musl libc和busybox,使用glibc库的大型c语言可能会遇到很多坑 | apk, lbu |
busybox | latest | 1 | 包含了基本的linux命令 | apk |
- | glibc | 5 | GNU C library版本 | - |
scratch | latest | 0 | 空白镜像,sh工具也没有 | 无 |
也可以使用编程语言对应的基础镜像,适合分步构建完成之后在其上运行或者在对应的基础镜像中编译。
各个基础镜像换源⌗
CentOS yum源
mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup
wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-6.repo
Ubuntu apt源 清华源:
sed -i 's http://.*.ubuntu.com http://mirrors.tuna.tsinghua.edu.cn g' /etc/apt/sources.list
阿里云源:
sed -i 's http://.*.ubuntu.com http://mirrors.aliyun.com g' /etc/apt/sources.list
Debian apt源
sed -i 's http://.*.debian.org http://mirrors.aliyun.com g' /etc/apt/sources.list
Alphine apk源
阿里源:
sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
科大源:
sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
各个基础镜像设置时区方法⌗
Ubuntu
FROM ubuntu:16.04
ENV TZ=Asia/Shanghai
RUN apt-get update \
&& ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone \
&& apt-get install tzdata \
&& apt-get clean \
&& apt-get autoclean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
CentOS
ENV TimeZone=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TimeZone /etc/localtime && echo $TimeZone > /etc/timezone
Debian
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
Alphine
RUN apk add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \
&& apk del tzdata
把日志文件输出到标准输出流⌗
有时候我们写的程序不是以标准输出到控制台的方式来打印日志,而是存在某个日志文件,这就造成容器跑起来之后docker logs
命令查看不到日志,这是因为docker
容器只接管标准输出流和标准错误流,因此还需要重定向到stdout
和stderr
:
RUN ln -sf /dev/stdout /var/log/nginx/access.log \
&& ln -sf /dev/stderr /var/log/nginx/error.log