Docker容器的优化思路

image.png

镜像构建的过程,视具体业务场景的不同而不同。在很多情况下,我们需要先以满足业务目标为准,而不是镜像的构建层数。如果需要减少镜像层数,一定要选择合适的基础镜像或者创建符合需要的基础镜像。

image.png

1. 选择基础镜像

选择合适产品的基础镜像,这点相对来说非常重要。选择一个合适的基础镜像,需要能够满足运行应用所需要的最小的镜像。理论上是能用小的就不要用大的,能用轻量级的就不要用重量级的,能用性能好的就不要用性能差的,能用稳定版就不要用开发版。

比如我们构建 Java 语言的程序,最好的方式就是使用官方提供的 openjdk:8 作为基础镜像,而非使用比较大的 ubuntu:18.04 作为基础镜像。另外,还有一个需要我们注意的地方就是,尽可能使用官方的特定版本的镜像,而不要使用 latest 这个频繁变动的 tag 作为基础镜像。

FROM ubuntu:18.04
FROM openjdk:latest
FROM openjdk:8

image.png

image.png

image.png

2. 优化指令顺序,缩短构建时间

构建 Docker 镜像的时候,会缓存 Dockerfile 中尚未更改的所有步骤。所以,如果新构建时更改任何指令,将后的指令步骤将会重新来不再使用缓存。举例来说,就是指令 3 发生了变更,其后的 4-n 就会重跑并重新生成缓存。

因此,编写 Dockerfile 的时候,就需要将最不可能产生更改的指令放在前面。比如,可以把 WORKDIR/ENV 等命令放在前面,COPY/ADD 等命令放在后面。这样,在构建过程中较多的使用了缓存,就可以节省很多时间了。

FROM ubuntu:18.04
WORKDIR /opt/app
ARG env=env
ENV ENV=${env}
COPY . /opt/app/
RUN apt update -y htop

image.png

同时,在使用 COPY/ADD 等命令的时候越具体越好,最好只复制所需的内容。

FROM ubuntu:18.04
WORKDIR /opt/app
COPY target/docker_patch.py /opt/app/
RUN apt update -y htop

image.png

3. 合并构建指令,缩短构建时间

我们都知道,在编写 Dockerfile 的时候,每一个指令都会创建一层并构成新的镜像。当运行多个指令时,会产生非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。因此,在这种情况下,我们需要将同类型的指令合并然后再一起运行。合并指令时,一定要注意格式(比如换行、缩进、注释等)会让维护、排障更为容易。

RUN apt update -y && \
    DEBIAN_FRONTEND=noninteractive apt install --no-install-recommends -y \
        python3.6-dev \
        python3-distutils \
        nginx \
        vim.tiny \
        supervisor \
        ca-certificates \
        tzdata \
        locales \
        build-essential \
        fontconfig

image.png

4. 清理中间结果,减少镜像大小

这点很易于理解,通常来讲,体积更小,部署更快!因此在构建过程中,我们需要清理那些最终不需要的代码或文件。比如说,临时文件、源代码、缓存等等。

