Dockerfile最佳实践

Posted by NianShen Blog on March 5, 2019

Dockerfile中常见指令

  • FROM指定一个基础镜像,一般情况下一个可用的Dockerfile FROM是第一个指令。image可以是任何合理存在的image镜像。FROM可以在一个Dockerfile中出现多次,如果没有指定镜像tag,将会使用latest镜像版本。
  • RUN 在当前image中执行任意合法命令并提交执行结果。命令执行提交后,就会自动执行Dockerfile中的下一个指令。通过RUN指令可以对image镜像进行定制化构建。
  • ENV 指令可以用于为docker容器设置环境变量
  • USER 用来切换运行属主身份的。Docker默认是使用root,但若不需要,建议切换使用者身分,毕竟root权限太大了,使用上有安全的风险。
  • WORKDIR 用来切换工作目录的。Docker默认的工作目录是/,只有RUN能执行cd命令切换目录,而且还只作用在当下的RUN,也就是说每一个RUN都是独立进行的。如果想让其他指令在指定的目录下执行就得靠WORKDIR。使用WORKDIR进行的目录改变是持久的,不用每个指令前都使用一次WORKDIR。
  • COPY 将文件从路径 复制添加到容器内部路径 必须是想对于源文件夹的一个文件或目录。 是目标容器中的绝对路径。
  • ADD 将文件从路径 复制添加到容器内部路径 。与COPY的不同在于ADD可以自动解压文件。
  • VOLUME 指定需要持久化的目录
  • EXPOSE 指定需要暴露的端口
  • CMD Dockerfile中只能有一个CMD指令。如果指定了多个,那么只有最后个CMD指令是生效的。
  • ENTRYPOINT image运行成instance(container)时,要执行的命令或者文件。

我们的目标是星辰大海

  • 更快的构建速度
  • 更小的镜像大小
  • 更少的镜像分层
  • 充分利用镜像缓存
  • 增加Dockerfile可读性
  • 让Docker容器使用起来更简单

该怎样实现

  • 编写.dockerignore文件
  • 容器只运行单个应用
  • 将多个RUN指令合并为一个
  • 基础镜像的标签不要用latest
  • 每个RUN指令后删除多余文件
  • 选择合适的基础镜像(alpine版本最好)
  • 设置WORKDIR和CMD
  • COPY与ADD优先使用前者
  • 合理调整COPY与RUN的顺序
  • 设置默认的环境变量,映射端口和数据卷
  • 使用LABEL设置镜像元数据

违反所有最佳实践的一个Dockerfile的例子,逐渐改造他

FROM ubuntu
ADD . /app
RUN apt-get update  
RUN apt-get upgrade -y  
RUN apt-get install -y nodejs ssh mysql  
RUN cd /app && npm install
CMD mysql & sshd & npm start

1.编写.dockerignore文件

构建镜像时,Docker需要先准备context ,将所有需要的文件收集到进程中。默认的context包含Dockerfile目录中的所有文件,但是实际上并不需要.git目录和node_modules目录等内容。.dockerignore的作用和语法类似于.gitignore可以忽略一些不需要的文件,这样可以有效加快镜像构建时间,同时减少Docker镜像的大小。

2.容器只运行单个应用

从技术上来说,可以在Docker容器中运行多个进程。你可以将数据库,前端,后端,supervisor都运行在同一个Docker容器中。比如说laputa和pangu-master的那套代码。

  • 非常长的构建时间(修改前端之后,整个后端也需要重新构建)
  • 非常大的镜像size
  • 多个应用的日志难以处理(不能直接使用stdout,否则多个应用的日志会混合到一起)
  • 横向扩展时非常浪费资源(不同的应用需要运行的容器数并不相同)

    3.将多个RUN指令合并为一个

    Docker镜像是分层的,类似于一个洋葱,想要吃到最里边的葱心,就需要将外边的葱皮一层一层的都剥掉。

  • Dockerfile中的每个指令都会创建一个新的镜像层。
  • 镜像层将被缓存和复用
  • 当Dockerfile的指令修改了,复制的文件变化了,或者构建镜像时指定的变量不同了,对应的镜像层缓存就会失效
  • 某一层的镜像缓存失效之后,它之后的镜像层缓存都会失效
  • 将变化频率一样的指令合并在一起。 通过两个例子对比,如果将node.js安装与npm模块安装放在一起的话,则每次修改源代码,都需要重新安装node.js,这显然不合适。Docker镜像的分层理解一定要深刻,变动少一定要放到前边。

