寫給前端工程師看的Docker教程-實戰篇

深紅發表於2019-09-30

相關文章:

在前幾篇的文章裡,我們學習了Docker常用的命令和基本操作,現在可以開始實戰了。

單頁應用

前端工作中最常見的就是單頁應用了。我們首先用create-react-app快速建立一個應用

npm i create-react-app -g
create-react-app react-app
cd react-app
npm run start
複製程式碼

可以看見正常啟動的頁面。

打包試一下

npm run build
複製程式碼

可以看到本地生成了一個build目錄,這就是最後線上執行的程式碼。

我們先在本地執行下build目錄看看

npm i http-server -g
http-server -p 4444 ./build
複製程式碼

訪問 http://localhost:4444 即可看到打包後的頁面

單頁應用Docker化

react-app目錄下新建Dockerfile .dockerignorenginx.conf

.dockerignore

node_modules
build
複製程式碼

dockerignore指定了哪些檔案不需要被拷貝進映象裡,類似.gitignore

我們知道單頁應用的路由一般都被js託管,所以對於nginx需要特別配置

nginx.conf

server {
    listen       80;
    server_name  localhost;

    location / {
        root   /app/build; # 打包的路徑
        index  index.html index.htm;
        try_files $uri $uri/ /index.html; # 防止重重新整理返回404
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}
複製程式碼

Dockerfile

# 基於node11
FROM node:11

# 設定環境變數
ENV PROJECT_ENV production
ENV NODE_ENV production

# 安裝nginx
RUN apt-get update && apt-get install -y nginx

# 把 package.json package-lock.json 複製到/app目錄下
# 為了npm install可以快取
COPY package*.json /app/

# 切換到app目錄
WORKDIR /app

# 安裝依賴
RUN npm install --registry=https://registry.npm.taobao.org

# 把所有原始碼拷貝到/app
COPY . /app

# 打包構建
RUN npm run build

# 拷貝配置檔案到nginx
COPY nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80

# 啟動nginx,關閉守護式執行,否則容器啟動後會立刻關閉
CMD ["nginx", "-g", "daemon off;"]
複製程式碼

需要特別注意的是:

COPY package*.json /app/
RUN npm install
COPY . /app
複製程式碼

我們單獨把package.json檔案先拷貝到app,安裝完依賴,然後才把所有的檔案拷貝到app,這是為什麼?

這是為了充分利用docker快取

COPY . /app
RUN npm install
複製程式碼

如果這麼寫,那麼每一次重新構建映象,都需要下載一次npm包,這是非常浪費時間的!而把package.json與原始檔分隔開寫入映象,這樣只有當package.json發生改變了,才會重新下載npm包。

當然快取有時候也會造成一些麻煩,比如在進行一些shell操作輸出內容時,由於快取的存在,導致新構建的映象裡的內容還是舊版本的。

我們可以指定構建映象時不使用快取

docker build --no-cache -t deepred5/react-app .
複製程式碼

最佳實踐是在檔案頂部指定一個環境變數,如果希望不用快取,則更新這個環境變數即可,因為快取失效是從第一條發生變化的指令開始。

打包映象

docker build -t deepred5/react-app .  
複製程式碼

啟動容器

docker run -d --name my-react-app  -p 8888:80 deepred5/react-app
複製程式碼

訪問 http://localhost:8888 即可看到頁面

訪問 http://localhost:8888/deepred5, 也可以看見頁面,說明nginx防重新整理配置生效了!

多層構建

我們之前寫的Dockerfile其實是有些問題的: 映象基於node11,但是整個映象用到node環境的地方只是為了前端打包,真正啟動的是Nginx。映象裡的專案原始碼以及node_modules其實根本沒有用,這些冗餘檔案造成了映象的體積變得非常龐大。

而我們僅僅需要打包出來的靜態檔案以及啟動一個靜態伺服器Nginx即可。

這時就可以使用multi-stage多層構建。

新建一個Dockerfile.multi

# node映象僅僅是用來打包檔案
FROM node:alpine as builder

ENV PROJECT_ENV production
ENV NODE_ENV production

COPY package*.json /app/

WORKDIR /app

RUN npm install --registry=https://registry.npm.taobao.org

COPY . /app

RUN npm run build

# 選擇更小體積的基礎映象
FROM nginx:alpine

COPY nginx.conf /etc/nginx/conf.d/default.conf

COPY --from=builder /app/build /app/build
複製程式碼

這個檔案裡,我們使用了兩個FROM基礎映象,第一個node:alpine僅僅作為打包環境,真正的基礎映象是nginx:alpine

打包映象

# -f 指定使用Dockerfile.multi進行構建
docker build -t deepred5/react-app-multi .  -f Dockerfile.multi
複製程式碼

啟動容器

docker run -d --name my-react-app-multi  -p 8889:80 deepred5/react-app-multi
複製程式碼

訪問 http://localhost:8889 即可看到頁面

檢視映象大小

docker images deepred5/react-app-multi
docker images deepred5/react-app
複製程式碼

可以發現,兩者的大小相差巨大。

deepred5/react-app映象有1G多,而deepred5/react-app-multi只有20多M

主要原因是:deepred5/react-app的基礎映象node:11就有900M,而deepred5/react-app-multi的基礎映象nginx:alpine只有20M。由此可見多層構建對於減少映象大小是非常有幫助的。

Node應用

前端有時也會參與到Node BFF層的開發。我們來建立一個Node結合Redis的簡單專案

mkdir node-redis
cd node-redis
npm init -y
npm i koa koa-router ioredis
touch index.js
複製程式碼

node-redis/index.js

const Koa = require('koa');
const Router = require('koa-router');
const Redis = require("ioredis");

const app = new Koa();
const router = new Router();
const redis = new Redis({
  port: 6379,
  host: '127.0.0.1'
});

router.get('/', (ctx, next) => {
  ctx.body = 'hello world.';
});

router.get('/api/json/get', async (ctx, next) => {
  const result = await redis.get('age');
  ctx.body = result;
});

router.get('/api/json/set', async (ctx, next) => {
  const result = await redis.set('age', ctx.query.age);
  ctx.body = {
    status: result,
    age: ctx.query.age
  }
});

app
  .use(router.routes())
  .use(router.allowedMethods());

app.listen(3000, () => {
  console.log('server start at localhost:3000');
})
複製程式碼

我們首先需要本地安裝Redis,然後啟動redis

redis-server
複製程式碼

啟動Node專案

node index.js
複製程式碼

訪問 http://localhost:3000/ 即可看到頁面

訪問 http://localhost:3000/api/json/set?age=2 ,我們就向Redis裡設定age的值為2

訪問 http://localhost:3000/api/json/get ,我們就取得Redis裡age的值

Node應用Docker化

首先我們來思考下,這個後端應用涉及Node和Redis。如果我們要部署到Docker裡,應該怎麼構建映象?

  1. 方案一:基於一個最基礎的ubuntu映象,然後我們在其中安裝Node和Redis,這樣Node和Redis之間就可以進行通訊了。這種方案只需要啟動一個容器,因為Node和Redis已經在這個容器裡了。
  2. 方案二:我們基於Redis映象啟動一個容器,專門用來跑Redis。基於Node映象再啟動一個容器,專門用來跑Node。

Docker的理念更傾向於方案二。我們希望一個映象專注於做一件事,現在流行的微服務,微前端也是這種思想。

中級篇中我們說過每個容器都是相互隔離的,通過對映埠才能訪問容器裡的網路應用。但是容器和容器之間怎麼進行通訊呢?

Docker裡使用Networking進行容器間的通訊

Networking

# 建立一個app-test網路
docker network create app-test
複製程式碼

我們只需要把需要通訊的容器都加入到app-test網路裡,之後容器間就可以互相訪問了。

docker run -d --name redis-app --network app-test  -p 6389:6379 redis 
docker run -it --name node-app --network app-test node:11 /bin/bash
複製程式碼

我們建立了兩個容器,這兩個容器都在app-test網路裡。

我們進入node-app容器裡,然後ping redis-app,發現可以訪ping通,說明容器間可以通訊了!

我們修改之前的程式碼:

const redis = new Redis({
  port: 6379,
  host: 'db',
});
複製程式碼

redis的host改為db

新建一個Dockerfile

FROM node:11
COPY package*.json /app/ 
WORKDIR /app
RUN npm install
COPY . /app
EXPOSE 3000
CMD ["node","index.js"]
複製程式碼

構建映象

docker build -t deepred5/node-redis-app .
複製程式碼

啟動容器

# 建立網路
docker network create app-test
# 啟動redis容器
docker run -d --name db --network app-test  -p 6389:6379 redis 
# 啟動node容器
docker run --name node-redis-app -p 4444:3000 --network app-test -d deepred5/node-redis-app
複製程式碼

訪問 http://localhost:4444/ 即可看到頁面

還記得我們之前做的react-app單頁應用嗎?我們可以也把這個應用加入到app-test網路裡來,這樣前端單頁應用也可以訪問後端了!

修改react-app目錄下的nginx.conf

server {
    listen       80;
    server_name  localhost;

    location / {
        root   /app/build; # 打包的路徑
        index  index.html index.htm;
        try_files $uri $uri/ /index.html; # 防止重重新整理返回404
    }

    location /api {
        proxy_pass http://node-redis-app:3000; #後臺轉發地址
    }

}
複製程式碼

重新構建映象

docker build -t deepred5/react-app-multi .  -f Dockerfile.multi
複製程式碼

啟動容器

docker run -d --name my-react-app-multi --network app-test  -p 9999:80 deepred5/react-app-multi
複製程式碼

訪問 http://localhost:9999/api/json/set?age=55 成功返回資料

Docker compose

我們現在這個專案有3個啟動映象:

  • deepred5/react-app-multi 前端單頁應用
  • redis 資料快取
  • deepred5/node-redis-app 後端服務,訪問redis,同時給前端提供介面

如果要把這個專案完整的啟動起來,按照順序需要這樣啟動:

# 啟動redis容器
docker run -d --name db --network app-test  -p 6389:6379 redis 
# 啟動node容器
docker run --name node-redis-app -p 4444:3000 --network app-test -d deepred5/node-redis-app
# 啟動前端容器
docker run -d --name my-react-app-multi --network app-test  -p 9999:80 deepred5/react-app-multi
複製程式碼

這還僅僅只是3個容器的專案,如果容器再多,啟動就變得非常複雜了!

這時,就需要docker compose出場了。

首先需要安裝docker compose,安裝完成之後

我們新建一個my-all-app目錄,然後新建docker-compose.yml

mkdir my-all-app
cd my-all-app
touch docker-compose.yml
複製程式碼
version: '3.7'

services:
  db:
    image: redis
    restart: always
    ports:
      - 6389:6379
    networks:
      - app-test

  node-redis-app:
    image: deepred5/node-redis-app
    restart: always
    depends_on:
      - db
    ports:
      - 4444:3000
    networks:
      - app-test
    
  react-app-multi:
    image: deepred5/react-app-multi
    restart: always
    depends_on:
      - node-redis-app
    ports:
      - 9999:80
    networks:
      - app-test

networks:
  app-test:
    driver: bridge
複製程式碼
# 啟動所有容器
docker-compose up -d
# 停止所有容器
docker-compose stop
複製程式碼

訪問 http://localhost:9999 檢視前端頁面

訪問 http://localhost:4444 檢視後端介面

可以看見,使用docker-compose.yml配置完啟動步驟後,啟動多容器就變得十分簡單了。

參考

相關文章