「Part 1」面向 Javascript 開發人員的 Docker 簡介(基於 Node.js)

前端晚間課發表於2022-01-05

Docker開源的應用容器引擎,如果你是從事後端的開發者,相信對這門技術應該是瞭解或是熟悉,而對於很多前端開發者,也許只是停留在聽過的階段上,甚至不知道是啥?或是會認為這是後端的技術,我不需要知道,比如說我,還真的不知道是什麼,但是如果想成為一名資深前端,這部分空缺是需要填補上的。鹹魚也要有夢想嘛,也許哪天可以躍龍門呢!

本文會通過構建帶有web前端程式碼和mongoDB資料庫的全棧node.js應用程式出發,來進一步瞭解Docker以及它的用途。

什麼是Docker

Docker 是一個開源的應用容器引擎,基於 Go 語言 並遵從 Apache2.0 協議開源。Docker 可以讓開發者打包他們的應用以及依賴包到一個輕量級、可移植的容器中,然後釋出到任何流行的 Linux、Window 機器上,也可以實現虛擬化。

聽不懂,能否通俗解釋Docker前世今生?

2010年,幾個搞IT的年輕人,在美國舊金山成立了一家名叫dotCloud(搞容器技術)的公司。結果堅持不下去,於是開源,結果火了,火了得重新起個牛逼的名字呀,於是Docker就出現了。

Docker出現前,如何模擬一個相互隔離的系統環境?答案就是虛擬機器,大家應該都不陌生,很多開發者電腦裡面都會裝VMWare,通過它,我們可以變出好幾臺子電腦,一個裝window11、一個裝CentOS,安裝上我喜歡的QQ微信等軟體,多個子電腦互相隔離、互不影響,美滋滋!但是動不動就幾個G、幾十個G,磁碟吃不消呀,而且還啟動慢。

前面說到了Docker出現前,虛擬機器在做環境隔離上是業界的網紅,但是弊端是,而Docker容器技術,其實也是一種虛擬化技術,而且輕、快、一體化,只需要MB級甚至KB級,不像虛擬機器,需要模擬一個作業系統出來,Docker只需要虛擬一個小規模的環境(類似“沙箱”)。

不行,我要看資料比對,我才信,安排....

Docker核心概念

上一小節,我們瞭解了Docker是一種容器虛擬化技術,更輕、更快、更容易一體化,接下來我們快速瞭解一下它的核心概念後,再去進入我們今天的主題,在編寫程式碼才更好理解。

Docker的三大核心概念:

  • 映象(Image)
  • 容器(Container)
  • 倉庫(Repository)

以上關係圖能反映出三者之間的關係,此處需要注意的是,我們說Docker是一種容器技術,但是Docker本身並不是容器,它是建立容器的工具,是應用容器引擎。

映象,也就是Docker映象,是一個特殊的檔案系統。它除了提供容器執行時所需的程式、庫、資源、配置等檔案外,還包含了一些為執行時準備的一些配置引數(例如環境變數),同時映象不包含任何動態資料。

我們可以有很多映象,我們想存起來,然後可以到任何地方去使用它建立容器環境,那麼就需要倉庫來儲存,也就是Docker倉庫

有怎麼一個倉庫存在,那麼所有人都可以往裡面存映象麼?不是的,如果存放了個有問題的映象,那建立容器時候不就掛了麼?所以需要有個負責對Docker映象進行管理的角色,就是Docker Registry服務(類似倉庫管理員)了。官方也提供了公共Registry服務,就是Docker Hub(有點像我們的npm市場),裡面存放著很多高質量的官方映象。

同時我們還可以通過Dockfile檔案來定製我們的映象,後續有介紹

通過上面的介紹,相信大家對Docker應該也有了個大概的認識了,這裡提供一些常用的Docker命令,

# 容器
$ docker run  // 建立並啟動容器
$ docker start // 啟動容器
$ docker ps // 檢視容器
$ docker stop // 終止容器
$ docker restart // 檢視容器
$ docker attach // 進入容器
$ docker exec // 檢視容器
$ docker export // 匯出容器
$ docker import // 匯入容器快照
$ docker rm // 刪除容器
$ docker log // 檢視日誌

