前端如何玩轉 Docker 部署,請看這裡

2021-12-29 懶人碼農
前言

相信很多人都很頭疼 Docker 的部署,我自己也是。

最近發現一個很有意思的現象:一個人想學某樣技術的時候,當學會了之後,但是這時出現了一個問題需要學習另一門技術時,無論這個人前面學得多麼刻苦,用功,到這一步有 99% 的概率都會放棄。我願稱這種現象為 「學習窗口」

寫一個網站、學會 Vue.js 是很多人的「學習窗口」,只要離開了這個「學習窗口」,他們就不想學了:我都學這麼多了,草,怎麼最後還要學部署啊。

所以,這篇文章就跟大家分享一下關於 Docker 部署的那些事。

需求

按照國際慣例,先從一個非常簡單的需求入手,這個需求只完成幾件事:

上面就是一個經典到不能再經典的 Todo List 應用。

分析一下需求:待辦事項列表需要用到 資料庫 完成,記錄網站訪問量則要用到高速讀取的 緩存 來完成。

技術選型

目前我前端技術棧是 React.js,所以前端用 React.js

由於 Express 有自己的腳手架,所以,後端採用 Express

資料庫方面,因為我自己用的是 M1 的 Mac,所以 mysql 鏡像無法拉取,暫時用 mariadb 來代替。

緩存大家都很熟悉了,直接用 redis 搞定。

前端實現

關於前端的實現非常簡單,發請求使用 axios

interface Todo {
  id: number;
  title: string;
  status: 'todo' | 'done';
}

const http = axios.create({
  baseURL: 'http://localhost:4200',
})

const App = () => {
  const [newTodoTitle, setNewTodoTitle] = useState<string>('');
  const [count, setCount] = useState(0);
  const [todoList, setTodoList] = useState<Todo[]>([]);

  // 添加 todo
  const addTodo = async () => {
    await http.post('/todo', {
      title: newTodoTitle,
      status: 'todo',
    })
    await fetchTodoList();
  }

  // 獲取訪問量,並添加一個訪問量
  const fetchCount = async () => {
    await http.post('/count');
    const { data } = await http.get('/count');
    setCount(data.myCount);
  }

  // 獲取 todo 列表
  const fetchTodoList = async () => {
    const { data } = await http.get('/todo');
    setTodoList(data.todoList);
  }

  useEffect(() => {
    fetchCount().then();
    fetchTodoList().then();
  }, []);

  return (
    <div className="App">
      <header>網站訪問量:{count}</header>

      <ul>
        {todoList.map(todo => (
          <li key={todo.id}>{todo.title} - {todo.status}</li>
        ))}
      </ul>

      <div>
        <input value={newTodoTitle} onChange={e => setNewTodoTitle(e.target.value)} type="text"/>
        <button onClick={addTodo}>提交</button>
      </div>
    </div>
  );
}

後端實現

後端稍微麻煩了一點,要解決的問題有:

先在 main.ts 裡配置好路由:

var cors = require('cors')

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/count');
var todosRouter = require('./routes/todo');

var app = express();

// 解決跨域
app.use(cors());

// 業務路由
app.use('/', indexRouter);
app.use('/count', usersRouter);
app.use('/todo', todosRouter);

...

module.exports = app;

訪問量路由需要用到 redis 來實現高速讀寫:

const express = require('express');
const Redis = require("ioredis");

const router = express.Router();

// 連接 redis
const redis = new Redis({
  port: 6379,
  host: "127.0.0.1",
});

router.get('/', async (req, res, next) => {
  const count = Number(await redis.get('myCount')) || 0;

  res.json({ myCount: count })
});

router.post('/', async (req, res) => {
  const count = Number(await redis.get('myCount'));
  await redis.set('myCount', count + 1);
  res.json({ myCount: count + 1 })
})

module.exports = router;

todo 路由裡使用 sequelize 這個庫來實現資料庫連接和初始化:

const { Sequelize, DataTypes} = require('sequelize');
const express = require("express");

const router = express.Router();

// 連接資料庫
const sequelize = new Sequelize({
  host: 'localhost',
  database: 'docker_todo',
  username: 'root',
  password: '123456',
  dialect: 'mariadb',
});

// 定義 todo model
const Todo = sequelize.define('Todo', {
  id: {
    type: Sequelize.INTEGER,
    autoIncrement: true,
    primaryKey: true
  },
  title: { type: DataTypes.STRING },
  status: { type: DataTypes.STRING }
}, {});

