用Docker简化Nodejs开发4-全栈项目模板(vue+nginx+node+mongodb)

本文分析了用docker搭建一个Web全栈项目(vue+nginx+node+mongodb)运行环境时碰到的问题,以一个开箱即用的项目为例,整理了制作应用docker镜像的基础模板和一些使用技巧

现在越来越多的项目采用vue+nginx+node+mongodb的组合,这样一个JS全栈工程师就可以独立搞定一个完整的应用。要达到这个目标,只会敲代码是不够的,还要能搞定运行环境。通过使用docker可以极大简化运行环境的搭建,并且给开发和运维的衔接工作带来便利。

典型的全栈项目包括3个部分:前端(vue+nginx),后端(node)和数据库(mongodb)。从部署的角度看,每个部分又可以分为3个部分:应用(代码+配置)、中间件和主机。docker解决了中间件和主机的组合问题,我们用到的各种中间件都有标准docker镜像,通过docker,它们的安装和运行都实现了标准化。我们真正要干的活是,如何以中间件镜像为基础,加上应用代码和环境配置,制作项目的应用镜像。有了应用镜像,后面的工作就可以由运维接手了。

因此,对于全栈工程师来说,需要掌握一项新技能:制作应用镜像

下面,通过一个实际项目,描述一种制作应用镜像的通用方法。


项目概况

https://github.com/jasony62/tms-finder

tms-finder项目是一个在线文档管理系统,back目录下是用node实现的后端服务,ue目录下是用Vue实现的用户端应用,build后部署到nginx。上传文件时用户可以输入文件的描述信息(可配置),文件会存在放在服务端指定的本地硬盘上(可配置),描述信息会保存在指定的mongodb中(可配置)。

这个项目是开箱即用的,在安装好dockerdocker-compose的机器上,从github拉取代码,执行docker-compose up -d命令就可以把整个应用运行起来。

这个项目是环境友好的,制作的默认镜像可以灵活部署在不同的环境中(通过设置环境变量),也可以根据环境的要求制作新的镜像(通过设置构建参数)。

这个项目是编码友好的,程序员可以有选择地使用docker,前后端都可以在容器外运行,方便调试代码。


关键概念

node环境变量

后台服务是node应用,Vue本质上也是node应用,所以应该知道node的使用环境变量的方式。node中通过process.env这个对象访问环境变量,该对象可以修改,但是只会在应用内有效。

进入容器后,可以通过下面的命令查看可用的环境变量:

node -e "console.log(process.env)"

需要注意的是,使用vue-service-cli命令时,Vue对process.env做了处理,并不直接传递所有环境变量,而是要通过.env传递,且必须以VUE_APP_开头。这会对制作镜像产生影响。

docker环境变量

docker-compose中和环境变量设置相关的指令主要是enviromentenv_file,另外,多配置文件也会影响影响环境变量设置,详细信息请看在线文档。tms-finder项目中需要知道的是,在compose配置文件中设置的变量会传递给容器(process.env),docker-compose.override.yml中的内容会覆盖docker-compose.yml中的内容。(后面会用到)

我采用在docker-compose.yml定义环境变量的默认值(在版本库),如果需要修改就通过docker-compose.override.yml覆盖(不在版本库)。

tms-finder中包含了这两个文件,docker-compose.yml用于指定基础设置,docker-compose.override.yml用于指定和运行环境相关的设置,例如:端口号等。如果有更多的配置要求,可以通过docker-compose -f解决。

注意:端口(ports)不能通过覆盖,环境变量(environment)和文件卷(volumes)可以,所以端口没有写在docker-compose.yml文件中,这样有利于复用。另外,只能是“覆盖”并不能“清除”,例如:volumns只能从一个设置改成另一个,而不能清除掉。

参考:https://docs.docker.com/compose/environment-variables/

参考:https://docs.docker.com/compose/extends/

强调一个概念,环境变量是作用于容器的,在构造阶段是无效的。

docker参数(ARG)

Dockerfile有个ARG指令,用来定义在构造镜像时,从外部接收的参数。在docker-compose中和ARG对应的是build/args

通过ARG在镜像构造阶段传递参数。

docker网络

docker网络涉及很多内容,现在只需要记住一点,docker-compose.yml中定义的服务会被自动添加到一个默认docker的网络(tms-finder_default)中,服务之间可以将服务名用作主机名相互访问。

参考:https://docs.docker.com/compose/networking/


数据库(mongodb)