# 映象
$ docker search // 檢索映象
$ docker pull // 獲取映象
$ docker images // 列出映象
$ docker image ls // 列出映象
$ docker rmi // 刪除映象
$ docker image rm // 刪除映象
$ docker save // 匯出映象
$ docker load // 匯入映象

# Dockfile定製映象以及常用指令

$ docker build // 構建映象
$ docker run // 執行映象

COPY // 複製檔案
ADD // 高階複製
CMD // 容器啟動指令
ENV // 環境變數
EXPOSE // 暴露介面


# 服務
$ docker -v // 檢視docker的簡要資訊
$ docker -version // 檢視docker版本的簡詳細資訊
$ systemctl start docker // 啟動docker
$ systemctl stop docker // 關閉docker
$ systemctl enable docker // 設定開機啟動
$ service docker restart // 重啟docker服務
$ service docker stop // 關閉docker服務

建立Hello-World容器

首先需要下載Docker下載地址,我下載的是Window Docker Desktop,接下來檢視版本資訊是否下載成功,

我下載的是20.10.11版本

然後拉取Docker Hub的官方hello-world映象,

建立並執行容器,

這樣我們第一個容器就建立出來了,可以用用上面提到的命令列docker image ls / docker image prune來檢視或是刪除沒有用(停止執行容器時,它不會被刪除,會幫助下次下載安裝速度加快)的映象,更多命令大家可以去試試!

建立Node程式

接下來我們建立個Node程式,在後面教程介紹需要使用到,具體程式碼詳情就不去做介紹了,可以檢視以下server.js和package.json

const express = require("express");
const app = express();
const port = 8080;