// 同步資料庫結構
sequelize.sync({ force: true }).then(() => {
  console.log('已同步');
});

router.get('/', async (req, res) => {
  // 獲取 todo list
  const todoList = await Todo.findAll();
  res.json({ todoList });
})

router.post('/', async (req, res, next) => {
  const { title, status } = req.body;

  // 創建一個 todo
  const newTodo = await Todo.create({
    title,
    status: status || 'todo',
  });

  res.json({ todo: newTodo })
});

module.exports = router;

本地運行

本來使用以下命令就可以跑本地應用了:

# 前端
cd client && npm run start

# 後端
cd server && npm run start

然而,我們本地並沒有 mariadb 和 redis,這就有點難受了。

啟動容器

如果是在以前,我一般會在 Mac 上用下面的命令安裝一個 mariadb 和 redis:

brew install mariadb

brew install redis

然後在 自己電腦 裡一通配置(username, password...),最後才能在本地跑項目,非常麻煩。而且一旦配置錯了,草,又要重裝。。。

而 Docker 其中一個作用就是將上面 mariadb 和 redis 都打成不同 image(鏡像),使用 DockerHub 統一管理,使用 Docker 就可以快速配置一個服務。

以前只能一個電腦裝一個 MySQL,現在我能同時跑 8 個 MySQL 容器(不同埠),想刪誰刪誰,想裝誰裝誰。遇事不決,先把容器重啟,重啟不行,再用鏡像構建一個容器,構建不行,再拉一個 latest 的鏡像,再構建一次,非常的帶勁。

廢話不多說,先來把 redis 啟動:

docker run --name docker-todo-redis -p 6379:6379 -d redis

然後再把 mariadb 啟動:

docker run -p 127.0.0.1:3306:3306  --name docker-todo-mariadb -e MARIADB_ROOT_PASSWORD=123456 MARIADB_DATABASE=docker_todo -d mariadb

解釋一下參數 -p 是埠映射:本機:容器,-e 指定環境變量,-d 表示後臺運行。

再次運行:

# 前端
cd client && npm run start

# 後端
cd server && npm run start

可以在 http://localhost:3000 看到頁面:

貌似一切都很 OK 的樣子~

docker-compose

試想一下,如果現在給你一個機器,請問你要怎麼部署?你要先跑上面兩條 docker 命令,再跑下面兩條 npm 的命令,麻煩。

能不能一鍵拉起 mariadb, redis 2 個容器呢?這就是 docker-compose.yml 的由來。創建一個 dev-docker-compose.yml 文件:

version: '3'
services:
  mariadb:
    image: mariadb
    container_name: 'docker-todo-mariadb'
    environment:
      MARIADB_ROOT_PASSWORD: '123456'
      MARIADB_DATABASE: 'docker_todo'
    ports:
      - '3306:3306'
    restart: always
  redis:
    image: redis
    container_name: 'docker-todo-redis'
    ports:
      - '6379:6379'
    restart: always

這個 yml 文件描述的內容其實就等同於上面兩條 docker 命令。好處有兩個:

不用寫一串長長長長長長長長長長長長長長得讓人受不了的命令把部署命令記到小本本 docker-compose.yml 文件裡。問:怎麼部署?答:自己看 docker-compose.yml

以後,一鍵跑本地服務的時候就可以一鍵啟動 mariadb 和 redis 了:

docker-compose -f dev-docker-compose.yml up -d

Dockerfile

不過,在生產環境時每次都要跑 npm 這兩條命令還是很煩,能不能把這兩行也整全到 docker-compose 裡呢?

注意:生產環境應該要用 npm run build 構建應用,然後再跑構建出來的 JS 才是正常開發流程,這裡為了簡化流程,就以 npm run start 來做例子說明。

既然 docker-compose 是通過 image 創建容器的,那麼我們的 React App 和 Express App 也打成兩個 image,然後用 docker-compose 分別創建容器不就 OK 了麼?

構建容器說白了就是我們常說的 「CICD 或者構建流水線」,只不過這個 「流水線」 關鍵的只有一條 npm run start。描述 「流水線」 的叫 Dockerfile (注意這裡不是駝峰寫法)。

