两层(Docker多阶段构建)的故事

使用SSR或Nginx的Node.js的生产就绪型Dockerfile

本系列最后一篇文章中 ,我们完成了向项目中添加单元测试以达到100%的代码覆盖率。 有了测试,下一步就是准备好我们的项目以进行部署。

为了使我们的应用程序准备好进行生产部署,我们需要准备的最后一件事是Dockerfile。

Dockerfile也是运行我们的单元测试的好地方,这就是为什么我决定首先编写测试的原因。

我们的构建有一些目标:

  1. 它应该是安全的
  2. 它应该尽可能苗条
  3. 如果不符合质量标准,则不应建立

牢记目标,让我们开始吧。

Docker本质上是一个运行代码的隔离环境。就像配置服务器一样,您也配置了Docker容器。 正如在使用Docker开发Node.js的更好方法中所讨论的那样,大多数流行的框架/语言都可以从Docker Hub获得。 似乎我们如何使用Node,我们需要一个运行node的环境。 我们将Dockerfile启动Dockerfile

但是在开始之前,让我们先谈谈运行命令docker build会发生什么。 发生的第一件事是Docker确定构建在其中运行的“上下文”。 除了.dockerignore文件中列出的文件或文件夹外,它从当前目录中吸收所有内容作为上下文。

我们只需要构建过程所需的最低限度,因此让我们首先创建一个.dockerignore文件,然后忽略其他所有内容。

. cache
 coverage
dist
 node_modules

成功完成构建/测试不需要的项目中最终出现的任何其他重文件夹也应忽略。

这是运行docker build . -t ssr的区别docker build . -t ssr docker build . -t ssr带有和不.dockerignore文件:

➜  docker build . -t ssr
Sending build context to Docker daemon  166.6MB
➜  docker build . -t ssr
Sending build context to Docker daemon  1.851MB

如您所见,这是一个很大的差异。

构建层

现在让我们Dockerfile创建Dockerfile

FROM node :11 . 10.0 -alpine AS build

首先,正如我提到的,它是一个Node应用程序,因此从官方的Node映像开始是有意义的。 这是生产环境,在生产环境中,我们需要不可变,可重复的构建,因此,我使用了特定的Node版本11.10.0 。 根据您的要求,您可能希望选择Node 10的最新LTS版本。我只是选择了最新的版本。 您可以在此处找到最新标签的列表: 节点标签-Docker Hub

接下来,注意AS指令。 这表明这不是Dockerfile的最后阶段。 稍后,我们可以将现阶段的工件COPY到最终容器中。 这样做的原因是要产生具有最少伪像数量的图像。 我们可以在第一阶段运行更昂贵的命令,而其结果的膨胀将在下一层中消除,仅剩下运行应用程序的基本要素。

除了生成更小的图像之外,使用多级构建也是一种很好的安全措施,因为所有构建工具都可以使用,因此,开发工具的安全漏洞可以从最后一层中剔除。

我还决定使用节点的alpine版本。 这意味着基本操作系统是Alpine Linux,这是用于容器化的大约5MB最小Linux发行版。

接下来,因为我们使用的是alpine并且它没有很多构建工具,所以我们应该安装node-gyp工具集合。

RUN  apk add --update --no-cache \
    python \
    make \
    g++

这样,我们就拥有了运行构建和测试所需的所有工具。 如果您依赖的包不需要通过gyp编译上一步的依赖,则可以节省10秒钟左右的构建时间。 但是,它将被从最后一层中剥离出来,因此,这并不是一个巨大的节省,并且许多节点依赖性确实需要它。

我们的代码尚未放入容器中,这对于运行它很有帮助! 让我们将其复制到一个简单命名的src目录中,并将该目录设置为我们的工作目录。 该层中将来的所有命令都将在指定的工作目录中运行。

WORKDIR  /src
 COPY  ./package* ./

接下来,让我们安装Node依赖项。

RUN  npm ci

npm ci工作方式与npm i相似,但是跳过了昂贵的依赖项解析步骤,而是仅安装在package-lock.json文件中指定的确切依赖package-lock.json 。 从npm i它是用于CI环境的更快的npm i

我们在项目的其余部分之前复制软件包文件的原因是为了进行缓存优化。 现在, npm ci的结果将被缓存,直到它上面的某个层中的某些内容发生更改为止,这可能是软件包文件,而不是全部代码。

现在我们可以复制其余的src并继续。

COPY  . .

现在我们可以进行质量检查和构建。 如果未通过,则将不会成功创建新映像,并且构建将失败。 这对于作为持续部署管道的一部分运行的构建非常有用。

RUN  npm run lint
 RUN  npm run build
 RUN  npm run test

我通常旨在尽可能快地失败,并且通常在build之前进行test ,但是我们的服务器端测试依赖于构建一个应用程序来为其提供服务,因此在这种情况下,我只是将它们翻转了。