app.get("/", async (req, res) => {
  res.setHeader("Content-Type", "text/html");
  res.status(200);
  res.send("<h1>你好呀!前端晚間課</h1>");
});

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`);
});
{
  "name": "docker-example",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "scripts": {
    "start": "nodemon server.js"
  },
  "author": "前端晚間課",
  "license": "ISC",
  "dependencies": {
    "express": "^4.17.2"
  },
  "devDependencies": {
    "nodemon": "^2.0.15"
  }
}

執行npm run start,成功跑起來了...

Node版本不同的困擾

針對上面的server.js檔案,我們新增下面的程式碼:

 // ...
 const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("good");
  }, 300);
  reject("bad");
});

myPromise.then(() => {
  console.log("this will never run");
});

然後分別在Node < 15Node >= 15執行,結果會得到兩個不同的結果,

Node < 15

(node:764) UnhandledPromiseRejectionWarning: something happened
(Use `node --trace-warnings ...` to show where the warning was created)
(node:764) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 2)
(node:764) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

Node >= 15

node:internal/process/promises:218
          triggerUncaughtException(err, true /* fromPromise */);
          ^

[UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "recipe 
could not be generated".] {
  code: 'ERR_UNHANDLED_REJECTION'
}

會發現兩個版本的執行結果會不一致,高版本(Node >= 15)會直接導致程式崩潰(ERR_UNHANDLED_REJECTION),這是未處理的被拒絕 Promise的錯誤。

現在假設出於某種原因,此應用程式必須在 Node v14 或更早版本上執行才能工作(忽略用try...catch)。團隊中的每個開發人員都必須準備好在該環境中開發和執行,但我們公司還有一個新的應用程式,要在 Node v17 上執行!

那這時應該如何去解決這個問題?答案:Docker

建立Dockerfile

上一小節,我們介紹了兩個不同版本的Node引起的未處理的被拒絕 Promise的問題,我們介紹如何使用Docker去解決這個問題,其實也很簡單,就是需要一個Node < 15的執行環境來保證我們的程式不會崩潰,
我們在Docker Hub可以搜尋到關於的Node的映象,而且還有很多版本資訊可選。

當然我們沒必要直接使用docker pull node來拉取node映象,前面我們有提過Dockerfile可以用來自定義定製映象,它自動判斷當前機器是否存在Node映象,沒有的話再自動去Docker Hub拉取,看一下我們的Dockerfile檔案:

# 首先先選擇你需要的映象,執行在alpine的node版本是當下最流行的
FROM node:14-alpine3.12

# 工作目錄
# 這是您你將在容器內的位置
WORKDIR /usr/src/app

# 萬用字元用於確保 package.json 和 package-lock.json 都被複制
# COPY 源目錄 容器的工作目錄
COPY package*.json ./

# 安裝應用依賴
RUN npm install

# 如果你正在構建用於生產的程式碼
# RUN npm ci --only=production

# 捆綁應用程式源
COPY . .

# 配置這個埠可以從容器外部訪問
# 瀏覽器向 Node 應用程式傳送 HTTP 請求所必需的
EXPOSE 8080

# CMD 在docker run 時執行
# 就是執行shell npm run start
CMD [ "npm", "run", "start"]

ok,我們的Dockerfile檔案已經建立成功,對Dockerfile指令在上面程式碼中註釋也有做簡單的介紹,更多詳細指令用法可以到谷歌搜尋,但你可能會好奇上面的配置檔案,為什麼COPY需要執行兩次,最後的COPY . .不是複製了整個目錄,為啥上面還需要複製package*.json?

Docker的層和快取

COPY兩次是有必要的,因為Docker擁有layers(層的特性),每執行一條指令都會基於上一次指令建立的圖層基礎上再建立一層圖層,建立的圖層會被快取,只要發生改變時才會再次重新建立,我們再回過頭來看Dockerfile檔案。

COPY package*.json ./ 我們建立了一個基於該檔案內容的圖層,然後再執行npm install,這意味著除非我們更改 package.json,否則下次我們構建 Docker 時將使用npm install已經執行的快取層,我們不必每次執行時都安裝所有依賴項docker build。這將為我們節省大量時間。

COPY . .會檢視我們專案目錄中的每個檔案,因此該層將在任何檔案更改時重建(除了package*.json)。這正是我們想要的。

構建應用容器

我們再新增個.dockerignore檔案,類似我們的.gitignore,因為我們不想複製這些檔案呀。

node_modules
npm-debug.log

一切準備就緒,我們開始構建屬於自己的映象,

# 以當前專案目錄為源目錄,給映象名個名叫做qianduanwanjianke
$ docker build . -t qianduanwanjianke

再檢視我們建立的映象存不存在?

建立映象後,我們現在準備從映象構建一個容器來執行我們的應用程式:

# --name 我們給容器名了個名,叫做qianduanwanjianke-container
# -p標誌將埠從我們的主機(我們的計算機)環境3001埠對映到容器環境的8080埠,當然也可以是8080:8080。
docker run -p 3001:8080 --name qianduanwanjianke-container qianduanwanjianke

大功告成,我們訪問一下http://localhost:3001/看看是否成功,

結語

到這裡,我們通過一個Node 應用程式切入來介紹Docker,建立了我們的第一個自定義 Docker 映象和容器,並在其中執行我們的應用程式!這是面向 Javascript 開發人員的 Docker 簡介,由Node程式切入,文章內容對於熟悉Docker的後端開發者來說可能是很相當容易的,對於我們前端開發者來說應該是很好的入門教程。由於篇幅已經很長了,我決定把Docker Volume("連線"起容器內部的程式副本與專案目錄中的副本,更新同步) 以及引入資料庫,分析資料庫託管伺服器不同,如何建立分離、Docker Compose等內容放置到下篇內容再做介紹。

過程中踩的坑

1、拉取映象時報錯,報錯資訊:error during connect: This error may indicate that the docker daemon is not running...

解決辦法:

# 在Powershell 提升訪問許可權解決此問題
cd "C:\Program Files\Docker\Docker"
./DockerCli.exe -SwitchDaemon

2、建立應用容器的時候,執行docker build . -t my-node-app, 報錯資訊:no matching manifest for windows/amd64 10.0.18363 in the manifest list entries

解決辦法:
開啟Docker Devlop軟體, settiong -> Docker Engine,將experimental設定為true,重啟Docker

相關文章