注意:正常的鏡像構建和啟動應該是整個項目 CICD 其中的一環,這裡只是打個比方。項目的 CICD 除了跑命令,構建應用,還會有代碼檢查、脫敏檢查、發布消息推送等步驟,是更為繁雜的一套流程。

先把 React 的 Dockerfile 整了:

# 使用 node 鏡像
FROM node

# 準備工作目錄
RUN mkdir -p /app/client
WORKDIR /app/client

# 複製 package.json
COPY package*.json /app/client/

# 安裝目錄
RUN npm install

# 複製文件
COPY . /app/client/

# 開啟 Dev
CMD ["npm", "run", "start"]

非常的簡單,需要注意的是容器也可以看成一個電腦裡的電腦,所以把自己電腦的文件複製到 「容器電腦」 裡是非常必要的一步。

Express App 的 Dockerfile 和上面的幾乎一毛一樣:

# 使用 node 鏡像
FROM node

# 初始化工作目錄
RUN mkdir -p /app/server
WORKDIR /app/server

# 複製 package.json
COPY package*.json /app/server/

# 安裝依賴
RUN npm install

# 複製文件
COPY . /app/server/

# 開啟 Dev
CMD ["npm", "run", "start"]

那麼現在再來改造一個 prod-docker-compose.yml 文件:

version: '3'
services:
  client:
    build:
      context: ./client
      dockerfile: Dockerfile
    container_name: 'docker-todo-client'
    # 暴露埠
    expose:
      - 3000
    # 暴露埠
    ports:
      - '3000:3000'
    depends_on:
      - server
    restart: always
  server:
    # 構建目錄
    build:
      context: ./server
      dockerfile: Dockerfile
    # 容器名
    container_name: 'docker-todo-server'
    # 暴露埠
    expose:
      - 4200
    # 埠映射
    ports:
      - '4200:4200'
    restart: always
    depends_on:
      - mariadb
      - redis
  mariadb:
    image: mariadb
    container_name: 'docker-todo-mariadb'
    environment:
      MARIADB_ROOT_PASSWORD: '123456'
      MARIADB_DATABASE: 'docker_todo'
    ports:
      - '3306:3306'
    restart: always
  redis:
    image: redis
    container_name: 'docker-todo-redis'
    ports:
      - '6379:6379'
    restart: always

上面的配置應該都不難理解,不過,還是有一些細節需要注意:

埠都要暴露出來,也要做映射,不然本地也訪問不了 3000 和  4200 埠depends_on 的作用是等 maraidb 和 redis 兩個容器起來了再啟動當前容器

然後運行下面命令,一鍵啟動:

docker-compose -f prod-docker-compose.yml up -d --build

後面 --build 是指每次跑時都構建一次鏡像。

然而,Boom:

ConnectionRefusedError: connect ECONNREFUSED 127.0.0.1:3306
...

怎麼連不上了?

解決連不上的問題

連不上的原因是我們這裡用了 localhost 和 127.0.0.1。

雖然每個容器都在我們主機 127.0.0.1 網絡裡,但是容器之間是需要通過對方的 IP 地址來交流和訪問的,按照官網的介紹 通過 Container Name 就可得知對方容器的 IP。

因此,Express App 裡的 host 不能寫 127.0.0.1,而要填 docker-todo-redis 和 docker-todo-mariadb。下面用環境變量 NODE_ENV 來區分是否以 Docker 啟動 App。

修改 mariadb 的連接:

// 連接資料庫
const sequelize = new Sequelize({
  host: process.env.NODE_ENV === 'docker' ? 'docker-todo-mariadb' : "127.0.0.1" ,
  database: 'docker_todo',
  username: 'root',
  password: '123456',
  dialect: 'mariadb',
});

再修改 redis 的連接:

const redis = new Redis({
  port: 6379,
  host: process.env.NODE_ENV === 'docker' ? 'docker-todo-redis' : "127.0.0.1" ,
});

然後在 /server/Dockerfile 裡添加 NODE_ENV=docker:

# 使用 node 鏡像
FROM node

# 初始化工作目錄
RUN mkdir -p /app/server
WORKDIR /app/server

# 複製 package.json
COPY package*.json /app/server/

ENV NODE_ENV=docker

# 安裝依賴
RUN npm install

# 複製文件
COPY . /app/server/

# 開啟 Dev
CMD ["npm", "run", "start"]

現在繼續運行我們的 「一鍵啟動」 命令,就能啟動我們的生產環境了:

