让镜像保持小巧
容器只运行单个应用。 从技术角度讲,你可以在Docker容器中运行多个进程。你可以将数据库,前端,后端,ssh,supervisor都运行在同一个Docker容器中。但是,这会让你非常痛苦:
非常长的构建时间(修改前端之后,整个后端也需要重新构建)
非常大的镜像大小
多个应用的日志难以处理(不能直接使用stdout,否则多个应用的日志会混合到一起)
横向扩展时非常浪费资源(不同的应用需要运行的容器数并不相同)
僵尸进程问题 - 你需要选择合适的init进程
因此,我建议大家为每个应用构建单独的Docker镜像基于一个合适的基础镜像。比如,如果你需要JDK,考虑让你的镜像基于官方的openjdk镜像,而不要从一个普通的ubuntu镜像开始再在Dockerfile中安装openjdk。
使用多级构建。比如,你可以用maven镜像构建你的Java应用,然后重置(reset)到tomcat镜像并把构建好的Java组件拷贝到正确的位置来发布你的应用,所有这些操作都可以在一个Dockerfile中。这意味着你最后的镜像不会包含构建过程中被拉取的库和依赖,只包含最终的组件和运行他们所需的环境。如果你要使用不包含多级构建功能的docker版本,尝试减少镜像中层的数量,通过最小化Dockerfile中独立的RUN命令的数量。你可以把多条命令合并到一个RUN行并用shell技巧来把他们组合成一句。考虑下面的两种情况,前者在镜像中创建了两层,而后者只创建了一层。
RUN apt-get -y update
RUN apt-get install -y python
RUN apt-get -y update && apt-get install -y python
把应用数据存到哪里,怎样存储
不要通过存储引擎把应用数据存到容器的可写层中。这会增加容器的大小而且从I/O角度看比数据卷或挂载点效率更低。
取而代之的,使用宿主机数据卷存储数据(文件IO/内存IO/数据库IO)。
尽可能使用集群服务
Kubernetes和Docker Swarm这样的平台是容器管理和容器编排引擎,使用户能够指导容器部署并自动执行更新,运行状况监视和故障转移过程。
如果可能,通过集群服务(swarm services)把你的应用设计成可扩展的。即使你只需运行一个应用实例,集群服务也能提供很多独立容器没有的优点。
使用持续集成/持续部署(CI/CD)进行开发和测试
当你在代码控制中进行了修改或创建了pull request时,用Docker Cloud或其他CI/CD流程自动构建docker镜像并打标签,然后测试它。Docker Cloud也能把测试过的应用直接发布到生产环境。
DOCKERFILE最佳实践
参考
以下知识点非常重要:
dockerfile中的每个指令都会创建一个新的镜像层。
镜像层将被缓存和复用
当Dockerfile的指令修改了,复制的文件变化了,或者构建镜像时指定的变量不同了,对应的镜像层缓存就会失效
某一层的镜像缓存失效之后,它之后的镜像层缓存都会失效
镜像层是不可变的,如果我们再某一层中添加一个文件,然后在下一层中删除它,则镜像中依然会包含该文件(只是这个文件在Docker容器中不可见了)
1、编写.dockerignore文件
构建镜像时,Docker需要先准备context ,将所有需要的文件收集到进程中。默认的context包含Dockerfile目录中的所有文件,但是实际上,我们并不需要.git目录,node_modules目录等内容。 .dockerignore 的作用和语法类似于 .gitignore,可以忽略一些不需要的文件,这样可以有效加快镜像构建时间,同时减少Docker镜像的大小。示例如下:
.git/
node_modules/
2、缓存的使用与强制更新
Dockerfile的每条指令都会将结果提交为新的镜像,下一个指令将会基于上一步指令的镜像的基础上构建,如果一个镜像存在相同的父镜像和指令(除了ADD),Docker将会使用镜像而不是执行该指令,即缓存。
为了有效地利用缓存,你需要保持你的Dockerfile一致,并且尽量在末尾修改。
FROM ubuntu
MAINTAINER Michael Crosby
RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
RUN apt-get update && apt-get upgrade -y
更改MAINTAINER指令会使Docker强制执行RUN指令来更新apt,而不是使用缓存。
所以,我们应该使用常用且不变的Dockerfile开始(译者注:上面的例子)指令来利用缓存。
3、构建中的更新指令依赖基础层级镜像
基于第二点, 开始的层级镜像应该尽量保持不变, 除了尽量利用低层级的缓存外 , 把更新指令(例如APK更新)放到基础层级的镜像, 这也是防止高层级镜像变动持续带来镜像构建非常不确定。
4、基础镜像的标签不要使用latest
当镜像没有指定标签时,将默认使用latest 标签。因此, FROM ubuntu 指令等同于FROM ubuntu:latest。当时,当镜像更新时,latest标签会指向不同的镜像,这时构建镜像有可能失败。如果你的确需要使用最新版的基础镜像,可以使用latest标签,否则的话,最好指定确定的镜像标签。
5、设置WORKDIR和CMD
WORKDIR指令可以设置默认目录,也就是运行RUN
/ CMD
/ ENTRYPOINT
指令的地方。
CMD指令可以设置容器创建是执行的默认命令。
6、CMD与ENTRYPOINT的语法注意以EXEC方式执行
CMD和ENTRYPOINT指令都非常简单,但它们都有一个隐藏的容易出错的“功能”,如果你不知道的话可能会在这里踩坑,这些指令支持两种不同的语法。
CMD /bin/echo
#or
CMD ["/bin/echo"]
这看起来好像没什么问题,但仔细一看其实两种方式差距很大。如果你使用第二个语法:CMD(或ENTRYPOINT)是一个数组,它执行的命令完全像你期望的那样。如果使用第一种语法,Docker会在你的命令前面加上/bin/sh -c, 如果你不知道Docker修改了CMD命令,在命令前加上/bin/sh -c可能会导致一些意想不到的问题以及难以理解的功能。因此,在使用这两个指令时你应当使用数组语法,因为数组语法会确切地执行你打算执行的命令。
使用CMD和ENTRYPOINT时,请务必使用数组语法。
7、COPY与ADD优先使用前者[COPY]
COPY指令非常简单,仅用于将文件拷贝到镜像中。ADD相对来讲复杂一些,可以用于下载远程文件以及解压压缩包(参考官方文档)。
8、设置默认的环境变量,映射端口和数据
运行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"]
9、添加HEALTHCHECK
运行容器时,可以指定--restart always
选项。这样的话,容器崩溃时,Docker守护进程(docker daemon)会重启容器。对于需要长时间运行的容器,这个选项非常有用。但是,如果容器的确在运行,但是不可(陷入死循环,配置错误)用怎么办?使用HEALTHCHECK指令可以让Docker周期性的检查容器的健康状况。我们只需要指定一个命令,如果一切正常的话返回0,否则返回1。对HEALTHCHECK感兴趣的话,可以参考这篇博客。