Nodejs Docker 镜像体积优化实践

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

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

简单的一段Node.js项目

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

// 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并写入如下代码web

# Dockerfile
FROM node

COPY . /home/app 
RUN cd /home/app && npm install 
WORKDIR /home/app 
CMD ['npm', 'start'] 复制代码

执行面试

  • docker build -t myapp .
  • docker images

结果

能够看到这段最简单的nodejs程序有920MB,请不要这样作。接下来咱们将逐步的减小这个镜像的体积。docker

优化docker生产环境镜像

  • 使用Node.js Alpine 镜像

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

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

    build以后 npm

    结果

    能够看到整整减小了800MB,这是一个很是大的优化。json

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

    但咱们还能继续优化。咱们正在安装全部依赖项,即便咱们最终只须要生成环境下的依赖包。若是只打包生产环境的以来不会怎么样,继续改进一下。缓存

    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每日一道面试题详解