docker-compose -f prod-docker-compose.yml up -d --build

總結

一句話總結,Dockerfile 是用於構建 Docker 鏡像的,跟我們平常接觸的 CICD 或者流水線有點類似。而 docker-compose 的作用則是 「一鍵拉起」 N 個容器。

上面整個例子放在 Github 這裡了,可以 Clone 下來自己搗鼓玩玩。

相關焦點

  • 如何使用 Docker 部署容器
    而不是安裝作業系統,然後安裝伺服器軟體,然後部署精心設計的應用程式或站點,您可以簡單地在一個獨立的包中開發所有內容,並使用單個命令將其推出。這是使用容器的眾多好處之一。它們使開發和部署周期變得異常高效。但是你如何部署這些容器?我想在這裡指導你完成這個過程。我們將專注於在Ubuntu Server 18.04上部署基本的NGINX Web伺服器作為容器。
  • 在 Docker 中完整部署 Web 應用
    本文就在一個 Docker 容器中完整部署整個 Web 應用的需求作詳細的介紹。作者簡介:外號夫子,長於長江之上「梨花島」,總喜歡一個江湖的傳說,如果你偶然記起關於「桃花島」黃藥師的傳說,記得划船來找我。個人博客:fuzhii.com。其他博文《換個timeline看知乎》,《用機器學習的方法鑑別紅樓夢作者》。
  • Flask + Docker 無腦部署新手教程
    這種情況我有經驗:「google 啥都有,搜 flask 部署去」朋友:「完全看不懂」我直覺想反駁,可是想起當初我學部署的時候也一頭霧水肝幾天也沒搞明白。其實在 docker 流行的今天,部署已經要比當初我學的時候要方便得多,但是前段時間我 google 搜了一圈的確沒看到幾篇比較好的 Docker + Flask 的指導,於是寫一篇菜鳥也能看懂的新手教程。本教程的特點就是比較無腦,照著做就能部署成功。同時給出一些連結,想深入一點了解的可以自行深入學習。
  • 搭建前端開發環境――docker篇
    可自行配置,滿足不同項目的需求 二、前端靜態搭建思路 基於ubuntu系統環境,利用nginx靜態資源伺服器經過docker暴露出來的埠進行請求轉發,這樣後端的開發機上面只需要安裝docker就能夠訪問前端的靜態資源,不需要訪問前端開發機。
  • Docker+Jenkins+Github實現Golang項目自動部署
    鏡像[jenkins調用的是宿主機上的docker環境,如何調用的,後面會說明]啟動golang項目的docker鏡像docker的安裝這裡就不說了/main"]cd $WORKSPACEexport GOPROXY=https://goproxy.iogo mod tidy# 列印依賴,部署成功後查看版本依賴是否如預期cat ./go.modcd .
  • docker會用了?不來了解一下全自動構建部署嗎
    如何使用github實現自動構建鏡像?如何手動推送(push)拉取(pull)鏡像?如何使用github+docker hub(官方)+shell定時任務實現項目自動構建、部署。docker配合github實現鏡像自動構建登陸docker hub創建自己的帳戶,並且創建一個鏡像倉庫,那這裡值得說一句的是,每一個docker帳戶都可以免費擁有一個私有的docker倉庫。當然,如果你覺得少的話也可以花錢購買價格也並不是很高。那我們就來創建一個倉庫。
  • Docker中部署TensorFlow GPU
    Docker中部署TensorFlow GPUDocker 是在 Linux 上啟用 TensorFlow GPU 支持的最簡單方法,因為只需在主機上安裝
  • .NET 之 Docker 部署詳細流程
    docker部署.net的流程,作為一種學習總結,以及後續會寫一些在該基礎之上的文章。生成鏡像添加dockerfile選中項目右鍵添加docker支持,本次部署在windows平臺難道我們部署的方式有問題?讓我們訪問下項目的接口
  • 用Docker部署SpringBoot應用程式
    將應用程式部署為Docker容器可以幫助您在多個環境(即dev、QA、暫存、生產)中順利地行動應用程式。本教程向您展示了如何利用Docker部署SpringBoot應用程式。unzip demo.zip -d spring-boot-dockercd spring-boot-docker接下來,通過創建文件添加一個web控制器src/main/java/com/okta/springdocker
  • 用 Docker 打包 Node.js 程序
    // 每日前端夜話 第378篇// 正文共:1600 字// 預計閱讀時間:7 分鐘你聽到過這樣的對話嗎?程序猿1:在我的計算機上不能用 😒程序猿2:在我這裡好好的啊 🤨這種對話很常見。這一般是由於工作環境設置或配置不同而引起的。這就是為什麼要使用 docker 的主要目的。
  • 使用 Docker 讓部署 Django 項目更加輕鬆
    這樣我們在部署上線前,就可以在本地進行驗證,只要驗證沒問題,我們就有 99% 的把握保證部署上線後也沒有問題(1%保留給程序玄學)。這個辦法就是使用 Docker。Docker 是一種容器技術,可以為我們提供一個隔離的運行環境。
  • 如何使用MongoDB和Docker運行Flask應用
    按照步驟1和步驟2「如何安裝和使用Docker」的指引安裝的Docker。按照步驟1「如何安裝Docker Compose」的指引安裝的Docker Compose。進入到新建的目錄下:前端服務,例如nginx,將連接到前端網絡,因為它需要公開訪問。後端服務,如MongoDB,將連接到後端網絡,以防止未經授權訪問該服務。接下來,你將使用捲來持久化資料庫、應用程式和配置文件。由於應用程式將使用資料庫和文件,因此必須保留對它們所做的更改。卷由Docker管理並存儲在文件系統中。將此代碼添加到docker-compose.yml文件以配置卷:
  • docker-4:mac使用docker部署開發用rocketmq
    為了開發方便,有時需要在本地部署rocketmq,使用docker是一個高性價比的方式,故有此文。目錄:(1).mac本地docker化rocketmq(2).mac本地docker化rocketmq-console(3).測試(1).mac本地docker化rocketmq現在官方rocketmq-docker:git clone https://github.com/apache/rocketmq-docker
  • 微服務部署到docker中
    可以看到項目根目錄下新增一個名為Dockerfile的文件6.通過Xftp將項目從Windows下的磁碟位置拷貝到Linux的root目錄下二、Linux下通過Docker構建應用1.通過Xshell連接上Linux虛擬機,進入到項目目錄下,然後通過docker
  • 實操將TensorFlow模型部署成Docker服務
    深度學習模型如何服務化是一個機器學習領域工程方面的熱點,現在業內一個比較主流的做法是將模型和模型的服務環境做成docker image。這樣做的一個好處是屏蔽了模型對環境的依賴,因為深度學習模型在服務的時候可能對各種框架版本和依賴庫有要求,解決運行環境問題一直是個令人頭痛的事情。
  • 【長篇博文】Docker學習筆記與深度學習環境的搭建和部署(二)
    1.5、Rancher與Rancher的安裝和部署本文將使用此Rancher工具,其他圖形化界面請選擇適合自己的進行開發。Rancher是一個開源的企業級全棧化容器部署及管理平臺。根據上一篇文章的基本知識,這裡可通過docker image ls或docker images命令查看我們pull的鏡像列表:
  • Kind + Docker 一鍵部署K8s集群
    時下網際網路最火的技術無非是容器雲和AI,而虛擬雲技術方面最火則是docker和K8S。docker學習和實踐都很容易,但是K8S的由於集群化,部署需要較多的機器,環境搭建學習實踐比較費勁這一度影響了K8S技術的普及。
  • 基於Docker部署 Tomcat集群、 Nginx負載均衡
    寫在前面看完Dokcer相關的書籍,正好有個項目要這樣搞,所以自己練習一下。
  • 實戰 Windows Server Docker :Docker化現有 IIS 應用的正確姿勢 (2)
    這一篇,我們來填一些稍大一些的坑:如何docker化一個現有的iis應用。問題分析聽說Windows支持原生docker了,大家一定都很興奮。然而,大家想過沒有,Windows Server Docker最適合什麼場景呢?部署.NET Core應用?為什麼不選擇Linux下的docker?
  • 使用Docker和Codeship來裝運Node.js應用
    以及它是如何讓你的生活變得更加簡單的?不可變的基礎結構所謂不可變的基礎結構通常包括數據和其他相關的東西。所謂其他相關的東西取決於部署階段的替代物。 不僅僅是安全不定,或者是在生產系統中的配置變化。為了達成這一點,我們需要在兩種途徑中選擇: 基於機器的或是基於容器的方案。