mongodb的标准镜像是以ubuntu为基础,需要调整时区,编写mongodb/Dockerfile文件。

RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

分别在docker-compose.ymldocker-compose.override.yml中添加服务。

  mongodb:
    build: ./mongodb
    image: tms-finder/mongo:latest
    container_name: tms-finder-mongo
  mongodb:
    volumes:
      - ./mongodb/data:/data/db
    ports:
      - '27017:27017'
    # logging:
    #   driver: 'none'

存储数据文件

这里需要注意volumes部分。通常,数据库中间件应该将数据目录挂载在主机的路径上,这样每次重启容器数据还在(docker-compose.override.yml中的设置)。但是,有时候可能并不需要持久化数据(在windows环境下不能挂载宿主机的目录),例如:测试,这时可以把数据放在容器内部,重启容器就可以清理数据了。

启动服务

编码阶段如果为了方便调试,可以单独启动mongodb服务,命令如下:

docker-compose up mongodb


后端(node)

编写back/Dockerfile文件,将后端代码放在标准node镜像中形成新镜像。

FROM node:alpine

# 设置时区
RUN sed -i 's?http://dl-cdn.alpinelinux.org/?https://mirrors.aliyun.com/?' /etc/apk/repositories && \
  apk add -U tzdata && \
  cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
  apk del tzdata

RUN npm install cnpm -g

WORKDIR /home/node/app

COPY . .

RUN cnpm i

CMD [ "node", "app" ]

选择node的官方标准镜像node:alpine。用npmyarn安装依赖包都会失败,所以在镜像中安装并使用cnpm。这里提个醒,应该避免从操作系统的镜像开始制作自己的镜像,优先从hub.docker.com上找中间件官方镜像,并且按照在线文档进行相应的设置,这样省很多事。

docker-compose.yml中添加服务。

  back:
    build: ./back
    image: tms-finder/back:latest
    container_name: tms-finder-back
    # ports:
    #   - '3000:3000'
    environment:
      - NODE_ENV=production
      - TMS_FINDER_MONGODB_HOST=mongodb
      - TMS_FINDER_MONGODB_PORT=27017
      - TMS_FINDER_FS_ROOTDIR=/home/storage
    volumes:
      - ./back/storage/upload:/home/storage/upload # 指定上传文件的外部存储位置
    command: ['./wait-for.sh', 'mongodb:27017', '-t', '300', '--', 'node', 'app']

docker-compose.override.yml中添加服务。

  back:
    ports:
      - '3000:3000'

设置环境变量

后端服务中使用了多个配置文件,包括:连接mongodbconfig/mongodb.js,设置文件上传服务的config/fs.js等。因为镜像只是一个“模板”,实际部署时运行环境不是确定的,例如:可能在生产环境中提供了独立的mongodb服务,需要通过配置将应用指向这个服务。我们通过设置环境变量解决这个问题。

下面以连接mongodb服务的配置文件config/mongodb.js为例:

module.exports = {
  master: {
    host: process.env.TMS_FINDER_MONGODB_HOST,
    port: parseInt(process.env.TMS_FINDER_MONGODB_PORT)
  }
}

docker-compose.yml文件中enviroment指令部分定义了环境变量TMS_FINDER_MONGODB_HOSTTMS_FINDER_MONGODB_PORT的值,容器启动后,node中可以通过process.env访问这些环境变量。注意这里的TMS_FINDER_MONGODB_HOST=mongodb,其中的mongodb是服务名,前面提到可以将服务名作为主机名进行访问。

服务启动顺序

通过docker-compose.yml同时启动多个服务时存在启动顺序的问题:后端服务back要连接mongodb,但是,如果mongodb启动需要很长时间(例如:数据文件放在宿主机上),back服务就有可能因连接超时启动失败。为了解决这个问题,我找到了一个脚本wait_for。通过这个脚本可以测试指定的端口是否已经可用,如果在指定时间内确定可用,就执行后面的命令。

参考:https://docs.docker.com/v17.12/compose/startup-order/

启动指定服务

如果为了调试前端代码,只需要同时启动mongodbback,可以执行如下命令:

docker-compose up mongodb back

容器外运行

不用容器启动时node服务时,用npm run pm2启动。pm2支持设置环境变量,在ecosystem.config.js文件中进行设置。

      env: {
        NODE_ENV: 'development',
        TMS_FINDER_MONGODB_HOST: 'localhost',
        TMS_FINDER_MONGODB_PORT: 27017
      }

容器外运行主要是为了方便调试代码,通过容器启动的mongodb就在本机,所以主机地址设置为localhost

