译者按: Dockerfile 的语法非常简单,然而如何加快镜像构建速度,如何减少 Docker 镜像的大小却不是那么直观,需要积累实践经验。这篇博客可以帮助你快速掌握编写 Dockerfile 的技巧。

本文采用意译,版权归原作者所有

我已经使用 Docker 有一段时间了,其中编写 Dockerfile 是非常重要的一部分工作。在这篇博客中,我打算分享一些建议,帮助大家编写更好的 Dockerfile。

目标:

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

总结

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

示例

示例 Dockerfile 犯了几乎所有的错(当然我是故意的)。接下来,我会一步步优化它。假设我们需要使用 Docker 运行一个 Node.js 应用,下面就是它的 Dockerfile(CMD 指令太复杂了,所以我简化了,它是错误的,仅供参考)。

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 # this should start three processes, mysql and ssh
# in the background and node app in foreground
# isn't it beautifully terrible? <3
CMD mysql & sshd & npm start

构建镜像:

docker build -t wtf .

1. 编写.dockerignore 文件

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

.git/
node_modules/

2. 容器只运行单个应用

从技术角度讲,你可以在 Docker 容器中运行多个进程。你可以将数据库,前端,后端,ssh,supervisor 都运行在同一个 Docker 容器中。但是,这会让你非常痛苦:

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

因此,我建议大家为每个应用构建单独的 Docker 镜像,然后使用 Docker Compose 运行多个 Docker 容器。

现在,我从 Dockerfile 中删除一些不需要的安装包,另外,SSH 可以用docker exec替代。示例如下:

FROM ubuntu

ADD . /app

RUN apt-get update
RUN apt-get upgrade -y # we should remove ssh and mysql, and use
# separate container for database
RUN apt-get install -y nodejs # ssh mysql
RUN cd /app && npm install CMD npm start

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

Docker 镜像是分层的,下面这些知识点非常重要:

  • Dockerfile 中的每个指令都会创建一个新的镜像层。
  • 镜像层将被缓存和复用
  • 当 Dockerfile 的指令修改了,复制的文件变化了,或者构建镜像时指定的变量不同了,对应的镜像层缓存就会失效
  • 某一层的镜像缓存失效之后,它之后的镜像层缓存都会失效
  • 镜像层是不可变的,如果我们再某一层中添加一个文件,然后在下一层中删除它,则镜像中依然会包含该文件(只是这个文件在 Docker 容器中不可见了)。

Docker 镜像类似于洋葱。它们都有很多层。为了修改内层,则需要将外面的层都删掉。记住这一点的话,其他内容就很好理解了。

现在,我们将所有的RUN指令合并为一个。同时把apt-get upgrade删除,因为它会使得镜像构建非常不确定(我们只需要依赖基础镜像的更新就好了)

FROM ubuntu

ADD . /app

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

记住一点,我们只能将变化频率一样的指令合并在一起。将 node.js 安装与 npm 模块安装放在一起的话,则每次修改源代码,都需要重新安装 node.js,这显然不合适。因此,正确的写法是这样的:

FROM ubuntu

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

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

当镜像没有指定标签时,将默认使用latest 标签。因此, FROM ubuntu 指令等同于FROM ubuntu:latest。当时,当镜像更新时,latest 标签会指向不同的镜像,这时构建镜像有可能失败。如果你的确需要使用最新版的基础镜像,可以使用 latest 标签,否则的话,最好指定确定的镜像标签。

示例 Dockerfile 应该使用16.04作为标签。

FROM ubuntu:16.04  # it's that easy!

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

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

假设我们更新了 apt-get 源,下载,解压并安装了一些软件包,它们都保存在/var/lib/apt/lists/目录中。但是,运行应用时 Docker 镜像中并不需要这些文件。我们最好将它们删除,因为它会使 Docker 镜像变大。

示例 Dockerfile 中,我们可以删除/var/lib/apt/lists/目录中的文件(它们是由 apt-get update 生成的)。

FROM ubuntu:16.04