最后,对于这一层,我们现在要做的最后一件事是摆脱任何开发依赖关系,因为在此之前不再需要它们。

RUN  npm prune --production

第一层就是这样。 为了方便阅读,以下是整个第一层的内容:

FROM node: 11.10 . 0 -alpine AS build

RUN  apk add --update --no-cache \
    python \
    make \
    g++

WORKDIR  /src
 COPY  ./package* ./

RUN  npm ci

COPY  . .

RUN  npm run format
 RUN  npm run build
 RUN  npm run test

RUN  npm prune --production

现在在第二层中,我们可以选择。

我们使用Node服务器构建应用程序以进行流服务器端渲染。 至此,在Dockerfile中,我们已经构建了一个客户端应用程序。 我们不一定也需要使用服务器。 我们可能会决定只需要一个静态服务的客户端专用应用程序。 在本文的下一部分中,我想向您展示如何使用原始Node SSR服务器构建最终层,或者将应用程序打包到Nginx部署中。

最终层选项1:节点流式SSR呈现的应用程序

首先,让我们从Dockerfile的Node SSR版本开始,因为这是迄今为止该系列的重点。

在第一阶段的正下方,我们现在要添加第二个FROM语句。 这次,我们将不使用AS因为它是最后一层。 我们还将希望继续公开应用程序运行所在的端口,并像以前一样设置工作目录。

FROM node: 11.10 . 0 -alpine AS build

// ...

RUN  npm prune --production

FROM node: 11.10 . 0 -alpine

ENV PORT= 1234
EXPOSE $PORT

WORKDIR  /usr/src/service

再次注意,我们从特定版本的相同高山节点图像开始。 当我们创建一个新层时,不会自动复制上一层的内容。 这是新鲜的石板。 对于Node应用程序,我们需要将工件复制几个文件和文件夹到我们的最后一层。 接下来让我们开始:

COPY  --from=build /src/node_modules node_modules
 COPY  --from=build /src/dist dist

最后,我们可以使用node运行我们的应用程序,但是我们希望在执行此操作之前将用户设置为不是root用户。 官方Node映像为此创建了一个名为node的用户。

USER node

CMD  [ "node" , "./dist/server/index.js" ]

部署时,我们应该依靠协调器来为我们管理应用程序的重启和扩展,例如Kubernetes或Docker Swarm,因此无需使用pm2forever类的工具。

就这样!

这是最终的Dockerfile

FROM node: 11 -alpine AS build

RUN  apk add --update --no-cache \
    python \
    make \
    g++

COPY  . /src
 WORKDIR  /src

RUN  npm ci

RUN  npm run format
 RUN  npm run build
 RUN  npm run test

RUN  npm prune --production


FROM node: 11.10 . 0 -alpine

EXPOSE 1234
WORKDIR  /usr/src/service

COPY  --from=build /src/node_modules node_modules
 COPY  --from=build /src/dist dist

USER node

CMD  [ "node" , "./dist/server/index.js" ]

并构建和运行该应用程序:

➜  docker build . -t ssr
➜  docker run -p 1234:1234 ssr
{ "level" :30, "time" :1551155555272, "msg" : "Listening on port 1234..." , "pid" :1, "hostname" : "d5b0db2acfbc" , "v" :1}

如果您正在关注或阅读了其他Docker文章,则可能会注意到我尚未定义HEALTHCHECKHEALTHCHECK是在某些编排程序(例如Docker Swarm)中运行时调用的命令。 在Kubernetes中运行时,我们取而代之依靠Kubernetes的活动性和就绪性探针。

有关编写节点运行状况检查的更多信息,请查看Node.js的有效Docker运行状况检查 。 出于完整性考虑,我们的SSR Node服务器非常简单,因此在这种情况下,使用curl即可。

这是最后阶段的修改版,其中HEALTHCHECK使用curl定义的HEALTHCHECK

// ... first layer ...

FROM node: 11.10 . 0 -alpine

RUN  apk add --update --no-cache curl

EXPOSE 1234

WORKDIR  /usr/src/service

COPY  --from=build /src/node_modules node_modules
 COPY  --from=build /src/dist dist

HEALTHCHECK  --interval=5s \
            --timeout=5s \
            --retries=6 \
            CMD curl -fs http://localhost:1234/ || exit 1

USER node

CMD  [ "node" , "./dist/server/index.js" ]

最终层选项2:Nginx提供静态客户端应用程序

现在,让我们创建第二个Dockerfile,这是完成最后阶段的另一种方法。

我们将从相同的构建层开始,但是这次,我们的最后阶段将使用Nginx静态地为应用程序提供服务,而不是使用Node在服务器端进行渲染。

在此之前,我们需要在package.json的脚本部分中创建一个新条目。 添加以下脚本:

