Docker容器的优化思路
镜像构建的过程,视具体业务场景的不同而不同。在很多情况下,我们需要先以满足业务目标为准,而不是镜像的构建层数。如果需要减少镜像层数,一定要选择合适的基础镜像或者创建符合需要的基础镜像。
1. 选择基础镜像
选择合适产品的基础镜像,这点相对来说非常重要。选择一个合适的基础镜像,需要能够满足运行应用所需要的最小的镜像。理论上是能用小的就不要用大的,能用轻量级的就不要用重量级的,能用性能好的就不要用性能差的,能用稳定版就不要用开发版。
比如我们构建 Java 语言的程序,最好的方式就是使用官方提供的 openjdk:8 作为基础镜像,而非使用比较大的 ubuntu:18.04 作为基础镜像。另外,还有一个需要我们注意的地方就是,尽可能使用官方的特定版本的镜像,而不要使用 latest 这个频繁变动的 tag 作为基础镜像。
FROM ubuntu:18.04
FROM openjdk:latest
FROM openjdk:8
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
同时,在使用 COPY/ADD 等命令的时候越具体越好,最好只复制所需的内容。
FROM ubuntu:18.04
WORKDIR /opt/app
COPY target/docker_patch.py /opt/app/
RUN apt update -y htop
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
4. 清理中间结果,减少镜像大小
这点很易于理解,通常来讲,体积更小,部署更快!因此在构建过程中,我们需要清理那些最终不需要的代码或文件。比如说,临时文件、源代码、缓存等等。
rm -rf /var/lib/apt/lists/* /tmp/*
rm -rf /etc/nginx/sites-enabled/default
5. 减少冗余文件,减少镜像大小
使用 .dockerignore 文件用于忽略那些镜像构建时非必须的文件,这些文件可以是开发文档、日志、其他无用的文件。注意再添加文件或者文件夹的时候,最好的方式是先全部排除,之后再添加。
*
.*
!config/config-docker.yml
!config/logging.conf
!docker/nginx
!docker/supervisor
......
- 使用 runtime 镜像,提高可维护性
当我们维护的项目越来越大的时候,使用单独的 Dockerfile 文件来维护就变得越来越笨重,且构建时间也越来越长。为了便捷且高效的管理,就需要我们构建基本的 runtime 镜像。在 runtime 镜像中,主要包含项目所依赖的基础环境包,多为很少改动的内容。而我们的项目就不用从基础镜像开始,而是从 runtime 镜像开始,大大的加速的构件速度。
7. 使用多段构建,提高可维护性
Docker v17.05 开始支持多阶段构建。多段构建的目的就是,使用多阶段构建来删除构建依赖项。容器多阶段构建可由多个 FROM 语句构成,每个 FROM 语句开始一个新的阶段。它们可以用 AS 关键字进行别名命名。我们用它来别名作为我们的第一阶段构建器,以便稍后引用,它将在一致的环境中包含所有构建依赖项。
第二阶段是我们的最后阶段,将产生最终镜像包。它将包括运行时的严格必要条件,在本例中是基于 Alpine 的最小 JRE(Java运行时)。中间构建器阶段将被缓存但不会出现在最终映像中。要将构建的内容添加到最终镜像的话,请使用 COPY --from=STAGE_NAME 参数进行制定。
# 使用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 指明缓存来源的文件夹。
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) |
---|---|
busybox | 1 |
ubuntu | 188 |
swarm | 17 |
nginx | 134 |
registry | 423 |
redis | 151 |
mysql | 360 |
mongo | 317 |
node | 643 |
debian | 125 |
使用较小的基础镜像(例如 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
docker search
这个命令可以帮助你在命令行中方便的搜索 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 这样的指令。