在 上篇文章 裡,我們學習了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
.dockerignore
和
nginx.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裡,應該怎麼建構鏡像?
- 方案一:基于一個最基礎的
鏡像,然後我們在其中安裝Node和Redis,這樣Node和Redis之間就可以進行通信了。這種方案隻需要啟動一個容器,因為Node和Redis已經在這個容器裡了。ubuntu
- 方案二:我們基于
鏡像啟動一個容器,專門用來跑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
-
後端服務,通路redis,同時給前端提供接口deepred5/node-redis-app
如果要把這個項目完整的啟動起來,按照順序需要這樣啟動:
# 啟動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 檢視前端頁面
檢視後端接口
可以看見,使用
docker-compose.yml
配置完啟動步驟後,啟動多容器就變得十分簡單了。