编写最佳的Dockerfile的方法

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

为了保证可读性,本文采用意译而非直译。另外,本文版权归原作者所有,翻译仅用于学习。

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

目标:

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

总结

  1. 编写.dockerignore文件
  2. 容器只运行单个应用
  3. 将多个RUN指令合并为一个
  4. 基础镜像的标签不要用latest
  5. 每个RUN指令后删除多余文件
  6. 选择合适的基础镜像(alpine版本最好)
  7. 设置WORKDIR和CMD
  8. 使用ENTRYPOINT (可选)
  9. 在entrypoint脚本中使用exec
  10. COPY与ADD优先使用前者
  11. 合理调整COPY与RUN的顺序
  12. 设置默认的环境变量,映射端口和数据卷
  13. 使用LABEL设置镜像元数据
  14. 添加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状态。

原文: How to write excellent Dockerfiles

译者: Fundebug

(0)

相关推荐

  • Docker 基础之Dockerfile命令详解

    Dockerfile 是一个文本格式的配置文件,用户可以使用 Dockerfile 快速创建自定义的镜像.我们会先介绍 Dockerfile 的基本结构及其支持的众多指令,并具体讲解通过执行指令来编写定制镜像的 Dockerfile. 基本结构 Dockerfile 由一行行命令语句组成,并且支持已 # 开头的注释行.一般而言,Dockerfile 的内容分为四个部分:基础镜像信息.维护者信息.镜像操作指令和容器启动时执行指令.例如: # This dockerfile uses the Ubu

  • docker官方镜像下载及使用Dockerfile创建镜像的方法

    1.登陆docker hut官方网站:https://hub.docker.com/ 2.search centos 3.docker pull centos:7.2.1511 4.创建Dockerfile文件: [root@vmhost centos7.2]# cat Dockerfile [plain] view plain copy FROM centos:7.2.1511 MAINTAINER wanghongwei(wanghongwei@4paradigm.com) RUN yum

  • Dockerfile 指令 ADD 和 COPY介绍

    一.ADD指令 ADD指令的功能是将主机构建环境(上下文)目录中的文件和目录.以及一个URL标记的文件 拷贝到镜像中. 其格式是: ADD  源路径  目标路径 如: #test FROM ubuntu MAINTAINER hello ADD test1.txt test1.txt ADD test1.txt test1.txt.bak ADD test1.txt /mydir/ ADD data1 data1 ADD data2 data2 ADD zip.tar /myzip 有如下注意事

  • Dockerfile指令详解

    什么是Dockerfile Dockerfile是由一系列命令和参数构成的脚本,这些命令应用于基础镜像并最终创建一个新的镜像.它们简化了从头到尾的流程并极大的简化了部署工作.Dockerfile从FROM命令开始,紧接着跟随者各种方法,命令和参数.其产出为一个新的可以用于创建容器的镜像. 当你在使用 Docker构建镜像的时候,每一个命令都会在前一个命令的基础上形成一个新层.这些基础镜像可以用于创建新的容器.本篇文章将手把手教您如何从基础镜像,一步一步,一层一层的从Dockerfile构建容器的

  • Docker Dockerfile的使用实例

    Dockerfile FROM 基础镜像 MAINTAINER 维护这信息 RUN 运行什么命令,在命令前面加上RUN ADD 往里面加点文件,copy文件,会自动解压 WORKDIR 当前的工作目录 VOLUME 目录挂载 EXPOSE 开放的端口 RUN 进程要一直运行 实战:构建nginx wget  http://xiazai.jb51.net/201611/yuanma/nginx-1.9.3(jb51.net).rar wget ftp://ftp.csx.cam.ac.uk/pub

  • docker在已有的tomcat镜像上打新的镜像的Dockerfile编写说明介绍

    如果需要将某个文件替换或者添加到旧的tomcatimage中,形成一个新的image,需要在dockerfile中做一下操作 FROM old_tomcat #这里可以使用docker官方的镜像,以下说明都是在此基础上的实例 MAINTAINER ****************** #如果需要替换原有的配置文件或者脚本 ADD web.xml /usr/local/tomcat/webapps/manager/WEB-INF/ ADD tomcat-users.xml /usr/local/t

  • Dockerfile 部署java web的环境详解

    Dockerfile 构建java web 环境 Dockfile 介绍: Dockfile是一种被Docker程序解释的脚本,Dockerfile由一条一条的指令组成,每条指令对应Linux下面的一条命令.Docker程序将这些Dockerfile指令翻译真正的Linux命令.Dockerfile有自己书写格式和支持的命令,Docker程序解决这些命令间的依赖关系,类似于Makefile.Docker程序将读取Dockerfile,根据指令生成定制的image.相比image这种黑盒子,Doc

  • Docker中镜像构建文件Dockerfile与相关命令的详细介绍

    前言 使用docker build命令或使用Docker Hub的自动构建功能构建Docker镜像时,都需要一个Dockerfile文件.Dockerfile文件是一个由一系列构建指令组成的文本文件,docker build命令会根据这些构建指令完成Docker镜像的构建.本文将会介绍Dockerfile文件,及其中使用的构建指令. 1. Dockerfile文件使用 docker build命令会根据Dockerfile文件及上下文构建新Docker镜像.构建上下文是指Dockerfile所在

  • 编写最佳的Dockerfile的方法

    Dockerfile的语法非常简单,然而如何加快镜像构建速度,如何减少Docker镜像的大小却不是那么直观,需要积累实践经验.这篇博客可以帮助你快速掌握编写Dockerfile的技巧. 为了保证可读性,本文采用意译而非直译.另外,本文版权归原作者所有,翻译仅用于学习. 我已经使用Docker有一段时间了,其中编写Dockerfile是非常重要的一部分工作.在这篇博客中,我打算分享一些建议,帮助大家编写更好的Dockerfile. 目标: 更快的构建速度 更小的Docker镜像大小 更少的Dock

  • 使用coffeescript编写node.js项目的方法汇总

    Node.js 基于JavaScript编写应用,JavaScript是我的主要开发语言.CoffeeScript是编译为JavaScript的编程语言.其实CoffeeScript语言因其可以一对一的翻译为JavaScript的特性,使用起来也非常灵活.将其引入项目的方式也有很多种,在此,我将使用coffeescript编写node.js项目的方法做一个汇总. 直接使用coffee指令运行纯coffeescript项目 一般提起coffeescript,自然而然地会想到他是javascript

  • vue请求本地自己编写的json文件的方法

    1.第一步,这是目录结构 2.接下来是build/webpack.dev.conf.js文件需要配置的内容 代码: //vue配置请求本地json数据 const express = require('express') const app = express() const appData = require('../static/major_info.json')//加载本地json文件 const majorlist = appData.contents;//获取本地对应数据 const

  • python3使用flask编写注册post接口的方法

    使用python3的Flask库写了一个接口,封装了很多东西,仅供参考即可! 代码如下: #!/usr/bin/python3 # -*- coding: utf-8 -*- import re from flask import request from flask_restful import Resource import aes_utils import mysql_utils import sqls_user class Register(Resource): """

  • docker 添加端口及获取dockerfile的方法

    从docker image中获取 dockerfile docker history --format {{.CreatedBy}} --no-trunc=true $DockerImage |sed "s//bin/sh\ -c\ #(nop)\ //g"|sed "s//bin/sh\ -c/RUN/g" | tac 注:该docker file 不对目录,端口进行映射 同时不执行docker开机启动的脚本及 server 服务 docker 多端口映射 Doc

  • 用VScode编写C++大型项目的方法步骤

    目录 vscode远程开发配置 选用vscode的原因 配置流程 安装C++扩展 ​clang-format格式化代码 安装clang-format 创建clang-format文件 vscode支持clang-format 代码检查工具clang-tidy clang-tidy的安装 cmake实现代码工程化 cmake实践 clang-tidy在cmake中的配置 ccache加速编译 ccache安装 结合cmake使用 vscode远程开发配置 由于后面项目用到了epoll和splice

  • JavaScript 编写枚举的最有效方法分享

    目录 前言 定义枚举 << 是什么? 用法 如何理解这段代码? 我们为什么要使用这个技巧? 学习Vue源码 前言 假设有这样一个场景,我们需要统计员工的技术栈,目前我们需要标记的技术有 CSS.JavaScript.HTML.WebGL. 然后我可以这样写枚举: const SKILLS = { CSS: 1 , JS: 2, HTML: 3, WEB_GL: 4 } 之前是这样写的,但是,最近看vue源码的时候,发现了一个高效使用枚举的技巧,在这里分享给大家. 定义枚举 我们可以这样写上面的

  • 编写高性能Lua代码的方法

    前言 Lua是一门以其性能著称的脚本语言,被广泛应用在很多方面,尤其是游戏.像<魔兽世界>的插件,手机游戏<大掌门><神曲><迷失之地>等都是用Lua来写的逻辑. 所以大部分时候我们不需要去考虑性能问题.Knuth有句名言:"过早优化是万恶之源".其意思就是过早优化是不必要的,会浪费大量时间,而且容易导致代码混乱. 所以一个好的程序员在考虑优化性能前必须问自己两个问题:"我的程序真的需要优化吗?".如果答案为是,那么再

  • 利用Angular.js编写公共提示模块的方法教程

    前言 在编写一些大型工程的时候,会经常遇到一些公用提示,使用框架自带很多时候不方便,于是我手写了一个,下面来看看详细的介绍: 效果图如下 方法如下 一.先在angular中注册一个模块 二. 注册一个模块 注入依赖 三. 返回不同的方法应对不同情况 四. 获取模板路径 五. 编写模板内容 和普通的页面一样调用使用angular服务 六. 1. 开启http服务获取模板内容 2.  重点注意 $template = $compile(template)(scope); 这句代码非常重要  用于激活

  • Ubuntu中为Android系统上编写Linux内核驱动程序实现方法

    在智能手机时代,每个品牌的手机都有自己的个性特点.正是依靠这种与众不同的个性来吸引用户,营造品牌凝聚力和用户忠城度,典型的代表非iphone莫属了.据统计,截止2011年5月,AppStore的应用软件数量达381062个,位居第一,而Android Market的应用软件数量达294738,紧随AppStore后面,并有望在8月份越过AppStore.随着Android系统逐步扩大市场占有率,终端设备的多样性亟需更多的移动开发人员的参与.据业内统计,Android研发人才缺口至少30万.目前,

随机推荐