Dockerfile 编写
转: https://blog.fundebug.com/2017/05/15/write-excellent-dockerfile/如何编写最佳的Dockerfile
译者按: 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 |
构建镜像:
docker build -t wtf . |
1. 编写.dockerignore 文件
构建镜像时,Docker 需要先准备context
,将所有需要的文件收集到进程中。默认的context
包含 Dockerfile 目录中的所有文件,但是实际上,我们并不需要.git 目录,node_modules 目录等内容。 .dockerignore
的作用和语法类似于 .gitignore
,可以忽略一些不需要的文件,这样可以有效加快镜像构建时间,同时减少 Docker 镜像的大小。示例如下:
.git/ |
2. 容器只运行单个应用
从技术角度讲,你可以在 Docker 容器中运行多个进程。你可以将数据库,前端,后端,ssh,supervisor 都运行在同一个 Docker 容器中。但是,这会让你非常痛苦:
- 非常长的构建时间(修改前端之后,整个后端也需要重新构建)
- 非常大的镜像大小
- 多个应用的日志难以处理(不能直接使用 stdout,否则多个应用的日志会混合到一起)
- 横向扩展时非常浪费资源(不同的应用需要运行的容器数并不相同)
- 僵尸进程问题 - 你需要选择合适的 init 进程
因此,我建议大家为每个应用构建单独的 Docker 镜像,然后使用 Docker Compose 运行多个 Docker 容器。
现在,我从 Dockerfile 中删除一些不需要的安装包,另外,SSH 可以用docker exec替代。示例如下:
FROM ubuntu ADD . /app RUN apt-get update |
3. 将多个 RUN 指令合并为一个
Docker 镜像是分层的,下面这些知识点非常重要:
- Dockerfile 中的每个指令都会创建一个新的镜像层。
- 镜像层将被缓存和复用
- 当 Dockerfile 的指令修改了,复制的文件变化了,或者构建镜像时指定的变量不同了,对应的镜像层缓存就会失效
- 某一层的镜像缓存失效之后,它之后的镜像层缓存都会失效
- 镜像层是不可变的,如果我们再某一层中添加一个文件,然后在下一层中删除它,则镜像中依然会包含该文件(只是这个文件在 Docker 容器中不可见了)。
Docker 镜像类似于洋葱。它们都有很多层。为了修改内层,则需要将外面的层都删掉。记住这一点的话,其他内容就很好理解了。
现在,我们将所有的RUN指令合并为一个。同时把apt-get upgrade
删除,因为它会使得镜像构建非常不确定(我们只需要依赖基础镜像的更新就好了)
FROM ubuntu ADD . /app RUN apt-get update \ |
记住一点,我们只能将变化频率一样的指令合并在一起。将 node.js 安装与 npm 模块安装放在一起的话,则每次修改源代码,都需要重新安装 node.js,这显然不合适。因此,正确的写法是这样的:
FROM ubuntu RUN apt-get update && apt-get install -y nodejs |
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 |
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 \ |
6. 选择合适的基础镜像(alpine 版本最好)
在示例中,我们选择了ubuntu
作为基础镜像。但是我们只需要运行 node 程序,有必要使用一个通用的基础镜像吗?node
镜像应该是更好的选择。
FROM node ADD . /app |
更好的选择是 alpine 版本的node
镜像。alpine 是一个极小化的 Linux 发行版,只有 4MB,这让它非常适合作为基础镜像。
FROM node:7-alpine ADD . /app |
apk是 Alpine 的包管理工具。它与apt-get
有些不同,但是非常容易上手。另外,它还有一些非常有用的特性,比如no-cache
和 --virtual
选项,它们都可以帮助我们减少镜像的大小。
7. 设置 WORKDIR 和 CMD
WORKDIR指令可以设置默认目录,也就是运行RUN
/ CMD
/ ENTRYPOINT
指令的地方。
CMD指令可以设置容器创建是执行的默认命令。另外,你应该讲命令写在一个数组中,数组中每个元素为命令的每个单词(参考官方文档)。
FROM node:7-alpine WORKDIR /app |
8. 使用 ENTRYPOINT (可选)
ENTRYPOINT指令并不是必须的,因为它会增加复杂度。ENTRYPOINT
是一个脚本,它会默认执行,并且将指定的命令错误其参数。它通常用于构建可执行的 Docker 镜像。entrypoint.sh 如下:
#!/usr/bin/env sh |
示例 Dockerfile:
FROM node:7-alpine WORKDIR /app |
可以使用如下命令运行该镜像:
# 运行开发版本 |
9. 在 entrypoint 脚本中使用 exec
在前文的 entrypoint 脚本中,我使用了exec
命令运行 node 应用。不使用exec
的话,我们则不能顺利地关闭容器,因为 SIGTERM 信号会被 bash 脚本进程吞没。exec
命令启动的进程可以取代脚本进程,因此所有的信号都会正常工作。
10. COPY 与 ADD 优先使用前者
COPY指令非常简单,仅用于将文件拷贝到镜像中。ADD相对来讲复杂一些,可以用于下载远程文件以及解压压缩包(参考官方文档)。
FROM node:7-alpine WORKDIR /app COPY . /app |
11. 合理调整 COPY 与 RUN 的顺序
我们应该把变化最少的部分放在 Dockerfile 的前面,这样可以充分利用镜像缓存。
示例中,源代码会经常变化,则每次构建镜像时都需要重新安装 NPM 模块,这显然不是我们希望看到的。因此我们可以先拷贝package.json
,然后安装 NPM 模块,最后才拷贝其余的源代码。这样的话,即使源代码变化,也不需要重新安装 NPM 模块。
FROM node:7-alpine WORKDIR /app COPY package.json /app |
12. 设置默认的环境变量,映射端口和数据卷
运行 Docker 容器时很可能需要一些环境变量。在 Dockerfile 设置默认的环境变量是一种很好的方式。另外,我们应该在 Dockerfile 中设置映射端口和数据卷。示例如下:
FROM node:7-alpine ENV PROJECT_DIR=/app WORKDIR $PROJECT_DIR COPY package.json $PROJECT_DIR |
ENV指令指定的环境变量在容器中可以使用。如果你只是需要指定构建镜像时的变量,你可以使用ARG指令。
13. 使用 LABEL 设置镜像元数据
使用LABEL指令,可以为镜像设置元数据,例如镜像创建者或者镜像说明。旧版的 Dockerfile 语法使用MAINTAINER指令指定镜像创建者,但是它已经被弃用了。有时,一些外部程序需要用到镜像的元数据,例如nvidia-docker需要用到com.nvidia.volumes.needed
。示例如下:
FROM node:7-alpine |
14. 添加 HEALTHCHECK
运行容器时,可以指定--restart always
选项。这样的话,容器崩溃时,Docker 守护进程(docker daemon)会重启容器。对于需要长时间运行的容器,这个选项非常有用。但是,如果容器的确在运行,但是不可(陷入死循环,配置错误)用怎么办?使用HEALTHCHECK指令可以让 Docker 周期性的检查容器的健康状况。我们只需要指定一个命令,如果一切正常的话返回 0,否则返回 1。对 HEALTHCHECK 感兴趣的话,可以参考这篇博客。示例如下:
FROM node:7-alpine |
当请求失败时,curl --fail
命令返回非 0 状态。
Dockerfile 编写的更多相关文章
- Docker容器化【Dockerfile编写&&搭建与使用Docker私有仓库】
# Docker 学习目标: 掌握Docker基础知识,能够理解Docker镜像与容器的概念 完成Docker安装与启动 掌握Docker镜像与容器相关命令 掌握Tomcat Nginx 等软件的常用 ...
- 最简单的Go Dockerfile编写姿势,没有之一!
1. Dockerfile一些额外注意点 选择最简单的镜像 比如alpine,整个镜像5M左右 设置镜像时区 RUN apk add --no-cache tzdata ENV TZ Asia/Sha ...
- docker 应用-2(Dockerfile 编写以及镜像保存提交)
我们可以从docker hub上pull别人的镜像,也可以将容器进行修改,然后commit镜像,并把镜像push到docker hub上被被人使用.但是,直接pull或者push镜像的方式太过笨重,尤 ...
- Dockerfile编写注意事项
转载自:https://blog.fundebug.com/2017/05/15/write-excellent-dockerfile/ 一.目标 更快的构建速度 更小的Docker镜像大小 更少的D ...
- Docker解析及轻量级PaaS平台演练(三)--Dockerfile编写
在本篇中将介绍Dockerfile的编写 除了通过修改Image,创建Container,在打包成Image来创建我们需要的Image之外 我们还可以编写Dockerfile文件,通过build来创建 ...
- Dockerfile编写
Dockerfile 是一个文本文件,其内包含了一条条的指令,每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建编写命令: 1.FROM作用:声明使用哪个基础镜像格式:FROM IMA ...
- Docker的简介以及Dockerfile编写与使用
Docker的简介 Docker是在容器的基础上,进行了进一步的封装,极大的简化了容器的创建和维护.使得Docker技术比虚拟机技术更为轻便.快捷. 下面是两张对比图. 可以看到传统虚拟机技术是虚拟出 ...
- Prometheus之Dockerfile编写、镜像构建、容器启动
目录 从官方镜像启动:prom/prometheus 官方Dockerfile分析 编写自己的Dockerfile 构建镜像: 启动容器: 从官方镜像启动:prom/prometheus 拉取镜像 $ ...
- Dockerfile编写语法
docker镜像本质上就是一个个基础镜像的堆叠,为了做出我们想要的镜像,我们需要考虑最终镜像所需的所有基础环境,然后一层层堆叠.也就是不断以基础镜像搭建上层镜像. 先看例子: # Version: # ...
随机推荐
- MongDB4.1-入门学习之下载安装配置
下载安装配置三步走 下载,MongoDB官网下载中心 下载_.msi_安装版 安装,注意以下几个步骤: Choose Setup Type: Please Choose Custom Custom S ...
- HashMap数据结构
2.1 HashMap 2.1.1 HashMap介绍 先看看HashMap类头部的源码: public class HashMap<K,V> extends AbstractMap< ...
- springboot导入excel到mysql
@Controller @RequestMapping(path = "/excel") public class ImportController { @Autowired pr ...
- PAT甲级——A1083 List Grades
Given a list of N student records with name, ID and grade. You are supposed to sort the records with ...
- PAT甲级——A1078 Hashing
The task of this problem is simple: insert a sequence of distinct positive integers into a hash tabl ...
- 数据库备份还原——mysqlbackup与mysqldump对比测试
1 环境描述 1.1 硬件环境 服务器类型:华为RH5885 IP: 10.148.128.100 内存: 64G 物理CPU个数:4 CPU核数:8 逻辑CPU个数:64 Int ...
- PostgreSQL的基础数据类型分析记录-转
src:http://www.codeweblog.com/postgresql%E7%9A%84%E5%9F%BA%E7%A1%80%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E ...
- <每日一题>题目17:super()继承相关的面试题
class A(object): def go(self): print("go A go!") def stop(self): print("stop A stop!& ...
- [Ynoi2012]D1T1
题目 套路的根号分治啊 我们设置一个值\(S\) 对于\(S\leq x\)的操作,我们直接暴力修改,显然这样只会修改\(\frac{n}{S}\)次,所以我们需要一个能够\(O(1)\)修改的数据结 ...
- 【机器学习】机器学习入门01 - kNN算法
0. 写在前面 近日加入了一个机器学习的学习小组,每周按照学习计划学习一个机器学习的小专题.笔者恰好近来计划深入学习Python,刚刚熟悉了其基本的语法知识(主要是与C系语言的差别),决定以此作为对P ...