rm -rf /var/lib/apt/lists/* /tmp/*
rm -rf /etc/nginx/sites-enabled/default

image.png

5. 减少冗余文件,减少镜像大小

使用 .dockerignore 文件用于忽略那些镜像构建时非必须的文件,这些文件可以是开发文档、日志、其他无用的文件。注意再添加文件或者文件夹的时候,最好的方式是先全部排除,之后再添加。

*
.*
!config/config-docker.yml
!config/logging.conf
!docker/nginx
!docker/supervisor
......
  1. 使用 runtime 镜像,提高可维护性
    当我们维护的项目越来越大的时候,使用单独的 Dockerfile 文件来维护就变得越来越笨重,且构建时间也越来越长。为了便捷且高效的管理,就需要我们构建基本的 runtime 镜像。在 runtime 镜像中,主要包含项目所依赖的基础环境包,多为很少改动的内容。而我们的项目就不用从基础镜像开始,而是从 runtime 镜像开始,大大的加速的构件速度。

7. 使用多段构建,提高可维护性

Docker v17.05 开始支持多阶段构建。多段构建的目的就是,使用多阶段构建来删除构建依赖项。容器多阶段构建可由多个 FROM 语句构成,每个 FROM 语句开始一个新的阶段。它们可以用 AS 关键字进行别名命名。我们用它来别名作为我们的第一阶段构建器,以便稍后引用,它将在一致的环境中包含所有构建依赖项。

第二阶段是我们的最后阶段,将产生最终镜像包。它将包括运行时的严格必要条件,在本例中是基于 Alpine 的最小 JRE(Java运行时)。中间构建器阶段将被缓存但不会出现在最终映像中。要将构建的内容添加到最终镜像的话,请使用 COPY --from=STAGE_NAME 参数进行制定。

image.png

# 使用as来为某一阶段命名
FROM golang:1.9-alpine as builder

# 当我们只想构建builder阶段的镜像时,增加--target=builder参数即可
$ docker build --target builder -t username/imagename:tag .

# 我们也可以复制任意镜像中的文件
$ COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf

8. 构建多种系统架构

使用 buildx 构建多种系统架构支持的 Docker 镜像

我们知道使用镜像创建一个容器,该镜像必须与 Docker 宿主机系统架构一致,例如 Linux x86_64 架构的系统中只能使用 Linux x86_64 的镜像创建容器。否则,我们尝试在其他平台系统上面,则根本获取不到对应镜像内容。

为不同系统架构打包不同的镜像,则会导致十分繁琐而且很难维护,所以后来官方引入了 manifest 和 buildx(在Docker19.03+版本引入) 两个子命令来处理上述问题。但是,BuildKit 是下一代的镜像构建组件。

# 可以看到现在都处于试验阶段
$ docker buildx build xxx
docker manifest is only supported on a Docker cli with experimental cli features enabled

# 命令属于实验特性必须设置环境变量
$ export DOCKER_CLI_EXPERIMENTAL=enabled

由于 Docker 默认的 builder 实例不支持同时指定多个 --platform,我们必须首先创建一个新的 builder 实例。

$ docker buildx create --name mybuilder
$ docker buildx use mybuilder

构建镜像之前,我们首先需要创建所需的 Dockerfile 文件。使用 buildx 命令构建镜像,注意将 myusername 替换为自己的 Docker Hub 用户名,而 --push 参数表示将构建好的镜像推送到 Docker 仓库。

# 构建多平台镜像
$ docker buildx build --platform \
    linux/arm,linux/arm64,linux/amd64 \
    -t <myusername>/hello . --push

# 查看镜像信息
$ docker buildx imagetools inspect <myusername>/hello

在不同架构运行该镜像,可以得到该架构的信息。

# arm
$ docker run -it --rm myusername/hello
Linux buildkitsandbox 4.9.125-linuxkit #1 SMP Fri Sep 7 08:20:28 UTC 2019 armv7l Linux

# arm64
$ docker run -it --rm myusername/hello
Linux buildkitsandbox 4.9.125-linuxkit #1 SMP Fri Sep 7 08:20:28 UTC 2019 aarch64 Linux

# amd64
$ docker run -it --rm myusername/hello
Linux buildkitsandbox 4.9.125-linuxkit #1 SMP Fri Sep 7 08:20:28 UTC 2019 x86_64 Linux

9. 使用 Buildx 优化构建

使用 BuildKit 提供的 Dockerfile 新指令来更快的构建 Docker 镜像。

官网地址:https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/experimental.md

RUN --mount=type=cache
RUN --mount=type=bind
RUN --mount=type=tmpfs
RUN --mount=type=secret
RUN --mount=type=ssh

目前,几乎所有的程序都会使用依赖管理工具,例如 Node.js 中的 npm 等等,当我们构建一个镜像时,往往会重复的从互联网中获取依赖包,难以缓存,大大降低了镜像的构建效率。

例如一个前端工程需要用到 npm 工具,使用多阶段构建,构建的镜像中只包含了目标文件夹 dist,但仍然存在一些问题,当 package.json 文件变动时,RUN npm i && rm -rf ~/.npm 这一层会重新执行,变更多次后,生成了大量的中间层镜像。

FROM node:alpine as builder

WORKDIR /app

COPY package.json /app/

RUN npm i --registry=https://registry.npm.taobao.org \
        && rm -rf ~/.npm

COPY src /app/src

RUN npm run build

FROM nginx:alpine

COPY --from=builder /app/dist /app/dist

为解决这个问题,进一步的我们可以设想一个类似 数据卷 的功能,在镜像构建时把 node_modules 文件夹挂载上去,在构建完成后,这个 node_modules 文件夹会自动卸载,实际的镜像中并不包含 node_modules 这个文件夹,这样我们就省去了每次获取依赖的时间,大大增加了镜像构建效率,同时也避免了生成了大量的中间层镜像。BuildKit 提供了 RUN --mount=type=cache 指令,可以实现上边的设想。

  • 第一个 RUN 指令执行后,id 为 my_app_npm_module 的缓存文件夹挂载到了 /app/node_modules 文件夹中。多次执行也不会产生多个中间层镜像。
  • 第二个 RUN 指令执行时需要用到 node_modules 文件夹,node_modules 已经挂载,命令也可以正确执行。
  • 第三个 RUN 指令将上一阶段产生的文件复制到指定位置,from 指明缓存的来源,这里 builder 表示缓存来源于构建的第一阶段,source 指明缓存来源的文件夹。

image.png

FROM node:alpine as builder

WORKDIR /app

COPY package.json /app/

RUN --mount=type=cache,target=/app/node_modules,id=my_app_npm_module,sharing=locked \
    --mount=type=cache,target=/root/.npm,id=npm_cache \
        npm i --registry=https://registry.npm.taobao.org

COPY src /app/src

RUN --mount=type=cache,target=/app/node_modules,id=my_app_npm_module,sharing=locked \
# --mount=type=cache,target=/app/dist,id=my_app_dist,sharing=locked \
        npm run build

FROM nginx:alpine

# COPY --from=builder /app/dist /app/dist

# 为了更直观的说明 from 和 source 指令,这里使用 RUN 指令
RUN --mount=type=cache,target=/tmp/dist,from=builder,source=/app/dist \
    # --mount=type=cache,target/tmp/dist,from=my_app_dist,sharing=locked \
    mkdir -p /app/dist && cp -r /tmp/dist/* /app/dist

用 Trivy 扫描新操作系统的漏洞

安装

apt-get install
yum install
brew install

扫描

扫描本地镜像:

当镜像位于本地,大小90MB左右时候的扫描:

trivy http://registry.cn-hangzhou.aliyuncs.com/choerodon-tools/javabase:0.5.0

时间:第一次扫描会DownLoad DB,大概花十分钟以内(14M,看网速),国外的主机10s以内,第二次扫描十秒钟以内完成。

trivy 172.21.0.8/devops/gin-demo:v1
2021-09-01T16:22:20.999+0800	INFO	Number of language-specific files: 1
2021-09-01T16:22:20.999+0800	INFO	Detecting gobinary vulnerabilities...

gin_demo (gobinary)
===================
Total: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 1, CRITICAL: 0)

+---------------------+------------------+----------+------------------------------------+------------------------------------+---------------------------------------+
|       LIBRARY       | VULNERABILITY ID | SEVERITY |         INSTALLED VERSION          |           FIXED VERSION            |                 TITLE                 |
+---------------------+------------------+----------+------------------------------------+------------------------------------+---------------------------------------+
| golang.org/x/crypto | CVE-2020-29652   | HIGH     | v0.0.0-20200622213623-75b288015ac9 | v0.0.0-20201216223049-8b5274cf687f | golang: crypto/ssh: crafted           |
|                     |                  |          |                                    |                                    | authentication request can            |
|                     |                  |          |                                    |                                    | lead to nil pointer dereference       |
|                     |                  |          |                                    |                                    | -->avd.aquasec.com/nvd/cve-2020-29652 |
+---------------------+------------------+----------+------------------------------------+------------------------------------+---------------------------------------+

扫描镜像文件

$ docker save ruby:2.3.0-alpine3.9 -o ruby-2.3.0.tar
$ trivy --input ruby-2.3.0.tar

根据严重程度进行过滤

$ trivy --severity HIGH,CRITICAL ruby:2.3.0

忽略未修复问题

$ trivy --ignore-unfixed ruby:2.3.0

忽略特定问题

使用 .trivyignore:

$ cat .trivyignore
# Accept the risk
CVE-2018-14618

# No impact in our settings
CVE-2019-1543

$ trivy python:3.4-alpine3.9

使用 JSON 输出结果

$ trivy -f json dustise/translat-chatbot:20190428-5

定义返回值

$ trivy --exit-code 0 --severity MEDIUM,HIGH ruby:2.3.0
$ trivy --exit-code 1 --severity CRITICAL ruby:2.3.0

优化 Dockerfile,缩减镜像尺寸

下面列出今天 Docker Hub 上的 Top 10 镜像的尺寸( Latest ):

镜像名称尺寸(MB)
busybox1
ubuntu188
swarm17
nginx134
registry423
redis151
mysql360
mongo317
node643
debian125

使用较小的基础镜像(例如 Alphine Linux, BusyBox 等)有很多好处。

所以我假设你已经试用了其中的一个为基础镜像。接下来的事情就取决于操作者控制镜像尺寸的能力了。特别的,我们会验证一些缩减镜像大小的手段的效果,例如把多个 RUN 指令中的命令集中到一行,一些关于 apt-get 的技巧(例如移除 apt-get 缓存以及 --no-install-recommends)

在同一行中进行清理工作

Docker 镜像的基础是一种层次化文件系统。每一层都只是包含同下面一层不同的部分。在最上方只能看到一个统一视图,而无法获知他的构建历史。Dockerfile 的每一行都会在顶部建立一个新的层。

例如我们以这样一个 Dockerfile 片段开始:

ADD https://storage.googleapis.com/golang/go1.5.3.src.tar.gz /tmp

# do some things with that file

RUN rm /tmp/go1.5.3.src.tar.gz

你可以能会觉得删除 .tar.gz 文件是个负责任的好办法。但是包含这一文件的层还遗留在镜像之中。rm 操作只是对最终镜像隐藏了这一文件,使用 docker pull 获取这一镜像时还是会下载这些内容的。

更好的办法就是把这些操作集中到一行之中,这样就不会提交多个层了。例如,对上面的指令进行简单的改写:

RUN curl -o \
	/tmp/go.1.5.3.src.tar.gz \
	https://storage.googleapis.com/golang/go1.5.3.src.tar.gz && \
	<do some things with the file> && \
	rm /tmp/go1.5.3.src.tar.gz

这样看起来有点丑,不过能够优化镜像尺寸。如果你讨厌这种写法,可以建立一个脚本,然后用 ADD、RUN 来在 Dokerfile 中运行。

用正确的方式移除 apt/yum 缓存

多数 Dockerfile 作者都知道应该 apt-get remove 所有不必要的包。一个例子就是,用 curl/wget 来进行下载安装其他软件。可以事后使用 apt-get remove curl,但是同上面的例子一样,curl 所在的层已经被封装到镜像之中,因此也应该把删除(包括相关的自动安装的依赖包)工作放到同一行中。

一个实际的例子。

这是一个简化版本的用于运行 Python 服务的 Dokerfile,我们将对他进行优化。

FROM ubuntu:14.04
RUN apt-get update
RUN apt-get install -y curl python-pip

RUN pip install requests

ADD ./my_service.py /my_service.py
ENTRYPOINT ["python", "/my_service.py"]

myservice.py 是一个 Python 脚本:

#!/usr/bin/python
print 'Hello, world!'

接下来 build 并检查尺寸:

$ sudo docker build -t size .
$ sudo docker images
REPOSITORY      TAG           IMAGE ID            CREATED           VIRTUAL SIZE
size            latest        da8a9be731ac        4 seconds ago     360.5 MB
ubuntu          14.04         6cc0fc2a5ee3        2 weeks ago       187.9 MB

188 MB 的基础镜像已经很吓人了,不过更值得注意的是,只是一个 hello-world 的 Python 脚本为什么会导致镜像尺寸倍增。这 360.5 MB 中到底有什么?其中包含了最上一层(本例中是 da8a9be731ac),以及用于建立顶层的所有的层的内容。

添加一个清理层

我们也许应该做一些清理工作,我们试试这样的 Dockerfile :

FROM ubuntu:14.04
RUN apt-get update
RUN apt-get install -y curl python-pip

RUN pip install requests

## Clean up
RUN apt-get remove -y python-pip curl
RUN rm -rf /var/lib/apt/lists/*

ADD ./my_service.py /my_service.py
ENTRYPOINT ["python", "/my_service.py"]

Build,然后再看尺寸:

$ sudo docker build -t size .
$ sudo docker images
REPOSITORY      TAG           IMAGE ID            CREATED           VIRTUAL SIZE
size            latest        c6dacdd00660        2 seconds ago     361.3 MB
ubuntu          14.04         6cc0fc2a5ee3        2 weeks ago       187.9 MB

事与愿违,镜像更大了。

在同一层进行清理

接下来试试把 APT 操作进行合并:

FROM ubuntu:14.04
RUN apt-get update && \
    apt-get install -y curl python-pip && \
    pip install requests && \
    apt-get remove -y python-pip curl && \
    rm -rf /var/lib/apt/lists/*

ADD ./my_service.py /my_service.py
ENTRYPOINT ["python", "/my_service.py"]

Build 然后查看镜像:

$ sudo docker build -t size .
$ sudo docker images
REPOSITORY      TAG           IMAGE ID            CREATED           VIRTUAL SIZE
size            latest        e531f8674f33        9 seconds ago     338 MB
ubuntu          14.04         6cc0fc2a5ee3        2 weeks ago       187.9 MB

果然小了一点点 —— 只是一点点。

进一步优化 APT

apt-get install 过程中加入了很多 推荐 内容。推荐包只是简单的加入了一些可有可无的依赖。有些用户会因为特殊的应用方式,或者环境要求才需要这些东西,换句话说,这些推荐内容并非必要。

在 Ubuntu 14.04 中运行 PIP,很容易就可以得出结论:删除推荐包并不会有什么副作用。这一点在产品发布之前是完全可以确认的。可以在 Docker Hub 上看看 redis,mysql,mongo,postgres,elasticsearch 等的镜像,使用这一技巧来缩减镜像尺寸。

实际操作一下带有 --no-install-recommends 选项的 APT-GET:

FROM ubuntu:14.04
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl python-pip && \
    pip install requests && \
    apt-get remove -y python-pip curl && \
    rm -rf /var/lib/apt/lists/*

ADD ./my_service.py /my_service.py
ENTRYPOINT ["python", "/my_service.py"]

Build,再次检查尺寸:

REPOSITORY      TAG           IMAGE ID            CREATED           VIRTUAL SIZE
size            latest        fddc30aee4dc        6 seconds ago     229.2 MB
ubuntu          14.04         6cc0fc2a5ee3        2 weeks ago       187.9 MB

这次我们把镜像缩小了 120 MB。

Dockerfile 的语法非常简单,也同样有优化的需求,因此在组织中应用 Docker 时,需要为 Dockerfile 建立健全相应的策略来优化这一过程。

多级构建,减少容器镜像大小

修改基础镜像

尽量使用 alpine 等小镜像作为基础镜像

多级构建

NPM 实例

FROM node:12-alpine AS build
WORKDIR /app
COPY package.json ./
RUN yarn install
COPY . /app
RUN yarn build

FROM node:12-alpine
WORKDIR /app
RUN npm install -g webserver.local
COPY --from=build /app/build ./build
EXPOSE 3000
CMD webserver.local -d ./build

或者使用 nginx

FROM node:12-alpine AS build
WORKDIR /app
COPY package.json ./
RUN yarn install
COPY . /app
RUN yarn build

FROM nginx:stable-alpine
COPY --from=build /app/build /usr/share/nginx/html
EXPOSE 80
CMD \["nginx", "-g", "daemon off;"\]

Docker 常用命令

docker top

这个命令是用来查看一个容器里面的进程信息的,比如你想查看一个 nginx 容器里面有几个 nginx 进程的时候,就可以这么做:

docker top 83b0e7a18861171819ebe3970739ae38c0978f92be942003053f6e36d9189539
UID                 PID                 PPID                C                   STIME               TTY                 TIME                CMD
root                11314               11295               0                   Sep11               ?                   00:00:00            nginx: master process nginx -g daemon off;
101                 11351               11314               0                   Sep11               ?                   00:00:01            nginx: worker process
101                 11352               11314               0                   Sep11               ?                   00:00:00            nginx: worker process

保存以及导入容器镜像

docker load && docker save

docker save 可以把一个镜像保存到 tar 文件中,你可以这么做:

docker save registry:2.7.1 > registry-2.7.1.tar

同时 docker load 可以把镜像从 tar 文件导入到 docker 中

docker load < registry-2.7.1.tar

这个命令可以帮助你在命令行中方便的搜索 DockerHub 中的镜像,比如:

docker search nginx
NAME                              DESCRIPTION                                     STARS     OFFICIAL   AUTOMATED
nginx                             Official build of Nginx.                        15492     [OK]       
jwilder/nginx-proxy               Automated Nginx reverse proxy for docker con…   2066                 [OK]
richarvey/nginx-php-fpm           Container running Nginx + PHP-FPM capable of…   818                  [OK]
.......

docker update

当你 docker run 了之后却发现里面有一些参数并不是你想要的状态比如你设置的 nginx 容器 cpu 或者内存太小,这个时候你就可以使用 docker update 去修改这些参数。

docker update nginx --cpus 2

docker history

当你修改了一个镜像,但是忘记了每一层的修改命令,或者你想查看一个镜像是怎么构建的时候就可以使用这个命令,比如:

docker history nginx:latest 
IMAGE          CREATED       CREATED BY                                      SIZE      COMMENT
08b152afcfae   8 weeks ago   /bin/sh -c #(nop)  CMD ["nginx" "-g" "daemon…   0B        
<missing>      8 weeks ago   /bin/sh -c #(nop)  STOPSIGNAL SIGQUIT           0B        
<missing>      8 weeks ago   /bin/sh -c #(nop)  EXPOSE 80                    0B        
<missing>      8 weeks ago   /bin/sh -c #(nop)  ENTRYPOINT ["/docker-entr…   0B        
<missing>      8 weeks ago   /bin/sh -c #(nop) COPY file:09a214a3e07c919a…   4.61kB    
<missing>      8 weeks ago   /bin/sh -c #(nop) COPY file:0fd5fca330dcd6a7…   1.04kB    
<missing>      8 weeks ago   /bin/sh -c #(nop) COPY file:0b866ff3fc1ef5b0…   1.96kB    
<missing>      8 weeks ago   /bin/sh -c #(nop) COPY file:65504f71f5855ca0…   1.2kB     
<missing>      8 weeks ago   /bin/sh -c set -x     && addgroup --system -…   63.9MB    
<missing>      8 weeks ago   /bin/sh -c #(nop)  ENV PKG_RELEASE=1~buster     0B        
<missing>      8 weeks ago   /bin/sh -c #(nop)  ENV NJS_VERSION=0.6.1        0B        
<missing>      8 weeks ago   /bin/sh -c #(nop)  ENV NGINX_VERSION=1.21.1     0B        
<missing>      8 weeks ago   /bin/sh -c #(nop)  LABEL maintainer=NGINX Do…   0B        
<missing>      8 weeks ago   /bin/sh -c #(nop)  CMD ["bash"]                 0B        
<missing>      8 weeks ago   /bin/sh -c #(nop) ADD file:45f5dfa135c848a34…   69.3MB

docker wait

这个命令可以查看容器的退出状态

docker pause && docker unpause

当你运行了一个容器但是想要暂停它运行的时候,你就可以使用这个命令。

docker diff

当你运行了一个容器,但是你不知道容器里修改了哪一些文件的时候可以使用这个命令,比如:

docker diff 3b0e7a1886
C /etc
C /etc/nginx
A /etc/nginx/cert
C /etc/nginx/conf.d
C /etc/nginx/conf.d/default.conf
C /run
A /run/nginx.pid
A /run/secrets
A /run/secrets/kubernetes.io
A /run/secrets/kubernetes.io/serviceaccount
C /var
C /var/cache
C /var/cache/nginx
A /var/cache/nginx/fastcgi_temp
A /var/cache/nginx/proxy_temp
A /var/cache/nginx/proxy_temp/4
A /var/cache/nginx/proxy_temp/4/06
A /var/cache/nginx/proxy_temp/4/10
A /var/cache/nginx/proxy_temp/4/11
A /var/cache/nginx/proxy_temp/4/15
A /var/cache/nginx/proxy_temp/4/16
A /var/cache/nginx/proxy_temp/4/17
A /var/cache/nginx/proxy_temp/4/00
A /var/cache/nginx/proxy_temp/4/05
A /var/cache/nginx/proxy_temp/6
A /var/cache/nginx/proxy_temp/6/01
A /var/cache/nginx/proxy_temp/6/02
A /var/cache/nginx/proxy_temp/6/06
A /var/cache/nginx/proxy_temp/6/11
A /var/cache/nginx/proxy_temp/6/15
A /var/cache/nginx/proxy_temp/6/16
A /var/cache/nginx/proxy_temp/6/17
A /var/cache/nginx/proxy_temp/6/00
A /var/cache/nginx/proxy_temp/7
A /var/cache/nginx/proxy_temp/7/00
A /var/cache/nginx/proxy_temp/7/05
A /var/cache/nginx/proxy_temp/7/11
A /var/cache/nginx/proxy_temp/7/15
A /var/cache/nginx/proxy_temp/7/04
A /var/cache/nginx/proxy_temp/7/07
A /var/cache/nginx/proxy_temp/7/10
A /var/cache/nginx/proxy_temp/7/13
A /var/cache/nginx/proxy_temp/7/17
A /var/cache/nginx/proxy_temp/7/18
A /var/cache/nginx/proxy_temp/0
A /var/cache/nginx/proxy_temp/0/06
A /var/cache/nginx/proxy_temp/0/08
A /var/cache/nginx/proxy_temp/0/15
A /var/cache/nginx/proxy_temp/0/16
A /var/cache/nginx/proxy_temp/0/17
A /var/cache/nginx/proxy_temp/0/18
A /var/cache/nginx/proxy_temp/0/01
A /var/cache/nginx/proxy_temp/1
A /var/cache/nginx/proxy_temp/1/09
A /var/cache/nginx/proxy_temp/1/02
A /var/cache/nginx/proxy_temp/1/03
A /var/cache/nginx/proxy_temp/1/05
A /var/cache/nginx/proxy_temp/1/07
A /var/cache/nginx/proxy_temp/1/11
A /var/cache/nginx/proxy_temp/1/01
A /var/cache/nginx/proxy_temp/1/04
A /var/cache/nginx/proxy_temp/1/08
A /var/cache/nginx/proxy_temp/1/10
A /var/cache/nginx/proxy_temp/1/17
A /var/cache/nginx/proxy_temp/1/18
A /var/cache/nginx/proxy_temp/1/00
A /var/cache/nginx/proxy_temp/1/16
A /var/cache/nginx/proxy_temp/3
A /var/cache/nginx/proxy_temp/3/02
A /var/cache/nginx/proxy_temp/3/07
A /var/cache/nginx/proxy_temp/3/08
A /var/cache/nginx/proxy_temp/3/16
A /var/cache/nginx/proxy_temp/3/17
A /var/cache/nginx/proxy_temp/3/18
A /var/cache/nginx/proxy_temp/3/00
A /var/cache/nginx/proxy_temp/3/03
A /var/cache/nginx/proxy_temp/3/04
A /var/cache/nginx/proxy_temp/3/09
A /var/cache/nginx/proxy_temp/3/10
A /var/cache/nginx/proxy_temp/3/11
A /var/cache/nginx/proxy_temp/3/13
A /var/cache/nginx/proxy_temp/9
A /var/cache/nginx/proxy_temp/9/02
A /var/cache/nginx/proxy_temp/9/04
A /var/cache/nginx/proxy_temp/9/06
A /var/cache/nginx/proxy_temp/9/15
A /var/cache/nginx/proxy_temp/9/00
A /var/cache/nginx/proxy_temp/9/01
A /var/cache/nginx/proxy_temp/9/07
A /var/cache/nginx/proxy_temp/9/11
A /var/cache/nginx/proxy_temp/9/17
A /var/cache/nginx/proxy_temp/2
A /var/cache/nginx/proxy_temp/2/00
A /var/cache/nginx/proxy_temp/2/06
A /var/cache/nginx/proxy_temp/2/11
A /var/cache/nginx/proxy_temp/2/16
A /var/cache/nginx/proxy_temp/2/17
A /var/cache/nginx/proxy_temp/2/18
A /var/cache/nginx/proxy_temp/5
A /var/cache/nginx/proxy_temp/5/01
A /var/cache/nginx/proxy_temp/5/04
A /var/cache/nginx/proxy_temp/5/10
A /var/cache/nginx/proxy_temp/5/15
A /var/cache/nginx/proxy_temp/5/03
A /var/cache/nginx/proxy_temp/5/11
A /var/cache/nginx/proxy_temp/5/16
A /var/cache/nginx/proxy_temp/5/17
A /var/cache/nginx/proxy_temp/5/18
A /var/cache/nginx/proxy_temp/8
A /var/cache/nginx/proxy_temp/8/17
A /var/cache/nginx/proxy_temp/8/00
A /var/cache/nginx/proxy_temp/8/07
A /var/cache/nginx/proxy_temp/8/11
A /var/cache/nginx/proxy_temp/8/12
A /var/cache/nginx/proxy_temp/8/15
A /var/cache/nginx/scgi_temp
A /var/cache/nginx/uwsgi_temp
A /var/cache/nginx/client_temp

docker stats

这个是 docker 内置的监控命令,当你想要查看当前主机下所有容器占用内存和 cpu 的情况的时候就可以使用这个命令。

docker inspect

平时可能因为测试或者一些规范的操作方式导致启动一个容器,忘记了这个容器的启动命令是什么了,又需要找回来在别的机器上创建的时候,就很麻烦,可能很多人会想到通过 docker inspect 分析输出的 json 文件中的volume、ports、Env等

docker inspect ...

编写生产环境中最优的 Dockerfile

Dockerfile 编写规则

制定内部 Dockerfile 编写规则,使得 Dockerfile 在内部同事间能够快速了解镜像的完整构建流程,还可以减少镜像的体积,加快镜像构建的速度和分发速度。

(1)单一作用

由于容器的本质是进程,一个容器代表一个进程,因此不同功能的应用应该尽量拆分为不同的容器,每个容器只负责单一业务进程。

(2)提供注释信息

Dockerfile 也是一种代码,我们应该保持良好的代码编写习惯,晦涩难懂的代码尽量添加注释,让协作者可以一目了然地知道每一行代码的作用,并且方便扩展和使用。

(3)保持容器最小化

应该避免安装无用的软件包,比如在一个 nginx 镜像中,我并不需要安装 vim 、gcc 等开发编译工具。这样不仅可以加快容器构建速度,而且可以避免镜像体积过大。

(4)合理选择基础镜像

容器的核心是应用,因此只要基础镜像能够满足应用的运行环境即可。例如一个Java类型的应用运行时只需要JRE,并不需要JDK,因此我们的基础镜像只需要安装JRE环境即可。

(5)使用 .dockerignore 文件

在使用git时,我们可以使用.gitignore文件忽略一些不需要做版本管理的文件。同理,使用.dockerignore文件允许我们在构建时,忽略一些不需要参与构建的文件,从而提升构建效率。.dockerignore的定义类似于.gitignore。

.dockerignore的本质是文本文件,Docker 构建时可以使用换行符来解析文件定义,每一行可以忽略一些文件或者文件夹。具体使用方式如下:

规则作用
## 开头的表示注释,# 后面所有内容将会被忽略
/tmp匹配当前目录下任何以 tmp 开头的文件或者文件夹
*.md匹配以 .md 为后缀的任意文件
tem?匹配以 tem 开头并且以任意字符结尾的文件,?代表任意一个字符
!README.md!README.md ! 表示排除忽略。例如 .dockerignore 定义如下:
*.md 表示除了 README.md 文件外所有以 .md 结尾的文件。

(6)尽量使用构建缓存

Docker 构建过程中,每一条 Dockerfile 指令都会提交为一个镜像层,下一条指令都是基于上一条指令构建的。如果构建时发现要构建的镜像层的父镜像层已经存在,并且下一条命令使用了相同的指令,即可命中构建缓存。

Docker 构建时判断是否需要使用缓存的规则如下:

  • 从当前构建层开始,比较所有的子镜像,检查所有的构建指令是否与当前完全一致,如果不一致,则不使用缓存;
  • 一般情况下,只需要比较构建指令即可判断是否需要使用缓存,但是有些指令除外(例如ADD和COPY);
  • 对于ADD和COPY指令不仅要校验命令是否一致,还要为即将拷贝到容器的文件计算校验和(根据文件内容计算出的一个数值,如果两个文件计算的数值一致,表示两个文件内容一致 ),命令和校验和完全一致,才认为命中缓存。

基于 Docker 构建时的缓存特性,我们可以把不轻易改变的指令放到 Dockerfile 前面(例如安装软件包),而可能经常发生改变的指令放在 Dockerfile 末尾(例如编译应用程序)。

如想要定义一些环境变量并且安装一些软件包,可以按照如下顺序编写 Dockerfile:

FROM centos:7

# 设置环境变量指令放前面
ENV PATH /usr/local/bin:$PATH

# 安装软件指令放前面
RUN yum install -y make

# 把业务软件的配置,版本等经常变动的步骤放最后

...

按照上面原则编写的 Dockerfile 在构建镜像时,前面步骤命中缓存的概率会增加,可以大大缩短镜像构建时间。

(7)正确设置时区

从 Docker Hub 拉取的官方操作系统镜像大多数都是 UTC 时间(世界标准时间)。如果你想要在容器中使用中国区标准时间(东八区),请根据使用的操作系统修改相应的时区信息,下面我介绍几种常用操作系统的修改方式:

Ubuntu 和Debian 系统

Ubuntu 和Debian 系统可以向 Dockerfile 中添加以下指令:

RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN echo "Asia/Shanghai" >> /etc/timezone

CentOS系统

CentOS 系统则向 Dockerfile 中添加以下指令:

RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

(8)使用国内软件源加快镜像构建速度

由于我们常用的官方操作系统镜像基本都是国外的,软件服务器大部分也在国外,所以我们构建镜像的时候想要安装一些软件包可能会非常慢。

这里以 CentOS 7 为例,介绍一下如何使用 163 软件源(国内有很多大厂,例如阿里、腾讯、网易等公司都免费提供的软件加速源)加快镜像构建。

首先在容器构建目录创建文件 CentOS7-Base-163.repo,文件内容如下:

[base]
name=CentOS-$releasever - Base - 163.com
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=os
baseurl=http://mirrors.163.com/centos/$releasever/os/$basearch/
gpgcheck=1
gpgkey=http://mirrors.163.com/centos/RPM-GPG-KEY-CentOS-7
#released updates

[updates]
name=CentOS-$releasever - Updates - 163.com
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=updates
baseurl=http://mirrors.163.com/centos/$releasever/updates/$basearch/
gpgcheck=1
gpgkey=http://mirrors.163.com/centos/RPM-GPG-KEY-CentOS-7
#additional packages that may be useful

[extras]
name=CentOS-$releasever - Extras - 163.com
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=extras
baseurl=http://mirrors.163.com/centos/$releasever/extras/$basearch/
gpgcheck=1
gpgkey=http://mirrors.163.com/centos/RPM-GPG-KEY-CentOS-7
#additional packages that extend functionality of existing packages

[centosplus]
name=CentOS-$releasever - Plus - 163.com
baseurl=http://mirrors.163.com/centos/$releasever/centosplus/$basearch/
gpgcheck=1
enabled=0
gpgkey=http://mirrors.163.com/centos/RPM-GPG-KEY-CentOS-7

然后在 Dockerfile 中添加如下指令:

COPY CentOS7-Base-163.repo /etc/yum.repos.d/CentOS7-Base.repo

执行完上述步骤后,再使用yum install命令安装软件时就会默认从 163 获取软件包,这样可以大大提升构建速度。

(9)最小化镜像层数

在构建镜像时尽可能地减少 Dockerfile 指令行数。例如我们要在 CentOS 系统中安装make和net-tools两个软件包,应该在 Dockerfile 中使用以下指令:

RUN yum install -y make net-tools

而不应该写成这样:

RUN yum install -y make
RUN yum install -y net-tools

Dockerfile 指令书写建议

(1)RUN

RUN指令在构建时将会生成一个新的镜像层并且执行RUN指令后面的内容。

使用RUN指令时应该尽量遵循以下原则:

当RUN指令后面跟的内容比较复杂时,建议使用反斜杠(\) 结尾并且换行;
RUN指令后面的内容尽量按照字母顺序排序,提高可读性。
如想在官方的 CentOS 镜像下安装一些软件,一个建议的 Dockerfile 指令如下:

FROM centos:7RUN yum install -y automake \
                   curl \
                   python \
                   vim

(2)CMD 和 ENTRYPOINT

CMD 和 ENTRYPOINT 指令都是容器运行的命令入口,这两个指令使用中有很多相似的地方和不同。

CMD 和 ENTRYPOINT 两种相同的基本使用格式。

  • CMD/ENTRYPOINT[“command” , “param”]。这种格式是使用 Linux 的exec实现的, 一般称为exec模式,这种书写格式为CMD/ENTRYPOINT后面跟 json 数组,也是Docker 推荐的使用格式。
  • CMD/ENTRYPOINTcommand param ,这种格式是基于 shell 实现的, 通常称为shell模式。当使用shell模式时,Docker 会以 /bin/sh -c command 的方式执行命令。

使用 exec 模式启动容器时,容器的 1 号进程就是 CMD/ENTRYPOINT 中指定的命令,而使用 shell 模式启动容器时相当于我们把启动命令放在了 shell 进程中执行,等效于执行 /bin/sh -c “task command” 命令。因此 shell 模式启动的进程在容器中实际上并不是 1 号进程。

CMD 和 ENTRYPOINT 两个指令的区别:

  • Dockerfile 中如果使用了ENTRYPOINT指令,启动 Docker 容器时需要使用 –entrypoint 参数才能覆盖 Dockerfile 中的ENTRYPOINT指令 ,而使用 CMD 设置的命令则可以被 docker run 后面的参数直接覆盖。
  • ENTRYPOINT 指令可以结合 CMD 指令使用,也可以单独使用,而 CMD 指令只能单独使用。

什么时候应该使用 ENTRYPOINT ,什么时候使用 CMD 呢?
如果希望镜像足够灵活,推荐使用CMD指令。如果你的镜像只执行单一的具体程序,并且不希望用户在执行docker run 时覆盖默认程序,建议使用ENTRYPOINT。

最后再强调一下,无论使用CMD还是ENTRYPOINT,都尽量使用exec模式。

(3)ADD 和 COPY

ADD和COPY指令功能类似,都是从外部往容器内添加文件。但是COPY指令只支持基本的文件和文件夹拷贝功能,ADD则支持更多文件来源类型,比如自动提取 tar 包,并且可以支持源文件为 URL 格式。

那么在日常应用中,我们应该使用哪个命令向容器里添加文件呢?你可能在想,既然ADD指令支持的功能更多,当然应该使用ADD指令了。然而事实恰恰相反,我更推荐你使用COPY指令,因为COPY指令更加透明,仅支持本地文件向容器拷贝,而且使用COPY指令可以更好地利用构建缓存,有效减小镜像体积。

当你想要使用ADD向容器中添加 URL 文件时,请尽量考虑使用其他方式替代。例如你想要在容器中安装 memtester(一种内存压测工具),你应该避免使用以下格式:

ADD http://pyropus.ca/software/memtester/old-versions/memtester-4.3.0.tar.gz /tmp/
RUN tar -xvf /tmp/memtester-4.3.0.tar.gz -C /tmp RUN make -C /tmp/memtester-4.3.0 && make -C /tmp/memtester-4.3.0 install

推荐写法:

RUN wget -O /tmp/memtester-4.3.0.tar.gz http://pyropus.ca/software/memtester/old-versions/memtester-4.3.0.tar.gz \ && tar -xvf /tmp/memtester-4.3.0.tar.gz -C /tmp \ && make -C /tmp/memtester-4.3.0 && make -C /tmp/memtester-4.3.0 install

(4)WORKDIR

为了使构建过程更加清晰明了,推荐使用 WORKDIR 来指定容器的工作路径,应该尽量避免使用 RUN cd /work/path && do some work 这样的指令。

为了使构建过程更加清晰明了,推荐使用 WORKDIR 来指定容器的工作路径,应该尽量避免使用 RUN cd /work/path && do some work 这样的指令。