Nodejs-docker-镜像体积优化实践

8次阅读

共计 2666 个字符,预计需要花费 7 分钟才能阅读完成。

你讨厌部署你的应用程序花费很长时间吗? 对于单个容器来说,超过 gb 并不是最佳实践。每次部署新版本时都要处理数十亿字节,这对我们来说并不太合适。

本文将通过 Nodejs 程序展示如何优化 Docker 镜像的几个简单步骤,使它们更小、更快、更适合生产环境。

简单的一段 Node.js 项目

首先写一段基于 express 的简单 web 服务器程序

// package.json
{
  "name": "docker-test",
  "version": "1.0.0",
  "description": "","main":"app.js","scripts": {"start":"node app"},"author":"",
  "license": "ISC",
  "dependencies": {"express": "^4.16.4"},
  "devDependencies": {"eslint": "^5.16.0"}
}
// app.js
const express = require('express')
const app = express()

app.get('/', function(req, res){res.send('hello world')
})

app.listen(3000)

在根目录下新建 Dockerfile 并写入以下代码

# Dockerfile
FROM node

COPY . /home/app

RUN cd /home/app && npm install

WORKDIR /home/app

CMD ['npm', 'start']

执行

  • docker build -t myapp .
  • docker images

![结果](https://i.loli.net/2019/04/10…
)

可以看到这段最简单的 nodejs 程序有 920MB, 请不要这样做。接下来我们将逐步的减少这个镜像的体积。

优化 docker 生产环境镜像

  • 使用 Node.js Alpine 镜像

    大幅减小镜像体积的最简单和最快的方法是选择一个小得多的基本镜像。Alpine 是一个很小的 Linux 发行版,可以完成这项工作。只要选择 Node.js 的 Alpine 版本,就会有很大的改进。

    FROM node:alpine
    
    COPY . /home/app
    
    RUN cd /home/app && npm install
    
    WORKDIR /home/app
    
    CMD ['npm', 'start']

    build 之后

    可以看到整整减少了 800MB,这是一个非常大的优化。

  • 生成环境下不打包开发的依赖包

    但我们还能继续优化。我们正在安装所有依赖项,即使我们最终只需要生成环境下的依赖包。如果只打包生产环境的以来不会怎么样,继续改进一下。

      FROM node:alpine
    
      COPY . /home/app
    
      RUN cd /home/app && npm install --production
    
      WORKDIR /home/app
    
      CMD ['npm', 'start']

    build 之后

    我们又减少了 6MB,因为我们目前只有一个开发依赖,可以想象在一个正常的项目中这也将是非常大的优化。

  • 使用基础版本的 Alpine 镜像组合 Nodejs

    如果我们使用基础版本的 Alpine 镜像,然后自己安装 Nodejs 结果会怎么样呢?

      FROM alpine:latest
    
      RUN apk add --no-cache --update nodejs nodejs-npm
    
      COPY . /home/app
    
      RUN cd /home/app && npm install --production
    
      WORKDIR /home/app
    
      CMD ['npm', 'start']

    build 之后

    现在只剩下了 65MB,相比刚开始已经减少了 10 倍多。

  • 多阶段构建

    • Docker 镜像是分层的,Dockerfile 中的每个指令都会创建一个新的镜像层,镜像层可以被复用和缓存。当 Dockerfile 的指令修改了,复制的文件变化了,或者构建镜像时指定的变量不同了,对应的镜像层缓存就会失效,某一层的镜像缓存失效之后,它之后的镜像层缓存都会失效。
    • 因此我们还可以将 RUN 指令合并,但是需要记住的是,我们只能将变化频率一致的指令合并。
    • 我们应该把变化最少的部分放在 Dockerfile 的前面,这样可以充分利用镜像缓存。
    • 通过最小化镜像层的数量,我们可以得到更小的镜像。

上述示例中,源代码会经常变化,则每次构建镜像时都需要重新安装 NPM 模块,这显然不是我们希望看到的。因此我们可以先拷贝 package.json,然后安装 NPM 模块,最后才拷贝其余的源代码。这样的话,即使源代码变化,也不需要重新安装 NPM 模块。

  FROM alpine AS builder
  WORKDIR /home/app
  RUN apk add --no-cache --update nodejs nodejs-npm
  COPY package.json package-lock.json ./
  RUN npm install --production

  FROM alpine
  WORKDIR /home/app
  RUN apk add --no-cache --update nodejs nodejs-npm
  COPY --from=builder /usr/src/app/node_modules ./node_modules
  COPY . .
  CMD ['npm', 'start']

最终的镜像只有 51MB,比最开始大概减少了 17 倍!并且后续的 build 速度也大大提升。

每一条 FROM 指令都是一个构建阶段,多条 FROM 就是多阶段构建,虽然最后生成的镜像只能是最后一个阶段的结果,但是,能够将前置阶段中的文件拷贝到后边的阶段中,这就是多阶段构建的最大意义。

在上面的 Dockerfile 文件中,我们先 copy 了 package.json,然后 npm install,在第二阶段构建时,我们直接 copy 了第一阶段已经下载好的 node_moduls, 在下一次 build 时,如果没有新增依赖,docker 将使用缓存中的 node_modules,这样就减少了部署的时间。

使用 docker inspect imageId 命令 我们可以看到,虽然我们有多个指令,但是最终的镜像也只有 5 层,这就是层的共享机制。

使用多阶段构建可以充分利用 Docker 镜像的缓存,大大减少最终部署到生产环境的时间。

结论

在实际生产环境中,没有任何理由使用 gb 大小的镜像,如果你确实需要提高部署速度,并且被缓慢的 CI/CD 所困扰,那么多阶段构建将会是一个非常有帮助的方法

希望这篇简短的文章对考虑使用 Docker 进行基于 Node.js 的应用程序开发或部署的人有些许帮助。

查看原文

关注 github 每日一道面试题详解

正文完
 0