"build:nginx" : "rimraf dist && npm run generate-imported-components && npm run create-bundle:nginx" ,
"create-bundle:nginx" : "cross-env BABEL_ENV=client parcel build app/index.html -d dist/client --public-url ." ,

与SSR版本不同的是我们设置为的公共网址. 在运行构建时,因为在这种情况下我们希望它相对于index.html文件。

现在,创建./nginx/Dockerfile

FROM node: 11.10 . 0 -alpine AS build

RUN  apk add --update --no-cache \
    python \
    make \
    g++

WORKDIR  /src
 COPY  ./package* ./

RUN  npm ci

COPY  . .

RUN  npm run format
 RUN  npm run build:nginx
 RUN  npm run test

RUN  npm prune --production

FROM nginx: 1.15 . 8 -alpine

RUN  apk add --update --no-cache curl

WORKDIR  /usr/src/service

COPY  --from=build /src/dist ./dist
 COPY  --from=build /src/nginx ./nginx

HEALTHCHECK  --interval=5s \
            --timeout=5s \
            --retries=6 \
            CMD curl -fs http://localhost:1234/ || exit 1

RUN  [ "chmod" , "+x" , "./nginx/entrypoint.sh" ]

ENTRYPOINT  [ "ash" , "./nginx/entrypoint.sh" ]

这里没有太多新内容,除了使用ENTRYPOINT而不是使用命令ENTRYPOINT 。 这使您可以运行脚本而不是命令。 我们还希望确保使用ash来调用sh的高山linux版本。 上面的RUN行只是更改linux权限以使文件可执行。

稍后我们将制作的脚本将使用配置文件启动nginx,我们也需要创建该配置文件并将其存储在nginx文件夹中。

让我们从entrypoint.sh脚本开始。 我将在其中包含两个有用的片段,这些片段有助于使用注释掉的环境变量。 对于本项目,我们不需要它们,但这是一个普遍的要求,例如,当您要使用nginx作为后端的代理,或者在JS捆绑包中包含分析令牌或密钥时。

#!/bin/bash

# This script can be used when you have webpack or parcel builds that 
# insert env variables at build time, usually as build args. 
# Just set the build args to an a unique string for replacement,
# and do it post build instead. Uncomment `echo` through `done` and modify
# to match your env variables
# --- Start Insert ENV to JS bundle ---
# echo "Inserting env variables"
# for file in ./dist/**/*.js
# do
#   echo "env sub for $file"
#   sed -i "s/REPLACE_MIXPANEL_TOKEN/${MIXPANEL_TOKEN}/g" $file
# done
# --- End Insert ENV to JS bundle ---
# And if you need env variables in Nginx, use this instead of `cp`
# --- Start Insert ENV to Nginx---

# echo "Injecting Nginx ENV Vars..."
# envsubst '${GRAPHQL_URL}' < nginx/nginx.conf.template > /etc/nginx/nginx.conf
# --- End Insert ENV to Nginx---
cp nginx/nginx.conf.template /etc/nginx/nginx.conf

echo "Using config:"
cat /etc/nginx/nginx.conf

echo "Starting nginx..."
nginx -c '/etc/nginx/nginx.conf' -g 'daemon off;'

基本上,我们要做的就是将我们的Nginx配置复制到/etc/nginx文件夹,然后启动它。

这是nginx配置-将其另存为./nginx/nginx.config.template 。 如果您取消注释上面的envsubst行,则可以在其中使用环境变量。

events {
  worker_connections 1024 ;
}

http {
  server {
    include /etc/nginx/mime.types;
    listen 1234 ;
    root   /usr/src/service/dist/client;
    index  index.html;
    gzip on ;
    gzip_min_length 1000 ;
    gzip_buffers 4 32k ;
    gzip_proxied any;
    gzip_types text/plain application/javascript application/x-javascript text/javascript text/xml text/css;
    gzip_vary on ;
    location ~* \.(?:css|js|eot|woff|woff2|ttf|svg|otf) {
      # Enable GZip for static files
      gzip_static on ;
      # Indefinite caching for static files
      expires max;
      add_header Cache-Control "public" ;
    }
  }
}

让我们运行吧!

➜  docker run -p 1234:1234 nginx-server              

Starting nginx...

结论

在本文中,我基于前一个样板,以添加可以用于生产的两个不同的Dockerfile。 根据您的用例,在某些情况下一个可能比另一个有用。

这就是全部!

如果您还不确定要查看该系列中的其他文章,请参阅! 这是第5部分。

所有这些文章都在构建此样板:

patrickleet /流式SSR反应式组件

因此,如果您发现它有用,请务必给它加星号!

直到下一次!

帕特里克·李·斯科特

图片来源:Adobe Stock Photos许可

From: https://hackernoon.com/a-tale-of-two-docker-multi-stage-build-layers-85348a409c84

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值