不合理Dockerfile

FROM ubuntu

ADD . /app

RUN apt-get update \  
    && apt-get install -y nodejs \
    && cd /app \
    && npm install

CMD npm start

合理Dockerfile

FROM ubuntu

RUN apt-get update && apt-get install -y nodejs  
ADD . /app  
RUN cd /app && npm install

CMD npm start

4.基础镜像的标签不要用latest

当base镜像没有指定tag时,将默认使用latest tag。FROM ubuntu指令等同于FROM ubuntu:latest 当base镜像更新后,latest tag会指向不同的镜像(操作系统版本升级了)这时构建镜像有可能失败。如果确定需要使用最新版的base image,可以使用latest tag,否则最好指定确定的image tag。

5.每个RUN指令后删除多余文件

假设我们执行了apt-get update,下载解压安装了一些软件包,它们都保存在/var/lib/apt/lists/目录中。问题在于,runtime的Docker镜像中并不需要这些文件。最好将它们删除,因为它会使Docker镜像变大。

FROM ubuntu:16.04

RUN apt-get update \  
    && apt-get install -y nodejs \
    && rm -rf /var/lib/apt/lists/*

ADD . /app  
RUN cd /app && npm install

CMD npm start

6.选择合适的基础镜像(alpine版本最好)

除非我们有深层定制化镜像的强烈需求,否则最好用alpine版本的镜像,实例选择了ubuntu作为基础镜像,但是我们只需要运行node程序,没有必要使用一个通用的基础镜像,node镜像应该是更好的选择。

FROM node

ADD . /app  
RUN cd /app && npm install

CMD npm start

更好的选择是alpine版本的node镜像。alpine是一个极小化的Linux发行版,只有4MB,非常适合作为基础镜像。apk是Alpine的包管理工具,与apt-get有些不同,另外它还有一些非常有用的特性,比如no-cache,可以有效减少镜像的大小。

FROM node:7-alpine

ADD . /app  
RUN cd /app && npm install

CMD npm start

7.设置WORKDIR和CMD

WORKDIR指令可以设置默认目录,也就是运行RUN / CMD / ENTRYPOINT指令的地方。 CMD指令可以设置容器创建是执行的默认命令。命令应该写在一个数组中,数组中每个元素为命令的每个单词。

FROM node:7-alpine

WORKDIR /app  
ADD . /app  
RUN npm install

CMD ["npm", "start"]

8.COPY与ADD命令优先使用COPY

COPY指令非常简单,仅用于将文件拷贝到镜像中。ADD相对来讲复杂一些,可以用于下载远程文件以及解压压缩包。

9.合理调整COPY与RUN的顺序

把变化最少的部分放在Dockerfile的前面,这样可以充分利用image cache。示例中,源代码会经常变化,则每次构建镜像时都需要重新安装NPM模块,因此我们可以先拷贝package.json,然后安装NPM模块,最后才拷贝其余的源代码。这样的话,即使源代码变化,也不需要重新安装NPM模块。

FROM node:7-alpine

WORKDIR /app

COPY package.json /app  
RUN npm install  
COPY . /app

ENTRYPOINT ["./entrypoint.sh"]  
CMD ["start"]

10.设置默认的环境变量,映射端口和数据卷

运行Docker容器时很可能需要一些环境变量。在Dockerfile设置默认的环境变量是一种很好的方式。我们应该在Dockerfile中设置映射端口和数据卷。

FROM node:7-alpine

ENV PROJECT_DIR=/app

WORKDIR $PROJECT_DIR

COPY package.json $PROJECT_DIR  
RUN npm install  
COPY . $PROJECT_DIR

ENV MEDIA_DIR=/media \  
    NODE_ENV=production \
    APP_PORT=3000

VOLUME $MEDIA_DIR  
EXPOSE $APP_PORT

ENTRYPOINT ["./entrypoint.sh"]  
CMD ["start"]

11.使用LABEL设置镜像元数据

使用LABEL指令,可以为镜像设置元数据,例如镜像创建者或者镜像说明。旧版的Dockerfile语法使用MAINTAINER指令指定镜像创建者,现已被弃用。

FROM node:7-alpine  
LABEL maintainer "nianshenlovelily@gmail.com"  
...