RUN apt-get update \
&& apt-get install -y nodejs \
# added lines
&& rm -rf /var/lib/apt/lists/* ADD . /app
RUN cd /app && npm install CMD npm start

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

在示例中,我们选择了ubuntu作为基础镜像。但是我们只需要运行 node 程序,有必要使用一个通用的基础镜像吗?node镜像应该是更好的选择。

FROM node

ADD . /app
# we don't need to install node
# anymore and use apt-get
RUN cd /app && npm install CMD npm start

更好的选择是 alpine 版本的node镜像。alpine 是一个极小化的 Linux 发行版,只有 4MB,这让它非常适合作为基础镜像。

FROM node:7-alpine

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

apk是 Alpine 的包管理工具。它与apt-get有些不同,但是非常容易上手。另外,它还有一些非常有用的特性,比如no-cache--virtual选项,它们都可以帮助我们减少镜像的大小。

7. 设置 WORKDIR 和 CMD

WORKDIR指令可以设置默认目录,也就是运行RUN / CMD / ENTRYPOINT指令的地方。

CMD指令可以设置容器创建是执行的默认命令。另外,你应该讲命令写在一个数组中,数组中每个元素为命令的每个单词(参考官方文档)。

FROM node:7-alpine

WORKDIR /app
ADD . /app
RUN npm install CMD ["npm", "start"]

8. 使用 ENTRYPOINT (可选)

ENTRYPOINT指令并不是必须的,因为它会增加复杂度。ENTRYPOINT是一个脚本,它会默认执行,并且将指定的命令错误其参数。它通常用于构建可执行的 Docker 镜像。entrypoint.sh 如下:

#!/usr/bin/env sh
# $0 is a script name,
# $1, $2, $3 etc are passed arguments
# $1 is our command
CMD=$1 case "$CMD" in
"dev" )
npm install
export NODE_ENV=development
exec npm run dev
;; "start" )
# we can modify files here, using ENV variables passed in
# "docker create" command. It can't be done during build process.
echo "db: $DATABASE_ADDRESS" >> /app/config.yml
export NODE_ENV=production
exec npm start
;; * )
# Run custom command. Thanks to this line we can still use
# "docker run our_image /bin/bash" and it will work
exec $CMD ${@:2}
;;
esac

示例 Dockerfile:

FROM node:7-alpine

WORKDIR /app
ADD . /app
RUN npm install ENTRYPOINT ["./entrypoint.sh"]
CMD ["start"]

可以使用如下命令运行该镜像:

# 运行开发版本
docker run our-app dev # 运行生产版本
docker run our-app start # 运行bash
docker run -it our-app /bin/bash

9. 在 entrypoint 脚本中使用 exec

在前文的 entrypoint 脚本中,我使用了exec命令运行 node 应用。不使用exec的话,我们则不能顺利地关闭容器,因为 SIGTERM 信号会被 bash 脚本进程吞没。exec命令启动的进程可以取代脚本进程,因此所有的信号都会正常工作。

10. COPY 与 ADD 优先使用前者

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

FROM node:7-alpine

WORKDIR /app

COPY . /app
RUN npm install ENTRYPOINT ["./entrypoint.sh"]
CMD ["start"]

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

我们应该把变化最少的部分放在 Dockerfile 的前面,这样可以充分利用镜像缓存。

示例中,源代码会经常变化,则每次构建镜像时都需要重新安装 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"]

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

运行 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"]

ENV指令指定的环境变量在容器中可以使用。如果你只是需要指定构建镜像时的变量,你可以使用ARG指令。

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

使用LABEL指令,可以为镜像设置元数据,例如镜像创建者或者镜像说明。旧版的 Dockerfile 语法使用MAINTAINER指令指定镜像创建者,但是它已经被弃用了。有时,一些外部程序需要用到镜像的元数据,例如nvidia-docker需要用到com.nvidia.volumes.needed。示例如下:

FROM node:7-alpine
LABEL maintainer "jakub.skalecki@example.com"
...

14. 添加 HEALTHCHECK

运行容器时,可以指定--restart always选项。这样的话,容器崩溃时,Docker 守护进程(docker daemon)会重启容器。对于需要长时间运行的容器,这个选项非常有用。但是,如果容器的确在运行,但是不可(陷入死循环,配置错误)用怎么办?使用HEALTHCHECK指令可以让 Docker 周期性的检查容器的健康状况。我们只需要指定一个命令,如果一切正常的话返回 0,否则返回 1。对 HEALTHCHECK 感兴趣的话,可以参考这篇博客。示例如下:

FROM node:7-alpine
LABEL maintainer "jakub.skalecki@example.com" 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
HEALTHCHECK CMD curl --fail http://localhost:$APP_PORT || exit 1 ENTRYPOINT ["./entrypoint.sh"]
CMD ["start"]

当请求失败时,curl --fail 命令返回非 0 状态。

关于Fundebug

Fundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了10亿+错误事件,付费客户有Google、360、金山软件、百姓网等众多品牌企业。欢迎大家免费试用

版权声明

转载时请注明作者Fundebug以及本文地址:

https://blog.fundebug.com/2017/05/15/write-excellent-dockerfile/

如何编写最佳的Dockerfile的更多相关文章

  1. 如何运用多阶构建编写优雅的Dockerfile

    导读 Kubernetes要从容器化开始,而容器又需要从Dockerfile开始,本文将介绍如何写出一个优雅的Dockerfile文件. 文章主要内容包括: Docker容器 Dockerfile 使 ...

  2. dart系列之:手写Library,Library编写最佳实践

    目录 简介 使用part和part of src中的文件 package中的lib文件 总结 简介 Library是dart用来组织代码的一种非常有用的方式,通过定义不同的Library,可以将非常有 ...

  3. Dockerfile 编写

    转: https://blog.fundebug.com/2017/05/15/write-excellent-dockerfile/如何编写最佳的Dockerfile 译者按: Dockerfile ...

  4. Docker 入门指南——Dockerfile 指令

    COPY 复制文件 格式: COPY [--chown=<user>:<group>] <源路径>... <目标路径> 源路径可以是多个,甚至可以使通配 ...

  5. Docker-核心笔记(含Dockerfile,Compose)

    Docker-核心笔记(含Dockerfile,Compose) 2017/03 Chenxin 参考 https://yeasy.gitbooks.io/docker_practice Docker ...

  6. 自己编写k8s

    ## 基于Docker和Kubernetes的企业级DevOps实践训练营 ### 课程准备 1. 离线镜像包 百度:https://pan.baidu.com/s/1N1AYGCYftYGn6L0Q ...

  7. Docker解析及轻量级PaaS平台演练(三)--Dockerfile编写

    在本篇中将介绍Dockerfile的编写 除了通过修改Image,创建Container,在打包成Image来创建我们需要的Image之外 我们还可以编写Dockerfile文件,通过build来创建 ...

  8. Prometheus之Dockerfile编写、镜像构建、容器启动

    目录 从官方镜像启动:prom/prometheus 官方Dockerfile分析 编写自己的Dockerfile 构建镜像: 启动容器: 从官方镜像启动:prom/prometheus 拉取镜像 $ ...

  9. 使用Dockerfile构建镜像-Docker for Web Developers(5)

    1.理解Dockerfile语法 语法命令 命令功能 举例 FROM 所有的dockerfile都必须以FROM命令指定镜像基于哪个基础镜像来制作 FROM ubuntu:14:04 MAINTAIN ...

随机推荐

  1. nuxt Window 或 Document未定义解决方案

    概述 在用nuxt开发服务端渲染项目并引入第三方库的时候,经常会遇到window或document未定义的情况,原因是这个第三方库里面用到了window或者document,然后在服务端打包的时候,n ...

  2. 闲聊js中的apply、call和arguments

    JavaScript提供了apply和call两种调用方式来确定函数中的this的指向,在现实编码中,我确实 很少接触到这两个方法.但很无奈,很多面试题都要考这两种方法,我又没怎么用到,所以我们先来 ...

  3. SpringCloud-服务注册与发现(注册中心)

    SpringCloud-服务注册与发现(注册中心) 作者 : Stanley 罗昊 [转载请注明出处和署名,谢谢!] 注:作者使用IDEA + Gradle 注:需要有一定的java&& ...

  4. php 通过header下载中文文件名 压缩包损坏或文件不存在的问题

    开发中大家都是使用的utf8编码,昨天遇到一个奇坑,本是一件很小的问题,解决也浪费了个吧小时.废话不多说,植入正题: 文件下载方式:通过header二进制流文件下载需求: 文件上传保留文件名不变数据字 ...

  5. 关于ios的光标和键盘回弹问题

    最近再做项目的时候(移动端),遇到了两个小问题,一个是ios端键盘不回弹的问题(微信浏览器),另一个是ios输入光标位置问题.首先看第二个问题: 就是点击一个按钮,要把输入框里面原来的内容加上一些固定 ...

  6. 什么是HTML?

    在了解概念之前,先做以下操作,在桌面新建一个txt文件,然后在txt文件中输入:“Hello World”,保存该文件并将其后缀名改为.html,然后双击打开,你就能在浏览器上看到页面显示“Hello ...

  7. 通过user.MYD MySQL密码

    具体的获取方法: 1)通过hex 十六进制的工具打开user.MYD文件 这里使用:0XED 2)复制MySQL密码出来,进行分析 *CEAF6E9FA3A448A25AC9D00C8D7078385 ...

  8. git入门手册:git的基本安装,本地库管理,远程上传

    前言: git是分布式的版本库控制系统,它能方便你将自己的代码寄存于远程服务器上,从而实现集体合作开发.git有GUI 图形界面,然而使用终端命令仍是主流.以下基于Ubuntu系统操作git(其方式也 ...

  9. 好代码是管出来的——使用Jenkins搭建CI服务器

    Jenkins是一个开源的跨平台的CI工具,它可以部署在Windows.Linux等平台上,并且Jenkins提供了非常丰富的插件来帮助完成编译.测试.部署等工作. 本文将介绍在Windows平台上使 ...

  10. sql语句 汉字转拼音首字母

    (1)------------------------------------------------------------------------------------------------- ...