用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=<name|index> 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文件路徑如下:

<script src="/js/chunk-vendors.4aa92667.js"></script>
<script src="/web/js/app.b474d9b9.js"></script>

如果publicPath設置為‘/my-app’,引用的js文件路徑如下:

<script src="/my-app/js/chunk-vendors.4aa92667.js"></script>
<script src="/my-app/web/js/app.b474d9b9.js"></script>

雖然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實現自動更新

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容