参考:https://pm2.keymetrics.io/docs/usage/environment/


前端镜像(vue+nginx)

Vue项目要部署的内容是通过yarn build命令生成的静态代码,这些代码可以部署到任何WebServer中,例如:nginx。

编写ue/Dockerfile文件。

# 标准基础镜像(构建阶段)
FROM node:alpine

RUN npm install cnpm -g

WORKDIR /home/node/app

COPY . .

# 生成.env文件
ARG vue_app_base_url
ARG vue_app_auth_server
ARG vue_app_login_key_username=username
ARG vue_app_login_key_password=password
ARG vue_app_login_key_pin=pin
ARG vue_app_api_server

RUN echo VUE_APP_BASE_URL=$vue_app_base_url > .env && \
  echo VUE_APP_AUTH_SERVER=$vue_app_auth_server >> .env && \
  echo VUE_APP_LOGIN_KEY_USERNAME=$vue_app_login_key_username >> .env && \
  echo VUE_APP_LOGIN_KEY_PASSWORD=$vue_app_login_key_password  >> .env && \
  echo VUE_APP_LOGIN_KEY_PIN=$vue_app_login_key_pin  >> .env && \
  echo VUE_APP_API_SERVER=$vue_app_api_server >> .env

# 安装依赖包,构建代码
RUN cnpm i && yarn build

# 标准基础镜像(部署阶段)
FROM nginx:alpine

# 设置时区
RUN sed -i 's?http://dl-cdn.alpinelinux.org/?https://mirrors.aliyun.com/?' /etc/apk/repositories && \
  apk add -U tzdata && \
  cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
  apk del tzdata

# 修改配置文件
ADD ./nginx.conf.template /etc/nginx/nginx.conf.template

ADD ./start_nginx.sh /usr/local/bin/start_nginx.sh

RUN chmod +x /usr/local/bin/start_nginx.sh

# 将构建阶段代码放在指定位置
COPY --from=0 /home/node/app/dist /usr/share/nginx/html

CMD ["start_nginx.sh"]

docker-compose.yml中添加服务。

  ue:
    build:
      context: ./ue
      args:
        vue_app_base_url: /finder_ue
        vue_app_auth_server: http://localhost:3000
        vue_app_login_key_username: username
        vue_app_login_key_password: password
        vue_app_login_key_pin: pin
        vue_app_api_server: http://localhost:3000
    image: tms-finder/ue:latest
    container_name: tms-finder-ue
    environment:
      - NGINX_WEB_BASE_URL=/finder_ue
      - NGINX_ACCESS_CONTROL_ALLOW_ORIGIN=*
    # ports:
    #   - '8080:80'

docker-compose.override.yml中添加服务。

  ue:
    ports:
      - '8080:80'

多阶段构建

FROM can appear multiple times within a single Dockerfile to create multiple images or use one build stage as a dependency for another. Simply make a note of the last image ID output by the commit before each new FROM instruction. Each FROM instruction clears any state created by previous instructions.

Optionally a name can be given to a new build stage by adding AS name to the FROM instruction. The name can be used in subsequent FROM and COPY --from= instructions to refer to the image built in this stage.

上面两段来自docker的官方文档,简单说就是在一个Dockerfile中,可以有多个FROM,每个构成一个阶段,后面的阶段可使用前面阶段生成的内容,最终生成的镜像只包含最后一个FROM的内容。

这个特性恰好满足了制作Vue前端代码镜像的需求,因为build要依赖node环境,但是最终运行环境只需要nginx和代码。所以ue/Dockerfile分为了构建和部署两个阶段,构建阶段生成代码,然后在部署阶段放到nginx的指定目录。

关于路由(VueRouter)

通常,复杂一些的Vue项目中都会用到Router,如果采用html5的history模式,需要在nginx.conf文件中进行设置。

参考:https://router.vuejs.org/zh/guide/essentials/history-mode.html

ue目录下编制nginx.conf.template文件。

location $NGINX_WEB_BASE_URL/web {
  root   /usr/share/nginx/html;
  try_files $uri $uri/index.html $NGINX_WEB_BASE_URL/web/index.html;
}

try_filesnginx的指令,功能是查找指定的文件,找到了就返回,如果找不到就转发最后1个url。在配置路由的Vue项目中,如果url找不到对应的文件就返回index.html,由前端代码解决路由问题。

$NGINX_WEB_BASE_URL是环境变量,下面讲。

关于基础路径(publicPath和BASE_URI)

