如何编写最佳的Dockerfile

小hub领读:

微服务时代,服务部署与运维成了运维人员头疼的问题,Docker的出现,大大提升了运维效率,你知道要把一个SpringBoot项目部署到Docker中,该如何编写Dockerfile文件吗?看看这篇文章!前端


译者: Fundebug

原文:https://rock-it.pl/how-to-wri...node

本文地址:https://blog.fundebug.com/201...mysql

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

本文采用意译,版权归原做者全部git

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

目标:

  • 更快的构建速度
  • 更小的 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

构建镜像:sql

docker build -t wtf .

1. 编写. dockerignore 文件

构建镜像时,Docker 须要先准备context ,将全部须要的文件收集到进程中。默认的context包含 Dockerfile 目录中的全部文件,可是实际上,咱们并不须要. git 目录,node_modules 目录等内容.dockerignore 的做用和语法相似于 .gitignore,能够忽略一些不须要的文件,这样能够有效加快镜像构建时间,同时减小 Docker 镜像的大小。示例以下: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 状态。


推荐阅读:

太赞了,SpringBoot+Vue先后端分离完整入门教程!

分享一套SpringBoot开发博客系统源码,以及完整开发文档!速度保存!

Github上最值得学习的100个Java开源项目,涵盖各类技术栈!

2020年最新的常问企业面试题大全以及答案

相关文章
相关标签/搜索