Vue项目的vue.config.js文件中有个publicPath设置,默认值是'/',其作用如下:

默认情况下,Vue CLI 会假设你的应用是被部署在一个域名的根路径上,例如 https://www.my-app.com/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.my-app.com/my-app/,则设置publicPath为 /my-app/。

采用默认值,build生成的index.html文件中引用生成的js文件路径如下:



如果publicPath设置为‘/my-app’,引用的js文件路径如下:



虽然nginx可以通过rewriteproxy_pass改变url指向的内容,但是,如果build时没有指定publicPath,而是通过nginxhttps://www.my-app.com/my-app/指向index.html,那么即使浏览器可以正确获得html文件,可是它引用的js文件地址为https://www.my-app.com/js/chunk-vendors.4aa92667.js,还是无法找到这个文件。

如果编码阶段就确定publicPath,直接修改相关配置就好了。但是,我们不希望代码与环境硬绑定,基础路径和代码自身的业务逻辑无关,是一个部署需求,因此通过环境变量进行设置。

这时又会用到Vue的另外一个参数outputDir,默认值为dist,其作用如下:

当运行 vue-cli-service build 时生成的生产环境构建文件的目录。

ue下新建vue.config.js文件中进行这两个参数的设置。

const VUE_APP_BASE_URL = process.env.VUE_APP_BASE_URL ? process.env.VUE_APP_BASE_URL : ''

module.exports = {
  publicPath: `${VUE_APP_BASE_URL}/web`,
  outputDir: `dist${VUE_APP_BASE_URL}/web`
}

注:为什么以web为结尾和这篇文章的主题无关,可忽略。

调用后台API

前端代码中需要确定后端服务API的地址,而且在构建阶段就要确定。按照代码不应该与环境硬绑定的原则,也需要通过环境变量进行指定,下面以2个文件为例。

程序文件ue/src/apis/auth.js

const baseAuth = (process.env.VUE_APP_AUTH_SERVER || '') + '/auth'

程序文件ue/src/apis/file/browse.js

const baseApi = (process.env.VUE_APP_API_SERVER || '') + '/file/browse'

生成.env文件

本以为环境变量可以直接传递到vuebuild过程中,但是因为vue对process.env做了处理,环境变量只能通过.env文件传递。所以,我在Dockerfile中直接通过传递的ARG生成.env文件。

生成nginx.conf文件

生成nginx.conf文件复杂一些,需要用到envsubst命令,这个命令在nginx:alpine中已经包含,它的作用是替换文件中的环境变量并生成新文件。编写start_nginx.sh这个shell脚本,实现替换nginx.conf.template中的环境变量,生成新的nginx.conf文件,并启动nginx。

注意:.env文件不能用这种模板文件的方式生成,因为环境变量只作用于容器内,多阶段构建中,前面的阶段并不会创建容器,所以环境变量用不上。

容器外运行

Vue在编码阶段并不需要docker,通过yarn serve命令就可以运行。

Vue设置环境变量的官方方法是用.env文件。

参考:https://cli.vuejs.org/zh/guide/mode-and-env.html

ue目录下编写.env文件(包含在版本中)

VUE_APP_BASE_URL=/finder_ue
VUE_APP_AUTH_SERVER=http://localhost:3000
VUE_APP_LOGIN_KEY_USERNAME=username
VUE_APP_LOGIN_KEY_PASSWORD=password
VUE_APP_LOGIN_KEY_PIN=pin
VUE_APP_API_SERVER=http://localhost:3000

如果需要修改定义的值,可以在ue目录编写.env.local(不包含在版本中)进行覆盖,例如:

VUE_APP_BASE_URL=

需要注意的是不要把.env或者.env.local不要放入docker容器,应该用.dockerignore文件忽略掉。

总结和其它

docker极大降低了运行环境搭建的门槛,只要掌握基本用法,程序员完成可以搞定一个应用的基本运行环境。

为了让应用更容易部署,应该尽量减少代码对运行环境的硬依赖,将这些依赖转化为可在部署时指定的环境变量。

本项目总结了不少使用docker的实用方法,这些方法可以用到同类型的项目中,这样可以更有效地搭建全栈项目。

本系列其他文章

用Docker简化Nodejs开发1——开发环境

用Docker简化Nodejs开发2——开发环境到测试环境

用Docker简化Nodejs开发3——用webhook实现自动更新

你可能感兴趣的:(用Docker简化Nodejs开发4-全栈项目模板(vue+nginx+node+mongodb))