科技 · 1 月. 9, 2023/星期一

Docker

Docker 完全手冊

Docker 完全手冊(2021 最新版)

容器化的概念很早就有了。2013 年 Docker 引擎的出現使應用程序容器化變得更加容易。

根據 Stack Overflow 開發者調查-2020Docker 是開發者 #1 最想要的平台#2 最喜歡的平台,以及#3 最流行的平台

盡管 Docker 功能強大,但上手確並不容易。因此,本書將介紹從基礎知識到更高層次容器化的的所有內容。讀完整本書之後,你應該能夠:

  • 容器化(幾乎)任何應用程序
  • 將自定義 Docker 鏡像上傳到在線倉庫
  • 使用 Docker Compose 處理多個容器

前提

  • 熟悉 Linux 終端操作
  • 熟悉 JavaScript(稍後的的演示項目用到了 JavaScript)

目錄

  • 容器化和 Docker 簡介
  • 怎樣安裝 Docker
    • 怎樣在 macOS 里安裝 Docker
    • 怎樣在 Windows 上安裝 Docker
    • 怎樣在 Linux 上安裝 Docker
  • 初識 Docker – Docker 基本知識介紹
    • 什麼是容器?
    • 什麼是 Docker 鏡像?
    • 什麼是倉庫?
    • Docker 架構概述
    • 全景圖
  • Docker 容器操作基礎知識
    • 怎樣運行容器
    • 怎樣公開端口
    • 如何使用分離模式
    • 怎樣列表展示容器
    • 怎樣命名或者重命名一個容器
    • 怎樣停止或者殺死運行中的容器
    • 怎樣重新啟動容器
    • 怎樣創建而不運行容器
    • 怎樣移除掛起的容器
    • 怎樣以交互式模式運行容器
    • 怎樣在容器里執行命令
    • 如何處理可執行鏡像
  • Docker 鏡像操作基礎知識
    • 如何創建 Docker 鏡像
    • 如何標記 Docker 鏡像
    • 如何刪除、列表展示鏡像
    • 理解 Docker 鏡像的分層
    • 怎樣從源碼構建 NGINX
    • 怎樣優化 Docker 鏡像
    • 擁抱 Alpine Linux
    • 怎樣創建可執行 Docker 鏡像
    • 怎樣在線共享 Docker 鏡像
  • 怎樣容器化 JavaScript 應用
    • 如何編寫開發 Dockerfile
    • 如何在 Docker 中使用綁定掛載
    • 如何在 Docker 中使用匿名卷
    • 如何在 Docker 中執行多階段構建
    • 如何忽略不必要的文件
  • Docker 中的網絡操作基礎知識
    • Docker 網絡基礎
    • 如何在 Docker 中創建用戶定義的橋接網絡
    • 如何在 Docker 中將容器連接到網絡
    • 如何在 Docker 中從網絡分離容器
    • 如何刪除 Docker 中的網絡
  • 如何容器化多容器 JavaScript 應用程序
    • 如何運行數據庫服務
    • 如何在 Docker 中使用命名卷
    • 如何從 Docker 中的容器訪問日志
    • 如何在 Docker 中創建網絡並連接數據庫服務
    • 如何編寫 Dockerfile
    • 如何在正在運行的容器中執行命令
    • 如何在 Docker 中編寫管理腳本
  • 如何使用 Docker-Compose 組合項目
    • Docker Compose 基礎
    • 如何在 Docker Compose 中啟動服務
    • 如何在 Docker Compose 中列表展示服務
    • 如何在 Docker Compose 正在運行的服務中執行命令
    • 如何訪問 Docker Compose 中正在運行的服務日志
    • 如何在 Docker Compose 中停止服務
    • 如何在 Docker Compose 中編寫全棧應用程序
  • 結論

項目代碼

可以在這個倉庫中找到示例項目的代碼,歡迎 ⭐。

完整代碼在 containerized 分支。

貢獻

這本書是完全開源的,歡迎高質量的貢獻。可以在這個倉庫中找到完整的內容。

我通常先在本書的 GitBook 版本上進行更改和更新,然後在將其發布在 freeCodeCamp 專欄。你可以在這個鏈接中找到本書的最新編輯中版本。別忘了評分支持。

如果你正在尋找本書的完整穩定版本,那麼 freeCodeCamp 是最好的選擇。如果你有所收獲,請分享給你的朋友。

不管閱讀本書的哪個版本,都不要忘記留下你的意見。歡迎提出建設性的批評。

容器化和 Docker 簡介

摘自 IBM,

容器化意味著封裝或打包軟件代碼及其所有依賴項,以便它可以在任何基礎架構上統一且一致地運行。

換句話說,容器化可以將軟件及其所有依賴項打包在一個自包含的軟件包中,這樣就可以省略麻煩的配置,直接運行。

舉一個現實生活的場景。假設你已經開發了一個很棒的圖書管理應用程序,該應用程序可以存儲所有圖書的信息,還可以為別人提供圖書借閱服務。

如果列出依賴項,如下所示:

  • Node.js
  • Express.js
  • SQLite3

理論上應該是這樣。但是實際上還要搞定其他一些事情。 Node.js 使用了 node-gyp 構建工具來構建原生加載項。根據官方存儲庫中的安裝說明,此構建工具需要 Python 2 或 3 和相應的的 C/C ++ 編譯器工具鏈。

考慮到所有這些因素,最終的依賴關系列表如下:

  • Node.js
  • Express.js
  • SQLite3
  • Python 2 or 3
  • C/C++ tool-chain

無論使用什麼平台,安裝 Python 2 或 3 都非常簡單。在 Linux 上,設置 C/C ++ 工具鏈也非常容易,但是在 Windows 和 Mac 上,這是一項繁重的工作。

在 Windows 上,C++ 構建工具包有數 GB 之大,安裝需要花費相當長的時間。在 Mac 上,可以安裝龐大的 Xcode 應用程序,也可以安裝小巧的 Xcode 命令行工具包。

不管安裝了哪一種,它都可能會在 OS 更新時中斷。實際上,該問題非常普遍,甚至連官方倉庫都專門提供了 macOS Catalina 的安裝說明

這里假設你已經解決了設置依賴項的所有麻煩,並且已經準備好開始。這是否意味著現在開始就一帆風順了?當然不是。

如果你使用 Linux 而同事使用 Windows 該怎麼辦?現在,必須考慮如何處理這兩個不同的操作系統不一致的路徑,或諸如 nginx 之類的流行技術在 Windows 上未得到很好的優化的事實,以及諸如 Redis 之類的某些技術甚至都不是針對 Windows 預先構建的。

即使你完成了整個開發,如果負責管理服務器的人員部署流程搞錯了,該怎麼辦?

所有這些問題都可以通過以下方式解決:

  • 在與最終部署環境匹配的隔離環境(稱為容器)中開發和運行應用程序。
  • 將你的應用程序及其所有依賴項和必要的部署配置放入一個文件(稱為鏡像)中。
  • 並通過具有適當授權的任何人都可以訪問的中央服務器(稱為倉庫)共享該鏡像。

然後,你的同事就可以從倉庫中下載鏡像,可以在沒有平台沖突的隔離環境中運行應用,甚至可以直接在服務器上進行部署,因為該鏡像也可以進行生產環境配置。

這就是容器化背後的想法:將應用程序放在一個獨立的程序包中,使其在各種環境中都可移植且可回溯。

現在的問題是:Docker 在這里扮演什麼角色?

正如我之前講的,容器化是一種將一切統一放入盒子中來解決軟件開發過程中的問題的思想。

這個想法有很多實現。Docker 就是這樣的實現。這是一個開放源代碼的容器化平台,可讓你對應用程序進行容器化,使用公共或私有倉庫共享它們,也可以編排它們。

目前,Docker 並不是市場上唯一的容器化工具,卻是最受歡迎的容器化工具。我喜歡的另一個容器化引擎是 Red Hat 開發的 Podman。其他工具,例如 Google 的 Kaniko,CoreOS 的 rkt 都很棒,但和 Docker 還是有差距。

此外,如果你想了解容器的歷史,可以閱讀 A Brief History of Containers: From the 1970s Till Now,它描述了該技術的很多重要節點。

怎樣安裝 Docker

Docker 的安裝因使用的操作系統而異。但這整個過程都非常簡單。

Docker可在 Mac、Windows 和 Linux 這三個主要平台上完美運行。在這三者中,在 Mac 上的安裝過程是最簡單的,因此我們從這里開始。

怎樣在 macOS 里安裝 Docker

在 Mac 上,要做的就是跳轉到官方的下載頁面,然後單擊_Download for Mac(stable)_按鈕。

你會看到一個常規的 Apple Disk Image 文件,在該文件的內有 Docker 應用程序。所要做的就是將文件拖放到 Applications 目錄中。

只需雙擊應用程序圖標即可啟動 Docker。應用程序啟動後,將看到 Docker 圖標出現在菜單欄上。

現在,打開終端並執行 docker --version 和 docker-compose --version 以驗證是否安裝成功。

怎樣在 Windows 上安裝 Docker

在 Windows 上,步驟幾乎相同,當然還需要執行一些額外的操作。安裝步驟如下:

  1. 跳轉到此站點,然後按照說明在 Windows 10 上安裝 WSL2。
  2. 然後跳轉到官方下載頁面 並單擊 Download for Windows(stable) 按鈕。
  3. 雙擊下載的安裝程序,然後使用默認設置進行安裝。

安裝完成後,從開始菜單或桌面啟動 Docker Desktop。Docker 圖標應顯示在任務欄上。

現在,打開 Ubuntu 或從 Microsoft Store 安裝的任何發行版。執行 docker --version 和 docker-compose --version 命令以確保安裝成功。

也可以從常規命令提示符或 PowerShell 訪問 Docker,只是我更喜歡使用 WSL2。

怎樣在 Linux 上安裝 Docker

在 Linux 上安裝 Docker 的過程有所不同,具體操作取決於你所使用的發行版,它們之間差異可能更大。但老實說,安裝與其他兩個平台一樣容易(如果不能算更容易的話)。

Windows 或 Mac 上的 Docker Desktop 軟件包是一系列工具的集合,例如Docker EngineDocker ComposeDocker DashboardKubernetes 和其他一些好東西。

但是,在 Linux 上,沒有得到這樣的捆綁包。可以手動安裝所需的所有必要工具。 不同發行版的安裝過程如下:

安裝完成後,打開終端並執行 docker --version 和 docker-compose --version 以確保安裝成功。

盡管無論使用哪個平台,Docker 的性能都很好,但與其他平台相比,我更喜歡 Linux。在整本書中,我將使用Ubuntu 20.10 或者 Fedora 33

一開始就需要闡明的另一件事是,在整本書中,我不會使用任何 GUI 工具操作 Docker。

我在各個平台用過很多不錯的 GUI 工具,但是介紹常見的 docker 命令是本書的主要目標之一。

初識 Docker – 介紹 Docker 基本知識

已經在計算機上啟動並運行了 Docker,現在該運行第一個容器了。打開終端並執行以下命令:

docker run hello-world

# Unable to find image 'hello-world:latest' locally
# latest: Pulling from library/hello-world
# 0e03bdcc26d7: Pull complete 
# Digest: sha256:4cf9c47f86df71d48364001ede3a4fcd85ae80ce02ebad74156906caff5378bc
# Status: Downloaded newer image for hello-world:latest
# 
# Hello from Docker!
# This message shows that your installation appears to be working correctly.
# 
# To generate this message, Docker took the following steps:
#  1. The Docker client contacted the Docker daemon.
#  2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
#     (amd64)
#  3. The Docker daemon created a new container from that image which runs the
#     executable that produces the output you are currently reading.
#  4. The Docker daemon streamed that output to the Docker client, which sent it
#     to your terminal.
#
# To try something more ambitious, you can run an Ubuntu container with:
#  $ docker run -it ubuntu bash
# 
# Share images, automate workflows, and more with a free Docker ID:
#  https://hub.docker.com/
#
# For more examples and ideas, visit:
#  https://docs.docker.com/get-started/

hello-world 鏡像是使用 Docker 進行最小化容器化的一個示例。它有一個從 hello.c 文件編譯的程序,負責打印出終端看到的消息。

現在,在終端中,可以使用 docker ps -a 命令查看當前正在運行或過去運行的所有容器:

docker ps -a

# CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                     PORTS               NAMES
# 128ec8ceab71        hello-world         "/hello"            14 seconds ago      Exited (0) 13 seconds ago                      exciting_chebyshev

在輸出中,使用 hello-world 鏡像運行了名為 exciting_chebyshev 的容器,其容器標識為 128ec8ceab71。它已經在 Exited (0) 13 seconds ago,其中 (0) 退出代碼表示在容器運行時未發生任何錯誤。

現在,為了了解背後發生的事情,必須熟悉 Docker 體系結構和三個非常基本的容器化概念,如下所示:

我已經按字母順序列出了這三個概念,並且將從列表中的第一個開始介紹。

什麼是容器?

在容器化世界中,沒有什麼比容器的概念更基礎的了。

官方 Docker resources 網站說 –

容器是應用程序層的抽象,可以將代碼和依賴項打包在一起。容器不虛擬化整個物理機,僅虛擬化主機操作系統。

可以認為容器是下一代虛擬機。

就像虛擬機一樣,容器是與主機系統是彼此之間完全隔離的環境。它也比傳統虛擬機輕量得多,因此可以同時運行大量容器,而不會影響主機系統的性能。

容器和虛擬機實際上是虛擬化物理硬件的不同方法。兩者之間的主要區別是虛擬化方式。

虛擬機通常由稱為虛擬機監控器的程序創建和管理,例如 Oracle VM VirtualBoxVMware WorkstationKVMMicrosoft Hyper-V 等等。 該虛擬機監控程序通常位於主機操作系統和虛擬機之間,充當通信介質。

每個虛擬機都有自己的 guest 操作系統,該操作系統與主機操作系統一樣消耗資源。

在虛擬機內部運行的應用程序與 guest 操作系統進行通信,該 guest 操作系統在與虛擬機監控器進行通信,後者隨後又與主機操作系統進行通信,以將必要的資源從物理基礎設施分配給正在運行的應用程序。

虛擬機內部運行的應用程序與物理基礎設施之間存在很長的通信鏈。在虛擬機內部運行的應用程序可能只擁有少量資源,因為 guest 操作系統會占用很大的開銷。

與虛擬機不同,容器以更智能的方式完成虛擬化工作。在容器內部沒有完整的 guest 操作系統,它只是通過容器運行時使用主機操作系統,同時保持隔離 – 就像傳統的虛擬機一樣。

容器運行時(即 Docker)位於容器和主機操作系統之間,而不是虛擬機監控器中。容器與容器運行時進行通信,容器運行時再與主機操作系統進行通信,以從物理基礎設施中獲取必要的資源。

由於消除了整個主機操作系統層,因此與傳統的虛擬機相比,容器的更輕量,資源占用更少。

為了說明這一點,請看下面的代碼片段:

uname -a
# Linux alpha-centauri 5.8.0-22-generic #23-Ubuntu SMP Fri Oct 9 00:34:40 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

docker run alpine uname -a
# Linux f08dbbe9199b 5.8.0-22-generic #23-Ubuntu SMP Fri Oct 9 00:34:40 UTC 2020 x86_64 Linux

在上面的代碼片段中,在主機操作系統上執行了 uname -a 命令以打印出內核詳細信息。然後在下一行,我在運行 Alpine Linux 的容器內執行了相同的命令。

從輸出中可以看到,該容器確實正在使用主機操作系統中的內核。這證明了容器虛擬化主機操作系統而不是擁有自己的操作系統這一點。

如果你使用的是 Windows 計算機,則會發現所有容器都使用 WSL2 內核。發生這種情況是因為 WSL2 充當了 Windows 上 Docker 的後端。在 macOS 上,默認後端是在 HyperKit 虛擬機管理程序上運行的 VM。

什麼是 Docker 鏡像?

鏡像是分層的自包含文件,充當創建容器的模板。它們就像容器的凍結只讀副本。 鏡像可以通過倉庫進行共享。

過去,不同的容器引擎具有不同的鏡像格式。但是後來,開放式容器計劃(OCI)定義了容器鏡像的標準規範,該規範被主要的容器化引擎所遵循。這意味著使用 Docker 構建的映像可以與 Podman 等其他運行時一起使用,而不會有兼容性問題。

容器只是處於運行狀態的鏡像。當從互聯網上獲取鏡像並使用該鏡像運行容器時,實際上是在先前的只讀層之上創建了另一個臨時可寫層。

在本書的後續部分中,這一概念將變得更加清晰。但就目前而言,請記住,鏡像是分層只讀文件,其中保留著應用程序所需的狀態。

什麼是倉庫?

已經了解了這個難題的兩個非常重要的部分,即 Containers 和 Images 。 最後一個是 Registry

鏡像倉庫是一個集中式的位置,可以在其中上傳鏡像,也可以下載其他人創建的鏡像。 Docker Hub 是 Docker 的默認公共倉庫。另一個非常流行的鏡像倉庫是 Red Hat 的 Quay

在本書中,我將使用 Docker Hub 作為首選倉庫。

可以免費在 Docker Hub 上共享任意數量的公共鏡像。供世界各地的人們下載免費使用。可在我的個人資料(fhsinchy)頁面上找到我上傳的鏡像。

除了 Docker Hub 或 Quay,還可以創建自己的鏡像倉庫來托管私有鏡像。計算機中還運行著一個本地倉庫,該倉庫緩存從遠程倉庫提取的鏡像。

Docker 架構概述

既然已經熟悉了有關容器化和 Docker 的大多數基本概念,那麼現在是時候了解 Docker 作為軟件的架構了。

該引擎包括三個主要組件:

  1. Docker 守護程序: 守護程序(dockerd)是一個始終在後台運行並等待來自客戶端的命令的進程。守護程序能夠管理各種 Docker 對象。
  2. Docker 客戶端: 客戶端(docker)是一個命令行界面程序,主要負責傳輸用戶發出的命令。
  3. REST API: REST API 充當守護程序和客戶端之間的橋梁。使用客戶端發出的任何命令都將通過 API 傳遞,最終到達守護程序。

根據官方文檔,

“ Docker 使用客戶端-服務器體系結構。Docker client 與 Docker daemon 對話,daemon 繁重地構建、運行和分發 Docker 容器”。

作為用戶,通常將使用客戶端組件執行命令。然後,客戶端使用 REST API 來訪問長期運行的守護程序並完成工作。

全景圖

好吧,說的夠多了。 現在是時候了解剛剛學習的所有這些知識如何和諧地工作了。在深入解釋運行 docker run hello-world 命令時實際發生的情況之前,看一下下面的圖片:

該圖像是在官方文檔中找到的圖像的略微修改版本。 執行命令時發生的事件如下:

  1. 執行 docker run hello-world 命令,其中 hello-world 是鏡像的名稱。
  2. Docker 客戶端訪問守護程序,告訴它獲取 hello-world 鏡像並從中運行一個容器。
  3. Docker 守護程序在本地倉庫中查找鏡像,並發現它不存在,所以在終端上打印 Unable to find image 'hello-world:latest' locally
  4. 然後,守護程序訪問默認的公共倉庫 Docker Hub,拉取 hello-world 鏡像的最新副本,並在命令行中展示 Unable to find image 'hello-world:latest' locally
  5. Docker 守護程序根據新拉取的鏡像創建一個新容器。
  6. 最後,Docker 守護程序運行使用 hello-world 鏡像創建的容器,該鏡像在終端上輸出文本。

Docker 守護程序的默認行為是在 hub 中查找本地不存在的鏡像。但是,拉取了鏡像之後,它將保留在本地緩存中。因此,如果再次執行該命令,則在輸出中將看不到以下幾行:

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
0e03bdcc26d7: Pull complete
Digest: sha256:d58e752213a51785838f9eed2b7a498ffa1cb3aa7f946dda11af39286c3db9a9
Status: Downloaded newer image for hello-world:latest

如果公共倉庫中有可用鏡像的更新版本,則守護程序將再次拉取該鏡像。那個 :latest 是一個標記。鏡像通常包含有意義的標記以指示版本或內部版本。稍後,將更詳細地介紹這一點。

Docker 容器操作基礎知識

在前面的部分中,已經了解了 Docker 的構建模塊,還使用 docker run 命令運行了一個容器。

在本節中,將詳細介紹容器的操作。容器操作是每天要執行的最常見的任務之一,因此,正確理解各種命令至關重要。

但是請記住,這並不是可以在 Docker 上執行的所有命令的詳盡列表。我只會介紹最常見的那些。當想知道某一命令的更多用法時,可以訪問 Docker 命令行的官方參考

怎樣運行容器

之前,已經使用 docker run 來使用 hello-world 鏡像創建和啟動容器。此命令的通用語法如下:

docker run <image name>

盡管這是一個完全有效的命令,但是有一種更好的方式可以將命令分配給 docker 守護程序。

在版本 1.13 之前,Docker 僅具有前面提到的命令語法。後來,命令行經過了重構具有了以下語法:

docker <object> <command> <options>

使用以下語法:

  • object 表示將要操作的 Docker 對象的類型。這可以是 containerimagenetwork 或者 volume 對象。
  • command 表示守護程序要執行的任務,即 run 命令。
  • options 可以是任何可以覆蓋命令默認行為的有效參數,例如端口映射的 --publish 選項。

現在,遵循此語法,可以將 run 命令編寫如下:

docker container run <image name>

image name 可以是在線倉庫或本地系統中的任何鏡像。例如,可以嘗試使用fhsinchy / hello-dock 鏡像運行容器。 該鏡像包含一個簡單的 Vue.js應用程序,該應用程序在容器內部的端口 80 上運行。

請在終端上執行以下命令以使用此鏡像運行容器:

docker container run --publish 8080:80 fhsinchy/hello-dock

# /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
# /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
# /docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
# 10-listen-on-ipv6-by-default.sh: Getting the checksum of /etc/nginx/conf.d/default.conf
# 10-listen-on-ipv6-by-default.sh: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
# /docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
# /docker-entrypoint.sh: Configuration complete; ready for start up

該命令不言自明。唯一需要說明的部分是 --publish 8080:80 部分,將在下一個小節中進行說明。

怎樣公開端口

容器是隔離的環境。主機系統對容器內部發生的事情一無所知。因此,從外部無法訪問在容器內部運行的應用程序。

要允許從容器外部進行訪問,必須將容器內的相應端口發布到本地網絡上的端口。--publish 或 -p 選項的通用語法如下:

--publish <host port>:<container port>

在上一小節中編寫了 --publish 8080:80 時,這意味著發送到主機系統端口 8080 的任何請求都將轉發到容器內的端口 80。

現在要在瀏覽器上訪問該應用程序,只需訪問 http://127.0.0.1:8080

可以在終端窗口按下 ctrl + c 組合鍵或關閉終端窗口來停止容器。

如何使用分離模式

run 命令的另一個非常流行的選項是 ---detach 或 -d 選項。 在上面的示例中,為了使容器繼續運行,必須將終端窗口保持打開狀態。關閉終端窗口會停止正在運行的容器。

這是因為,默認情況下,容器在前台運行,並像從終端調用的任何其他普通程序一樣將其自身附加到終端。

為了覆蓋此行為並保持容器在後台運行,可以在 run 命令中包含 --detach 選項,如下所示:

docker container run --detach --publish 8080:80 fhsinchy/hello-dock

# 9f21cb77705810797c4b847dbd330d9c732ffddba14fb435470567a7a3f46cdc

與前面的示例不同,這次不會看到很多文字,而只獲得新創建的容器的 ID。

提供選項的順序並不重要。 如果將 --publish 選項放在 --detach 選項之前,效果相同。

使用 run 命令時必須記住的一件事是鏡像名稱必須最後出現。如果在鏡像名稱後放置任何內容,則將其作為參數傳遞給容器入口點(在在容器內執行命令小節做了解釋),可能會導致意外情況。

怎樣列表展示容器

container ls 命令可用於列出當前正在運行的容器。執行以下命令:

docker container ls

# CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS              PORTS                  NAMES
# 9f21cb777058        fhsinchy/hello-dock   "/docker-entrypoint.…"   5 seconds ago       Up 5 seconds        0.0.0.0:8080->80/tcp   gifted_sammet

一個名為 gifted_sammet 的容器正在運行。它是在 5 seconds ago 前創建的,狀態為 Up 5 seconds,這表明自創建以來,該容器一直運行良好。

CONTAINER ID 為 9f21cb777058,這是完整容器 ID 的前 12 個字符。完整的容器 ID 是 9f21cb77705810797c4b847dbd330d9c732ffddba14fb435470567a7a3f46cdc,該字符長 64 個字符。在上一節中 docker container run 命令行的輸的就是完整的容器 ID 。

列表的 PORTS 列下,本地網絡的端口 8080 指向容器內的端口 80。name gifted_sammet 是由 Docker 生成的,可能與你的計算機的不同。

container ls 命令僅列出系統上當前正在運行的容器。為了列出過去運行的所有容器,可以使用 --all 或 -a 選項。

docker container ls --all

# CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS                     PORTS                  NAMES
# 9f21cb777058        fhsinchy/hello-dock   "/docker-entrypoint.…"   2 minutes ago       Up 2 minutes               0.0.0.0:8080->80/tcp   gifted_sammet
# 6cf52771dde1        fhsinchy/hello-dock   "/docker-entrypoint.…"   3 minutes ago       Exited (0) 3 minutes ago                          reverent_torvalds
# 128ec8ceab71        hello-world           "/hello"                 4 minutes ago       Exited (0) 4 minutes ago                          exciting_chebyshev

如你所見,列表 reverent_torvalds 中的第二個容器是較早創建的,並以狀態代碼 0 退出,這表明在容器運行期間未產生任何錯誤。

怎樣命名或者重命名一個容器

默認情況下,每個容器都有兩個標識符。 如下:

  • CONTAINER ID – 64 個字符的隨機字符串。
  • NAME – 兩個隨機詞的組合,下劃線連接。

基於這兩個隨機標識符來引用容器非常不方便。如果可以使用自定義的名稱來引用容器,那就太好了。

可以使用 --name 選項來命名容器。要使用名為 hello-dock-container 的 fhsinchy/hello-dock 鏡像運行另一個容器,可以執行以下命令:

docker container run --detach --publish 8888:80 --name hello-dock-container fhsinchy/hello-dock

# b1db06e400c4c5e81a93a64d30acc1bf821bed63af36cab5cdb95d25e114f5fb

本地網絡上的 8080 端口被 gifted_sammet 容器(在上一小節中創建的容器)占用了。這就是為什麼必須使用其他端口號(例如 8888)的原因。要進行驗證,執行 container ls 命令:

docker container ls

# CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS              PORTS                  NAMES
# b1db06e400c4        fhsinchy/hello-dock   "/docker-entrypoint.…"   28 seconds ago      Up 26 seconds       0.0.0.0:8888->80/tcp   hello-dock-container
# 9f21cb777058        fhsinchy/hello-dock   "/docker-entrypoint.…"   4 minutes ago       Up 4 minutes        0.0.0.0:8080->80/tcp   gifted_sammet

一個名為 hello-dock-container 的新容器已經啟動。

甚至可以使用 container rename 命令來重命名舊容器。該命令的語法如下:

docker container rename <container identifier> <new name>

要將 gifted_sammet 容器重命名為 hello-dock-container-2,可以執行以下命令:

docker container rename gifted_sammet hello-dock-container-2

該命令不會產生任何輸出,但是可以使用 container ls 命令來驗證是否已進行更改。 rename 命令不僅適用於處於運行狀態的容器和還適用於處於停止狀態的容器。

怎樣停止或者殺死運行中的容器

可以通過簡單地關閉終端窗口或單擊 ctrl + c 來停止在前台運行的容器。但是,不能以相同方式停止在後台運行的容器。

有兩個命令可以完成此任務。 第一個是 container stop 命令。該命令的通用語法如下:

docker container stop <container identifier>

其中 container identifier 可以是容器的 ID 或名稱。

應該還記得上一節中啟動的容器。它仍在後台運行。使用 docker container ls 獲取該容器的標識符(在本演示中,我將使用 hello-dock-container 容器)。現在執行以下命令來停止容器:

docker container stop hello-dock-container

# hello-dock-container

如果使用 name 作為標識符,則 name 將作為輸出返回。stop 命令通過發送信號SIGTERM 來正常關閉容器。如果容器在一定時間內沒有停止運行,則會發出 SIGKILL 信號,該信號會立即關閉容器。

如果要發送 SIGKILL 信號而不是 SIGTERM 信號,則可以改用 container kill 命令。container kill 命令遵循與 stop 命令相同的語法。

docker container kill hello-dock-container-2

# hello-dock-container-2

怎樣重新啟動容器

當我說重啟時,我指的如下是兩種情況:

  • 重新啟動先前已停止或終止的容器。
  • 重新啟動正在運行的容器。

正如上一小節中學到的,停止的容器保留在系統中。如果需要,可以重新啟動它們。container start 命令可用於啟動任何已停止或終止的容器。該命令的語法如下:

docker container start <container identifier>

可以通過執行 container ls --all 命令來獲取所有容器的列表,然後尋找狀態為 Exited 的容器。

docker container ls --all

# CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS                        PORTS               NAMES
# b1db06e400c4        fhsinchy/hello-dock   "/docker-entrypoint.…"   3 minutes ago       Exited (0) 47 seconds ago                         hello-dock-container
# 9f21cb777058        fhsinchy/hello-dock   "/docker-entrypoint.…"   7 minutes ago       Exited (137) 17 seconds ago                       hello-dock-container-2
# 6cf52771dde1        fhsinchy/hello-dock   "/docker-entrypoint.…"   7 minutes ago       Exited (0) 7 minutes ago                          reverent_torvalds
# 128ec8ceab71        hello-world           "/hello"                 9 minutes ago       Exited (0) 9 minutes ago                          exciting_chebyshev

現在要重新啟動 hello-dock-container 容器,可以執行以下命令:

docker container start hello-dock-container

# hello-dock-container

現在,可以使用 container ls 命令查看正在運行的容器列表,以確保該容器正在運行。

默認情況下,container start 命令以分離模式啟動容器,並保留之前進行的端口配置。因此,如果現在訪問 http://127.0.0.1:8080,應該能夠像以前一樣訪問 hello-dock 應用程序。

現在,在想重新啟動正在運行的容器,可以使用 container restart 命令。container restart 命令遵循與 container start 命令完全相同的語法。

docker container restart hello-dock-container-2

# hello-dock-container-2

這兩個命令之間的主要區別在於,container restart 命令嘗試停止目標容器,然後再次啟動它,而 start 命令只是啟動一個已經停止的容器。

在容器停止的情況下,兩個命令完全相同。但是如果容器正在運行,則必須使用container restart 命令。

怎樣創建而不運行容器

到目前為止,在本節中,已經使用 container run 命令啟動了容器,該命令實際上是兩個單獨命令的組合。這兩個命令如下:

  • container create 命令從給定的鏡像創建一個容器。
  • container start 命令將啟動一個已經創建的容器。

現在,要使用這兩個命令執行運行容器部分中顯示的演示,可以執行以下操作 :

docker container create --publish 8080:80 fhsinchy/hello-dock

# 2e7ef5098bab92f4536eb9a372d9b99ed852a9a816c341127399f51a6d053856

docker container ls --all

# CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS              PORTS               NAMES
# 2e7ef5098bab        fhsinchy/hello-dock   "/docker-entrypoint.…"   30 seconds ago      Created                                 hello-dock

通過 container ls --all 命令的輸出可以明顯看出,已經使用 fhsinchy/hello-dock 鏡像創建了一個名稱為 hello-dock 的容器。 容器的 STATUS 目前處於 Created 狀態,並且鑒於其未運行,因此不使用 --all 選項就不會列出該容器。

一旦創建了容器,就可以使用 container start 命令來啟動它。

docker container start hello-dock

# hello-dock

docker container ls

# CONTAINER ID        IMAGE                 COMMAND                  CREATED              STATUS              PORTS                  NAMES
# 2e7ef5098bab        fhsinchy/hello-dock   "/docker-entrypoint.…"   About a minute ago   Up 29 seconds       0.0.0.0:8080->80/tcp   hello-dock

容器 STATUS 已從 Created 更改為 Up 29 seconds,這表明容器現在處於運行狀態。端口配置也顯示在以前為空的 PORTS 列中。

盡管可以在大多數情況下使用 container run 命令,但本書稍後還會有一些情況要求使用 container create 命令。

怎樣移除掛起的容器

如你所見,已被停止或終止的容器仍保留在系統中。這些掛起的容器可能會占用空間或與較新的容器發生沖突。

可以使用 container rm 命令刪除停止的容器。 通用語法如下:

docker container rm <container identifier>

要找出哪些容器沒有運行,使用 container ls --all 命令並查找狀態為 Exited 的容器。

docker container ls --all

# CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS                      PORTS                  NAMES
# b1db06e400c4        fhsinchy/hello-dock   "/docker-entrypoint.…"   6 minutes ago       Up About a minute           0.0.0.0:8888->80/tcp   hello-dock-container
# 9f21cb777058        fhsinchy/hello-dock   "/docker-entrypoint.…"   10 minutes ago      Up About a minute           0.0.0.0:8080->80/tcp   hello-dock-container-2
# 6cf52771dde1        fhsinchy/hello-dock   "/docker-entrypoint.…"   10 minutes ago      Exited (0) 10 minutes ago                          reverent_torvalds
# 128ec8ceab71        hello-world           "/hello"                 12 minutes ago      Exited (0) 12 minutes ago                          exciting_chebyshev

從輸出中可以看到,ID為 6cf52771dde1 和 128ec8ceab71 的容器未運行。要刪除 6cf52771dde1,可以執行以下命令:

docker container rm 6cf52771dde1

# 6cf52771dde1

可以使用 container ls 命令檢查容器是否被刪除。也可以一次刪除多個容器,方法是將其標識符一個接一個地傳遞,每個標識符之間用空格隔開。

也可以使用 container prune 命令來一次性刪除所有掛起的容器。

可以使用 container ls --all 命令檢查容器列表,以確保已刪除了掛起的容器:

docker container ls --all

# CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS              PORTS                  NAMES
# b1db06e400c4        fhsinchy/hello-dock   "/docker-entrypoint.…"   8 minutes ago       Up 3 minutes        0.0.0.0:8888->80/tcp   hello-dock-container
# 9f21cb777058        fhsinchy/hello-dock   "/docker-entrypoint.…"   12 minutes ago      Up 3 minutes        0.0.0.0:8080->80/tcp   hello-dock-container-2

如果按照本書的順序進行操作,則應該只在列表中看到 hello-dock-container 和 hello-dock-container-2。 建議停止並刪除兩個容器,然後再繼續進行下一部分。

container run 和 container start 命令還有 --rm 選項,它們表示希望容器在停止後立即被移除。 執行以下命令,使用 --rm 選項啟動另一個 hello-dock 容器:

docker container run --rm --detach --publish 8888:80 --name hello-dock-volatile fhsinchy/hello-dock

# 0d74e14091dc6262732bee226d95702c21894678efb4043663f7911c53fb79f3

可以使用 container ls 命令來驗證容器是否正在運行:

docker container ls

# CONTAINER ID   IMAGE                 COMMAND                  CREATED              STATUS              PORTS                  NAMES
# 0d74e14091dc   fhsinchy/hello-dock   "/docker-entrypoint.…"   About a minute ago   Up About a minute   0.0.0.0:8888->80/tcp   hello-dock-volatile

現在,如果停止了容器,使用 container ls --all 命令再次檢查:

docker container stop hello-dock-volatile

# hello-dock-volatile

docker container ls --all

# CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

該容器已被自動刪除。從現在開始,我將對大多數容器使用 --rm 選項。不需要的地方我會明確提到。

怎樣以交互式模式運行容器

到目前為止,只運行了 hello-world 鏡像或 fhsinchy/hello-dock 鏡像。這些鏡像用於執行非交互式的簡單程序。

好吧,鏡像並不是那麼簡單。鏡像可以將整個 Linux 發行版封裝在其中。

流行的發行版,例如 UbuntuFedora 和 Debian 都在 hub 有官方的 Docker 鏡像。編程語言,例如 pythonphp、[go](https:// hub.docker.com/_/golang) 或類似 node 和 deno 都有其官方鏡像。

這些鏡像不但僅運行某些預配置的程序。還將它們配置為默認情況下運行的 shell 程序。在鏡像是操作系統的情況下,它可以是諸如 sh 或 bash 之類的東西,在竟像是編程語言或運行時的情況下,通常是它們的默認語言的 shell。

正如可能從以前的計算機中學到的一樣,shell 是交互式程序。被配置為運行這樣的程序的鏡像是交互式鏡像。這些鏡像需要在 container run 命令中傳遞特殊的 -it 選項。

例如,如果通過執行 docker container run ubuntu 使用 ubuntu 鏡像運行一個容器,將不會發生任何事情。但是,如果使用 -it 選項執行相同的命令,會直接進入到 Ubuntu 容器內的 bash 上。

docker container run --rm -it ubuntu

# root@dbb1f56b9563:/# cat /etc/os-release
# NAME="Ubuntu"
# VERSION="20.04.1 LTS (Focal Fossa)"
# ID=ubuntu
# ID_LIKE=debian
# PRETTY_NAME="Ubuntu 20.04.1 LTS"
# VERSION_ID="20.04"
# HOME_URL="https://www.ubuntu.com/"
# SUPPORT_URL="https://help.ubuntu.com/"
# BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
# PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
# VERSION_CODENAME=focal
# UBUNTU_CODENAME=focal

從 cat /etc/os-release 命令的輸出中可以看到,我確實正在與在 Ubuntu 容器中運行的 bash 進行交互。

-it 選項提供了與容器內的程序進行交互的場景。此選項實際上是將兩個單獨的選項混在一起。

  • 選項 -i 或 --interactive 連接到容器的輸入流,以便可以將輸入發送到 bash。
  • -t 或 --tty 選項可通過分配偽 tty 來格式化展示並提供類似本機終端的體驗。

當想以交互方式運行容器時,可以使用 -it 選項。以交互式方式運行 node 鏡像,如下:

docker container run -it node

# Welcome to Node.js v15.0.0.
# Type ".help" for more information.
# > ['farhan', 'hasin', 'chowdhury'].map(name => name.toUpperCase())
# [ 'FARHAN', 'HASIN', 'CHOWDHURY' ]

任何有效的 JavaScript 代碼都可以在 node shell 中執行。除了輸入 -it,還可以輸入 --interactive --tty,效果一樣,只不過更冗長。

怎樣在容器里執行命令

在本書中初識 Docker 部分中,已經了解了在 Alpine Linux 容器內執行命令。 它是這樣的:

docker run alpine uname -a
# Linux f08dbbe9199b 5.8.0-22-generic #23-Ubuntu SMP Fri Oct 9 00:34:40 UTC 2020 x86_64 Linux

在此命令中,在 Alpine Linux 容器中執行了 uname -a 命令。像這樣的場景(要做的就是在特定的容器內執行特定的命令)非常常見。

假設想使用 base64 程序對字符串進行編碼。幾乎所有基於 Linux 或 Unix 的操作系統都可以使用此功能(但 Windows 則不可用)。

在這種情況下,可以使用 busybox 之類的鏡像快速啟動容器,然後執行命令。

使用 base64 編碼字符串的通用語法如下:

echo -n my-secret | base64

# bXktc2VjcmV0

將命令傳遞到未運行的容器的通用語法如下:

docker container run <image name> <command>

要使用 busybox 鏡像執行 base64 編碼,可以執行以下命令:

docker container run --rm busybox echo -n my-secret | base64

# bXktc2VjcmV0

這里發生的是,在 container run 命令中,鏡像名稱後傳遞的任何內容都將傳遞到鏡像的默認入口里。

入口點就像是通往鏡像的網關。除可執行鏡像外的大多數鏡像(在使用可執行鏡像小節中說明)使用 shell 或 sh 作為默認入口點。因此,任何有效的 shell 命令都可以作為參數傳遞給它們。

如何處理可執行鏡像

在上一節中,我簡要提到了可執行鏡像。這些鏡像旨在表現得像可執行程序。

以的 rmbyext 項目為例。這是一個簡單的 Python 腳本,能夠遞歸刪除給定擴展名的文件。 要了解有關該項目的更多信息,可以查看倉庫

如果同時安裝了 Git 和 Python,則可以通過執行以下命令來安裝此腳本:

pip install git+https://github.com/fhsinchy/rmbyext.git#egg=rmbyext

假設的系統上已經正確設置了 Python,則該腳本應該可以在終端的任何位置使用。使用此腳本的通用語法如下:

rmbyext <file extension>

要對其進行測試,請在一個空目錄下打開終端,並在其中創建具有不同擴展名的一些文件。可以使用 touch 命令來做到這一點。現在,計算機上有一個包含以下文件的目錄:

touch a.pdf b.pdf c.txt d.pdf e.txt

ls

# a.pdf  b.pdf  c.txt  d.pdf  e.txt

要從該目錄刪除所有 pdf 文件,可以執行以下命令:

rmbyext pdf

# Removing: PDF
# b.pdf
# a.pdf
# d.pdf

該程序的可執行鏡像能夠將文件擴展名用作參數,並像 rmbyext 程序一樣刪除它們。

fhsinchy/rmbyext 鏡像的行為類似。該鏡像包含 rmbyext 腳本的副本,並配置為在容器內的目錄 /zone上運行該腳本。

現在的問題是容器與本地系統隔離,因此在容器內運行的 rmbyext 程序無法訪問本地文件系統。因此,如果可以通過某種方式將包含 pdf 文件的本地目錄映射到容器內的 /zone 目錄,則容器應該可以訪問這些文件。

授予容器直接訪問本地文件系統的一種方法是使用綁定掛載

綁定掛載可以在本地文件系統目錄(源)與容器內另一個目錄(目標)之間形成雙向數據綁定。這樣,在目標目錄中進行的任何更改都將在源目錄上生效,反之亦然。

讓我們看一下綁定掛載的實際應用。要使用此鏡像而不是程序本身刪除文件,可以執行以下命令:

docker container run --rm -v $(pwd):/zone fhsinchy/rmbyext pdf

# Removing: PDF
# b.pdf
# a.pdf
# d.pdf

已經在命令中看到了 -v $(pwd):/zone 部分,你可能已經猜到了 -v 或 --volume 選項用於為容器創建綁定掛載。該選項可以使用三個以冒號(:)分隔的字段。該選項的通用語法如下:

--volume <local file system directory absolute path>:<container file system directory absolute path>:<read write access>

第三個字段是可選的,但必須傳遞本地目錄的絕對路徑和容器內目錄的絕對路徑。

在這里,源目錄是 /home/fhsinchy/the-zone。假設終端當前在目錄中,則 $(pwd) 將替換為包含先前提到的 .pdf 和 .txt 文件的 /home/fhsinchy/the-zone

可以在command substitution here 上了解更多信息。

--volume 或 -v 選項對 container run 以及 container create 命令均有效。我們將在接下來的部分中更詳細地探討卷,因此,如果在這里不太了解它們,請不要擔心。

常規鏡像和可執行鏡像之間的區別在於,可執行鏡像的入口點設置為自定義程序而不是 sh,在本例中為 rmbyext 程序。正如在上一小節中所學到的那樣,在 container run 命令中在鏡像名稱之後編寫的所有內容都將傳遞到鏡像的入口點。

所以最後,docker container run --rm -v $(pwd):/zone fhsinchy/rmbyext pdf 命令轉換為容器內的 rmbyext pdf 。可執行鏡像並不常見,但在某些情況下可能非常有用。

Docker 鏡像操作基礎知識

現在,已經對如何使用公開可用的鏡像運行容器有了深入的了解,是時候學習如何創建自己的鏡像了。

在本部分中,將學習創建鏡像,使用鏡像運行容器以及在線共享鏡像的基礎知識。

我建議在 Visual Studio Code 中安裝官方的 Docker Extension 。 這將提升開發效率。

怎樣創建 Docker 鏡像

正如我在初識 Docker 部分中已經解釋的那樣,鏡像是分層的自包含文件,它們充當用於創建 Docker 容器的模板。它們就像是容器的凍結的只讀副本。

為了使用程序創建鏡像,必須對要從鏡像中獲得什麼有清晰的認識。以官方 nginx 鏡像為例。只需執行以下命令即可使用該鏡像啟動容器:

docker container run --rm --detach --name default-nginx --publish 8080:80 nginx

# b379ecd5b6b9ae27c144e4fa12bdc5d0635543666f75c14039eea8d5f38e3f56

docker container ls

# CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                  NAMES
# b379ecd5b6b9        nginx               "/docker-entrypoint.…"   8 seconds ago       Up 8 seconds        0.0.0.0:8080->80/tcp   default-nginx

現在,如果在瀏覽器中訪問 http://127.0.0.1:8080,則會看到一個默認的響應頁面。

看起來不錯,但是,如果想制作一個自定義的 NGINX 鏡像,該鏡像的功能與正式鏡像完全一樣,但是由你自己創建,可行嗎?老實說,完全可行。實際上,只需這樣做。

為了制作自定義的 NGINX 鏡像,必須清楚了解鏡像的最終狀態。我認為鏡像應如下所示:

  • 該鏡像應預安裝 NGINX,可以使用程序包管理器完成該鏡像,也可以從源代碼構建該鏡像。
  • 該鏡像在運行時應自動啟動 NGINX。

很簡單如果你已經克隆了本書中鏈接的項目倉庫,請進入項目根目錄並在其中查找名為 custom-nginx 的目錄。

現在,在該目錄中創建一個名為 Dockerfile 的新文件。Dockerfile 是指令的集合,該指令會被守護程序生成鏡像。Dockerfile 的內容如下:

FROM ubuntu:latest

EXPOSE 80

RUN apt-get update && \
    apt-get install nginx -y && \
    apt-get clean && rm -rf /var/lib/apt/lists/*

CMD ["nginx", "-g", "daemon off;"]

鏡像是多層文件,在此文件中,編寫的每一行(稱為說明)都會為鏡像創建一個層。

  • 每個有效的 Dockerfile 均以 FROM 指令開頭。該指令為生成的鏡像設置基本鏡像。通過在此處將 ubuntu:latest 設置為基本鏡像,可以在自定義鏡像中使用 Ubuntu 的所有功能,因此可以使用 apt-get 命令之類的東西來輕松安裝軟件包。
  • EXPOSE 指令表示需要發布的端口。使用此指令並不意味著不需要 --publish 端口。仍然需要顯式使用 --publish 選項。該 EXPOSE 指令的工作原理類似於文檔,適用於試圖使用你的鏡像運行容器的人員。它還有一些其他用途,我將不在這里討論。
  • Dockerfile 中的 RUN 指令在容器 shell 內部執行命令。apt-get update && apt-get install nginx -y 命令檢查更新的軟件包版本並安裝 NGINX。apt-get clean && rm -rf /var/lib/apt/lists/* 命令用於清除程序包緩存,因為不希望鏡像中出現任何不必要的文件。這兩個命令是簡單的 Ubuntu 東西,沒什麼特別的。此處的 RUN 指令以 shell 形式編寫。這些也可以以 exec 形式編寫。 可以查閱官方參考了解更多信息。
  • 最後,CMD 指令為鏡像設置了默認命令。該指令以 exec 形式編寫,此處包含三個獨立的部分。這里,nginx 是指 NGINX 可執行文件。 -g 和 daemon off 是 NGINX 的選項。 在容器內將 NGINX 作為單個進程運行是一種最佳實踐,因此請使用此選項。CMD 指令也可以以 shell 形式編寫。 可以查閱官方參考了解更多信息。

既然具有有效的 Dockerfile,可以從中構建鏡像。就像與容器相關的命令一樣,可以使用以下語法來執行與鏡像相關的命令:

docker image <command> <options>

要使用剛剛編寫的 Dockerfile 構建鏡像,請在 custom-nginx 目錄中打開終端並執行以下命令:

docker image build .

# Sending build context to Docker daemon  3.584kB
# Step 1/4 : FROM ubuntu:latest
#  ---> d70eaf7277ea
# Step 2/4 : EXPOSE 80
#  ---> Running in 9eae86582ec7
# Removing intermediate container 9eae86582ec7
#  ---> 8235bd799a56
# Step 3/4 : RUN apt-get update &&     apt-get install nginx -y &&     apt-get clean && rm -rf /var/lib/apt/lists/*
#  ---> Running in a44725cbb3fa
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container a44725cbb3fa
#  ---> 3066bd20292d
# Step 4/4 : CMD ["nginx", "-g", "daemon off;"]
#  ---> Running in 4792e4691660
# Removing intermediate container 4792e4691660
#  ---> 3199372aa3fc
# Successfully built 3199372aa3fc

為了執行鏡像構建,守護程序需要兩條非常具體的信息。Dockerfile 的名稱和構建上下文。在上面執行的命令中:

  • docker image build 是用於構建鏡像的命令。守護程序在上下文中找到任何名為 Dockerfile 的文件。
  • 最後的 . 設置了此構建的上下文。上下文是指在構建過程中守護程序可以訪問的目錄。

現在要使用此鏡像運行容器,可以將 container run 命令與在構建過程中收到的鏡像 ID 結合使用。在我這里,通過上一個代碼塊中的 Successfully built 3199372aa3fc 行可以看到 id 為 3199372aa3fc

docker container run --rm --detach --name custom-nginx-packaged --publish 8080:80 3199372aa3fc

# ec09d4e1f70c903c3b954c8d7958421cdd1ae3d079b57f929e44131fbf8069a0

docker container ls

# CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                  NAMES
# ec09d4e1f70c        3199372aa3fc        "nginx -g 'daemon of…"   23 seconds ago      Up 22 seconds       0.0.0.0:8080->80/tcp   custom-nginx-packaged

要進行驗證,請訪問 http://127.0.0.1:8080 ,應該會看到默認的響應頁面。

如何標記 Docker 鏡像

就像容器一樣,可以為鏡像分配自定義標識符,而不必依賴於隨機生成的 ID。如果是鏡像,則稱為標記而不是命名。在這種情況下,使用 --tag 或 -t 選項。

該選項的通用語法如下:

--tag <image repository>:<image tag>

repository 通常指鏡像名稱,而 tag 指特定的構建或版本。

以官方 mysql 鏡像為例。如果想使用特定版本的MySQL(例如5.7)運行容器,則可以執行 docker container run mysql:5.7,其中 mysql 是鏡像 repository,5.7 是 tag。

為了用 custom-nginx:packaged 標簽標記自定義 NGINX 鏡像,可以執行以下命令:

docker image build --tag custom-nginx:packaged .

# Sending build context to Docker daemon  1.055MB
# Step 1/4 : FROM ubuntu:latest
#  ---> f63181f19b2f
# Step 2/4 : EXPOSE 80
#  ---> Running in 53ab370b9efc
# Removing intermediate container 53ab370b9efc
#  ---> 6d6460a74447
# Step 3/4 : RUN apt-get update &&     apt-get install nginx -y &&     apt-get clean && rm -rf /var/lib/apt/lists/*
#  ---> Running in b4951b6b48bb
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container b4951b6b48bb
#  ---> fdc6cdd8925a
# Step 4/4 : CMD ["nginx", "-g", "daemon off;"]
#  ---> Running in 3bdbd2af4f0e
# Removing intermediate container 3bdbd2af4f0e
#  ---> f8837621b99d
# Successfully built f8837621b99d
# Successfully tagged custom-nginx:packaged

除了現在可以將鏡像稱為 custom-nginx:packaged(而不是一些較長的隨機字符串)之外,什麼都不會改變。

如果在構建期間忘記為鏡像添加標記,或者你想更改標記,可以使用 image tag 命令執行此操作:

docker image tag <image id> <image repository>:<image tag>

## 或者 ##

docker image tag <image repository>:<image tag> <new image repository>:<new image tag>

如何刪除、列表展示鏡像

就像 container ls 命令一樣,可以使用 image ls 命令列出本地系統中的所有鏡像:

docker image ls

# REPOSITORY     TAG        IMAGE ID       CREATED         SIZE
# <none>         <none>     3199372aa3fc   7 seconds ago   132MB
# custom-nginx   packaged   f8837621b99d   4 minutes ago   132MB

可以使用 image rm 命令刪除此處列出的鏡像。通用語法如下:

docker image rm <image identifier>

標識符可以是鏡像 ID 或鏡像倉庫。 如果使用倉庫,則還必須指定標記。要刪除 custom-nginx:packaged 鏡像,可以執行以下命令:

docker image rm custom-nginx:packaged

# Untagged: custom-nginx:packaged
# Deleted: sha256:f8837621b99d3388a9e78d9ce49fbb773017f770eea80470fb85e0052beae242
# Deleted: sha256:fdc6cdd8925ac25b9e0ed1c8539f96ad89ba1b21793d061e2349b62dd517dadf
# Deleted: sha256:c20e4aa46615fe512a4133089a5cd66f9b7da76366c96548790d5bf865bd49c4
# Deleted: sha256:6d6460a744475a357a2b631a4098aa1862d04510f3625feb316358536fcd8641

還可以使用 image prune 命令來清除所有未標記的掛起的鏡像,如下所示:

docker image prune --force

# Deleted Images:
# deleted: sha256:ba9558bdf2beda81b9acc652ce4931a85f0fc7f69dbc91b4efc4561ef7378aff
# deleted: sha256:ad9cc3ff27f0d192f8fa5fadebf813537e02e6ad472f6536847c4de183c02c81
# deleted: sha256:f1e9b82068d43c1bb04ff3e4f0085b9f8903a12b27196df7f1145aa9296c85e7
# deleted: sha256:ec16024aa036172544908ec4e5f842627d04ef99ee9b8d9aaa26b9c2a4b52baa

# Total reclaimed space: 59.19MB

--force 或 -f 選項會跳過所有確認問題。也可以使用 --all 或 -a 選項刪除本地倉庫中的所有緩存鏡像。

理解 Docker 鏡像的分層

從本書的開始,我就一直在說鏡像是多層文件。在本小節中,我將演示鏡像的各個層,以及它們如何在該鏡像的構建過程中發揮重要作用。

在本演示中,我將使用上一小節的 custom-nginx:packaged 鏡像。

要可視化鏡像的多個層,可以使用 image history 命令。custom-nginx:packaged 圖像的各個層可以如下所示:

docker image history custom-nginx:packaged

# IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
# 7f16387f7307        5 minutes ago       /bin/sh -c #(nop)  CMD ["nginx" "-g" "daemon…   0B                             
# 587c805fe8df        5 minutes ago       /bin/sh -c apt-get update &&     apt-get ins…   60MB                
# 6fe4e51e35c1        6 minutes ago       /bin/sh -c #(nop)  EXPOSE 80                    0B                  
# d70eaf7277ea        17 hours ago        /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B                  
# <missing>           17 hours ago        /bin/sh -c mkdir -p /run/systemd && echo 'do…   7B                  
# <missing>           17 hours ago        /bin/sh -c [ -z "$(apt-get indextargets)" ]     0B                  
# <missing>           17 hours ago        /bin/sh -c set -xe   && echo '#!/bin/sh' > /…   811B                
# <missing>           17 hours ago        /bin/sh -c #(nop) ADD file:435d9776fdd3a1834…   72.9MB

此鏡像有八層。最上面的一層是最新的一層,當向下移動時,這些層會變老。最頂層是通常用於運行容器的那一層。

現在,讓我們仔細看看從鏡像 d70eaf7277ea 到鏡像 7f16387f7307 的所有鏡像。我將忽略 IMAGE 是 <missing> 的最下面的四層,因為它們與我們無關。

  • d70eaf7277ea 是由 /bin/sh -c #(nop) CMD ["/bin/bash"] 創建的,它指示Ubuntu 內的默認 shell 已成功加載。
  • 6fe4e51e35c1 是由 /bin/sh -c #(nop) EXPOSE 80 創建的,這是代碼中的第二條指令。
  • 587c805fe8df 是由 /bin/sh -c apt-get update && apt-get install nginx -y && apt-get clean && rm -rf /var/lib/apt/lists/* 創建的,這是代碼中的第三條指令。如果在執行此指令期間安裝了所有必需的軟件包,可以看到該鏡像的大小為 60MB
  • 最後,最上層的 7f16387f7307 是由 /bin/sh -c #(nop) CMD ["nginx", "-g", "daemon off;"] 創建的,它為該鏡像設置了默認命令。

如你所見,該鏡像由許多只讀層組成,每個層都記錄了由某些指令觸發的一組新的狀態更改。當使用鏡像啟動容器時,會在其他層之上獲得一個新的可寫層。

每次使用 Docker 時都會發生這種分層現象,這是通過一個稱為 union file system 的技術概念而得以實現的。 在這里,聯合意味著集合論中的聯合。根據 Wikipedia

它允許透明地覆蓋獨立文件系統(稱為分支)的文件和目錄,從而形成單個一致的文件系統。合並分支內具有相同路徑的目錄的內容將在新的虛擬文件系統內的單個合並目錄中一起看到。

通過利用這一概念,Docker 可以避免數據重覆,並且可以將先前創建的層用作以後構建的緩存。這樣便產生了可在任何地方使用的緊湊,有效的鏡像。

怎樣從源碼構建 NGINX

在上一小節中,了解了 FROMEXPOSERUN 和 CMD 指令。在本小節中,將學到更多有關其他指令的信息。

在本小節中,將再次創建一個自定義的 NGINX 鏡像。但是,不同之處在於,將從源代碼構建 NGINX,而不是像上一個示例那樣使用諸如 apt-get 之類的軟件包管理器進行安裝。

從源代碼構建 NGINX,首先需要 NGINX 的源代碼。 如果克隆了我的項目倉庫,則會在 custom-nginx 目錄中看到一個名為 nginx-1.19.2.tar.gz 的文件。將使用此歸檔文件作為構建 NGINX 的源。

在開始編寫代碼之前,先規劃一下流程。 這次的鏡像創建過程可以分七個步驟完成。如下:

  • 獲得用於構建應用程序的基礎鏡像,例如 ubuntu
  • 在基礎鏡像上安裝必要的構建依賴項。
  • 覆制 nginx-1.19.2.tar.gz 文件到鏡像里。
  • 解壓縮壓縮包的內容並刪除壓縮包。
  • 使用 make 工具配置構建,編譯和安裝程序。
  • 刪除解壓縮的源代碼。
  • 運行nginx可執行文件。

現在有了一個規劃,讓我們開始打開舊的 Dockerfile 並按如下所示更新其內容:

FROM ubuntu:latest

RUN apt-get update && \
    apt-get install build-essential\ 
                    libpcre3 \
                    libpcre3-dev \
                    zlib1g \
                    zlib1g-dev \
                    libssl-dev \
                    -y && \
    apt-get clean && rm -rf /var/lib/apt/lists/*

COPY nginx-1.19.2.tar.gz .

RUN tar -xvf nginx-1.19.2.tar.gz && rm nginx-1.19.2.tar.gz

RUN cd nginx-1.19.2 && \
    ./configure \
        --sbin-path=/usr/bin/nginx \
        --conf-path=/etc/nginx/nginx.conf \
        --error-log-path=/var/log/nginx/error.log \
        --http-log-path=/var/log/nginx/access.log \
        --with-pcre \
        --pid-path=/var/run/nginx.pid \
        --with-http_ssl_module && \
    make && make install

RUN rm -rf /nginx-1.19.2

CMD ["nginx", "-g", "daemon off;"]

如你所見,Dockerfile 中的代碼反映了我上面提到的七個步驟。

  • FROM 指令將 Ubuntu 設置為基本映像,從而為構建任何應用程序提供了理想的環境。
  • RUN 指令安裝了從源代碼構建 NGINX 所需的標準軟件包。
  • 這里的 COPY 指令是新的東西。該指令負責在映像內覆制 nginx-1.19.2.tar.gz 文件。 COPY 指令的通用語法是 COPY <source> <destination>,其中 source 在本地文件系統中,而 destination 在鏡像內部。作為目標的 . 表示鏡像內的工作目錄,除非另有設置,否則默認為 /
  • 這里的第二條 RUN 指令使用 tar 從壓縮包中提取內容,然後將其刪除。
  • 存檔文件包含一個名為 nginx-1.19.2 的目錄,其中包含源代碼。因此,下一步,將 cd 進入該目錄並執行構建過程。 可以閱讀 How to Install Software from Source Code… and Remove it Afterwards 文章,以了解有關該主題的更多信息。
  • 構建和安裝完成後,使用 rm 命令刪除 nginx-1.19.2 目錄。
  • 在最後一步,像以前一樣以單進程模式啟動 NGINX。

現在,要使用此代碼構建鏡像,請執行以下命令:

docker image build --tag custom-nginx:built .

# Step 1/7 : FROM ubuntu:latest
#  ---> d70eaf7277ea
# Step 2/7 : RUN apt-get update &&     apt-get install build-essential                    libpcre3                     libpcre3-dev                     zlib1g                     zlib1g-dev                     libssl-dev                     -y &&     apt-get clean && rm -rf /var/lib/apt/lists/*
#  ---> Running in 2d0aa912ea47
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container 2d0aa912ea47
#  ---> cbe1ced3da11
# Step 3/7 : COPY nginx-1.19.2.tar.gz .
#  ---> 7202902edf3f
# Step 4/7 : RUN tar -xvf nginx-1.19.2.tar.gz && rm nginx-1.19.2.tar.gz
 ---> Running in 4a4a95643020
### LONG EXTRACTION STUFF GOES HERE ###
# Removing intermediate container 4a4a95643020
#  ---> f9dec072d6d6
# Step 5/7 : RUN cd nginx-1.19.2 &&     ./configure         --sbin-path=/usr/bin/nginx         --conf-path=/etc/nginx/nginx.conf         --error-log-path=/var/log/nginx/error.log         --http-log-path=/var/log/nginx/access.log         --with-pcre         --pid-path=/var/run/nginx.pid         --with-http_ssl_module &&     make && make install
#  ---> Running in b07ba12f921e
### LONG CONFIGURATION AND BUILD STUFF GOES HERE ###
# Removing intermediate container b07ba12f921e
#  ---> 5a877edafd8b
# Step 6/7 : RUN rm -rf /nginx-1.19.2
#  ---> Running in 947e1d9ba828
# Removing intermediate container 947e1d9ba828
#  ---> a7702dc7abb7
# Step 7/7 : CMD ["nginx", "-g", "daemon off;"]
#  ---> Running in 3110c7fdbd57
# Removing intermediate container 3110c7fdbd57
#  ---> eae55f7369d3
# Successfully built eae55f7369d3
# Successfully tagged custom-nginx:built

這段代碼還不錯,但是我們可以在某些地方進行改進。

  • 可以使用 ARG 指令創建自變量,而不是像 nginx-1.19.2.tar.gz 這樣的文件名進行硬編碼。這樣,只需更改參數即可更改版本或文件名。
  • 可以讓守護程序在構建過程中下載文件,而不是手動下載存檔。還有另一種類似於COPY 的指令,稱為 ADD 指令,該指令能夠從互聯網添加文件。

打開 Dockerfile 文件,並按如下所示更新其內容:

FROM ubuntu:latest

RUN apt-get update && \
    apt-get install build-essential\ 
                    libpcre3 \
                    libpcre3-dev \
                    zlib1g \
                    zlib1g-dev \
                    libssl-dev \
                    -y && \
    apt-get clean && rm -rf /var/lib/apt/lists/*

ARG FILENAME="nginx-1.19.2"
ARG EXTENSION="tar.gz"

ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .

RUN tar -xvf ${FILENAME}.${EXTENSION} && rm ${FILENAME}.${EXTENSION}

RUN cd ${FILENAME} && \
    ./configure \
        --sbin-path=/usr/bin/nginx \
        --conf-path=/etc/nginx/nginx.conf \
        --error-log-path=/var/log/nginx/error.log \
        --http-log-path=/var/log/nginx/access.log \
        --with-pcre \
        --pid-path=/var/run/nginx.pid \
        --with-http_ssl_module && \
    make && make install

RUN rm -rf /${FILENAME}}

CMD ["nginx", "-g", "daemon off;"]

該代碼幾乎與先前的代碼塊相同,除了在第 13、14 行有一條名為 ARG 的新指令,以及在第 16 行用法了 ADD 指令。有關更新代碼的說明如下:

  • ARG 指令可以像其他語言一樣聲明變量。以後可以使用 ${argument name} 語法訪問這些變量或參數。在這里,我將文件名 nginx-1.19.2 和文件擴展名 tar.gz 放在了兩個單獨的參數中。這樣,我只需在一個地方進行更改就可以在 NGINX 的較新版本或存檔格式之間進行切換。在上面的代碼中,我向變量添加了默認值。變量值也可以作為 image build 命令的選項傳遞。你可以查閱官方參考了解更多詳細信息。
  • 在 ADD 指令中,我使用上面聲明的參數動態形成了下載 URL。https://nginx.org/download/${FILENAME}.${EXTENSION} 行將在構建過程生成類似於 https://nginx.org/download/nginx-1.19.2.tar.gz 的內容。可以通過一次更改文件版本或擴展名的方式來更改文件版本或擴展名,這里要使用 ARG 指令。
  • 默認情況下,ADD 指令不會提取從互聯網獲取的文件,因此在第18行使用了 tar

其余代碼幾乎不變。 現在應該可以自己理解參數的用法。最後,讓我們嘗試從此更新的代碼構建鏡像。

docker image build --tag custom-nginx:built .

# Step 1/9 : FROM ubuntu:latest
#  ---> d70eaf7277ea
# Step 2/9 : RUN apt-get update &&     apt-get install build-essential                    libpcre3                     libpcre3-dev                     zlib1g                     zlib1g-dev                     libssl-dev                     -y &&     apt-get clean && rm -rf /var/lib/apt/lists/*
#  ---> cbe1ced3da11
### LONG INSTALLATION STUFF GOES HERE ###
# Step 3/9 : ARG FILENAME="nginx-1.19.2"
#  ---> Running in 33b62a0e9ffb
# Removing intermediate container 33b62a0e9ffb
#  ---> fafc0aceb9c8
# Step 4/9 : ARG EXTENSION="tar.gz"
#  ---> Running in 5c32eeb1bb11
# Removing intermediate container 5c32eeb1bb11
#  ---> 36efdf6efacc
# Step 5/9 : ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .
# Downloading [==================================================>]  1.049MB/1.049MB
#  ---> dba252f8d609
# Step 6/9 : RUN tar -xvf ${FILENAME}.${EXTENSION} && rm ${FILENAME}.${EXTENSION}
#  ---> Running in 2f5b091b2125
### LONG EXTRACTION STUFF GOES HERE ###
# Removing intermediate container 2f5b091b2125
#  ---> 2c9a325d74f1
# Step 7/9 : RUN cd ${FILENAME} &&     ./configure         --sbin-path=/usr/bin/nginx         --conf-path=/etc/nginx/nginx.conf         --error-log-path=/var/log/nginx/error.log         --http-log-path=/var/log/nginx/access.log         --with-pcre         --pid-path=/var/run/nginx.pid         --with-http_ssl_module &&     make && make install
#  ---> Running in 11cc82dd5186
### LONG CONFIGURATION AND BUILD STUFF GOES HERE ###
# Removing intermediate container 11cc82dd5186
#  ---> 6c122e485ec8
# Step 8/9 : RUN rm -rf /${FILENAME}}
#  ---> Running in 04102366960b
# Removing intermediate container 04102366960b
#  ---> 6bfa35420a73
# Step 9/9 : CMD ["nginx", "-g", "daemon off;"]
#  ---> Running in 63ee44b571bb
# Removing intermediate container 63ee44b571bb
#  ---> 4ce79556db1b
# Successfully built 4ce79556db1b
# Successfully tagged custom-nginx:built

現在可以使用 custom-nginx:built 鏡像來運行容器了。

docker container run --rm --detach --name custom-nginx-built --publish 8080:80 custom-nginx:built

# 90ccdbc0b598dddc4199451b2f30a942249d85a8ed21da3c8d14612f17eed0aa

docker container ls

# CONTAINER ID        IMAGE                COMMAND                  CREATED             STATUS              PORTS                  NAMES
# 90ccdbc0b598        custom-nginx:built   "nginx -g 'daemon of…"   2 minutes ago       Up 2 minutes        0.0.0.0:8080->80/tcp   custom-nginx-built

使用 custom-nginx:built-v2 映像的容器已成功運行。 現在可以從 http://127.0.0.1:8080 訪問該容器。

這是 NGINX 的默認響應頁面。可以訪問官方參考網站,以了解有關可用指令的更多信息。

怎樣優化 Docker 鏡像

在最後一個小節中構建的鏡像具有功能,但是沒有經過優化。為了證明我的觀點,讓我們使用 image ls 命令來查看鏡像的大小:

docker image ls

# REPOSITORY         TAG       IMAGE ID       CREATED          SIZE
# custom-nginx       built     1f3aaf40bb54   16 minutes ago   343MB

對於僅包含 NGINX 的鏡像,這太大了。 如果拉取官方鏡像並檢查其大小,會看到它很小:

docker image pull nginx:stable

# stable: Pulling from library/nginx
# a076a628af6f: Pull complete 
# 45d7b5d3927d: Pull complete 
# 5e326fece82e: Pull complete 
# 30c386181b68: Pull complete 
# b15158e9ebbe: Pull complete 
# Digest: sha256:ebd0fd56eb30543a9195280eb81af2a9a8e6143496accd6a217c14b06acd1419
# Status: Downloaded newer image for nginx:stable
# docker.io/library/nginx:stable

docker image ls

# REPOSITORY         TAG       IMAGE ID       CREATED          SIZE
# custom-nginx       built     1f3aaf40bb54   25 minutes ago   343MB
# nginx              stable    b9e1dc12387a   11 days ago      133MB

為了找出根本原因,讓我們首先看一下 Dockerfile

FROM ubuntu:latest

RUN apt-get update && \
    apt-get install build-essential\ 
                    libpcre3 \
                    libpcre3-dev \
                    zlib1g \
                    zlib1g-dev \
                    libssl-dev \
                    -y && \
    apt-get clean && rm -rf /var/lib/apt/lists/*

ARG FILENAME="nginx-1.19.2"
ARG EXTENSION="tar.gz"

ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .

RUN tar -xvf ${FILENAME}.${EXTENSION} && rm ${FILENAME}.${EXTENSION}

RUN cd ${FILENAME} && \
    ./configure \
        --sbin-path=/usr/bin/nginx \
        --conf-path=/etc/nginx/nginx.conf \
        --error-log-path=/var/log/nginx/error.log \
        --http-log-path=/var/log/nginx/access.log \
        --with-pcre \
        --pid-path=/var/run/nginx.pid \
        --with-http_ssl_module && \
    make && make install

RUN rm -rf /${FILENAME}}

CMD ["nginx", "-g", "daemon off;"]

正如在第 3 行看到的那樣,RUN 指令安裝了很多東西。盡管這些軟件包對於從源代碼構建 NGINX 是必需的,但對於運行它而言則不是必需的。

在安裝的 6 個軟件包中,只有兩個是運行 NGINX 所必需的,即 libpcre3 和 zlib1g。 因此,一個更好的主意是在構建過程完成後,卸載其他軟件包。

為此,請按如下所示更新的 Dockerfile :

FROM ubuntu:latest

EXPOSE 80

ARG FILENAME="nginx-1.19.2"
ARG EXTENSION="tar.gz"

ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .

RUN apt-get update && \
    apt-get install build-essential \ 
                    libpcre3 \
                    libpcre3-dev \
                    zlib1g \
                    zlib1g-dev \
                    libssl-dev \
                    -y && \
    tar -xvf ${FILENAME}.${EXTENSION} && rm ${FILENAME}.${EXTENSION} && \
    cd ${FILENAME} && \
    ./configure \
        --sbin-path=/usr/bin/nginx \
        --conf-path=/etc/nginx/nginx.conf \
        --error-log-path=/var/log/nginx/error.log \
        --http-log-path=/var/log/nginx/access.log \
        --with-pcre \
        --pid-path=/var/run/nginx.pid \
        --with-http_ssl_module && \
    make && make install && \
    cd / && rm -rfv /${FILENAME} && \
    apt-get remove build-essential \ 
                    libpcre3-dev \
                    zlib1g-dev \
                    libssl-dev \
                    -y && \
    apt-get autoremove -y && \
    apt-get clean && rm -rf /var/lib/apt/lists/*

CMD ["nginx", "-g", "daemon off;"]

如你所見,在第 10 行上,一條 RUN 指令正在執行所有必要的核心操作。確切的事件鏈如下:

  • 從第 10 行到第 17 行,安裝所有必需的軟件包。
  • 在第 18 行,將提取源代碼,並刪除下載的存檔。
  • 從第 19 行到第 28 行,NGINX 在系統上配置,構建和安裝。
  • 在第 29 行,從下載的檔案中提取的文件將被刪除。
  • 從第 30 行到第 36 行,所有不必要的軟件包都將被卸載並清除緩存。運行 NGINX 需要 libpcre3 和 zlib1g 包,因此我們保留了它們。

你可能會問,為什麼我要在一條 RUN 指令中做這麼多工作,而不是像我們之前那樣將它們很好地拆分成多個指令。 好吧,將它們拆分會是一個錯誤。

如果安裝了軟件包,然後按照單獨的 RUN 說明將其刪除,則它們將位於鏡像的不同層中。盡管最終鏡像不會包含已刪除的包,但是由於它們存在於組成該圖像的一層之一中,因此它們的大小仍將添加到最終鏡像中。因此,請確保在單層上進行了此類更改。

讓我們使用此 Dockerfile 來構建映像,並查看它們之間的區別。

docker image build --tag custom-nginx:built .

# Sending build context to Docker daemon  1.057MB
# Step 1/7 : FROM ubuntu:latest
#  ---> f63181f19b2f
# Step 2/7 : EXPOSE 80
#  ---> Running in 006f39b75964
# Removing intermediate container 006f39b75964
#  ---> 6943f7ef9376
# Step 3/7 : ARG FILENAME="nginx-1.19.2"
#  ---> Running in ffaf89078594
# Removing intermediate container ffaf89078594
#  ---> 91b5cdb6dabe
# Step 4/7 : ARG EXTENSION="tar.gz"
#  ---> Running in d0f5188444b6
# Removing intermediate container d0f5188444b6
#  ---> 9626f941ccb2
# Step 5/7 : ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .
# Downloading [==================================================>]  1.049MB/1.049MB
#  ---> a8e8dcca1be8
# Step 6/7 : RUN apt-get update &&     apt-get install build-essential                     libpcre3                     libpcre3-dev                     zlib1g                     zlib1g-dev                     libssl-dev                     -y &&     tar -xvf ${FILENAME}.${EXTENSION} && rm ${FILENAME}.${EXTENSION} &&     cd ${FILENAME} &&     ./configure         --sbin-path=/usr/bin/nginx         --conf-path=/etc/nginx/nginx.conf         --error-log-path=/var/log/nginx/error.log         --http-log-path=/var/log/nginx/access.log         --with-pcre         --pid-path=/var/run/nginx.pid         --with-http_ssl_module &&     make && make install &&     cd / && rm -rfv /${FILENAME} &&     apt-get remove build-essential                     libpcre3-dev                     zlib1g-dev                     libssl-dev                     -y &&     apt-get autoremove -y &&     apt-get clean && rm -rf /var/lib/apt/lists/*
#  ---> Running in e5675cad1260
### LONG INSTALLATION AND BUILD STUFF GOES HERE ###
# Removing intermediate container e5675cad1260
#  ---> dc7e4161f975
# Step 7/7 : CMD ["nginx", "-g", "daemon off;"]
#  ---> Running in b579e4600247
# Removing intermediate container b579e4600247
#  ---> 512aa6a95a93
# Successfully built 512aa6a95a93
# Successfully tagged custom-nginx:built

docker image ls

# REPOSITORY         TAG       IMAGE ID       CREATED              SIZE
# custom-nginx       built     512aa6a95a93   About a minute ago   81.6MB
# nginx              stable    b9e1dc12387a   11 days ago          133MB

如你所見,鏡像大小從 343MB 變為 81.6MB。官方鏡像是 133MB。這是一個非常優化的構建,我們可以在下一部分中進一步介紹。

擁抱 Alpine Linux

如果之前了解過 Docker,可能已經聽說了 Alpine Linux。 這是功能齊全的 Linux 發行版,就像 UbuntuDebian 或 Fedora

但是 Alpine 的好處是它是基於 musllibc 和 busybox 構建的,並且是輕量級的。最新的 ubuntu 鏡像大約為 28MB,而 alpine 僅為 2.8MB。

除了輕量之外,Alpine 還很安全,比其他發行版更適合創建容器。

盡管不像其他商業發行版那樣用戶友好,但是向 Alpine 的過渡仍然非常簡單。在本小節中,將學習有關以 Alpine 鏡像為基礎重新創建 custom-nginx 鏡像的信息。

打開的 Dockerfile 並更新其內容,如下所示:

FROM alpine:latest

EXPOSE 80

ARG FILENAME="nginx-1.19.2"
ARG EXTENSION="tar.gz"

ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .

RUN apk add --no-cache pcre zlib && \
    apk add --no-cache \
            --virtual .build-deps \
            build-base \ 
            pcre-dev \
            zlib-dev \
            openssl-dev && \
    tar -xvf ${FILENAME}.${EXTENSION} && rm ${FILENAME}.${EXTENSION} && \
    cd ${FILENAME} && \
    ./configure \
        --sbin-path=/usr/bin/nginx \
        --conf-path=/etc/nginx/nginx.conf \
        --error-log-path=/var/log/nginx/error.log \
        --http-log-path=/var/log/nginx/access.log \
        --with-pcre \
        --pid-path=/var/run/nginx.pid \
        --with-http_ssl_module && \
    make && make install && \
    cd / && rm -rfv /${FILENAME} && \
    apk del .build-deps

CMD ["nginx", "-g", "daemon off;"]

除了幾處更改外,代碼幾乎完全相同。我將列出更改並在進行過程中進行解釋:

  • 我們不使用 apt-get install 來安裝軟件包,而是使用 apk add--no-cache 選項意味著下載的軟件包將不會被緩存。同樣,我們將使用 apk del 代替 apt-get remove 來卸載軟件包。
  • apk add 命令的 --virtual 選項用於將一堆軟件包捆綁到單個虛擬軟件包中,以便於管理。僅用於構建程序所需的軟件包被標記為 .build-deps,然後通過執行 apk del .build-deps 命令在第 29 行將其刪除。可以在官方文檔中了解有關 virtuals 的更多信息。
  • 軟件包名稱在這里有些不同。通常,每個 Linux 發行版都有其軟件包倉庫,可供在其中搜索軟件包的每個人使用。如果你知道某項任務所需的軟件包,則可以直接轉到指定發行版的倉庫的並進行搜索。可以 在此處了解 Alpine Linux軟件包

現在使用此 Dockerfile 構建一個新鏡像,並查看文件大小的差異:

docker image build --tag custom-nginx:built .

# Sending build context to Docker daemon  1.055MB
# Step 1/7 : FROM alpine:latest
#  ---> 7731472c3f2a
# Step 2/7 : EXPOSE 80
#  ---> Running in 8336cfaaa48d
# Removing intermediate container 8336cfaaa48d
#  ---> d448a9049d01
# Step 3/7 : ARG FILENAME="nginx-1.19.2"
#  ---> Running in bb8b2eae9d74
# Removing intermediate container bb8b2eae9d74
#  ---> 87ca74f32fbe
# Step 4/7 : ARG EXTENSION="tar.gz"
#  ---> Running in aa09627fe48c
# Removing intermediate container aa09627fe48c
#  ---> 70cb557adb10
# Step 5/7 : ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .
# Downloading [==================================================>]  1.049MB/1.049MB
#  ---> b9790ce0c4d6
# Step 6/7 : RUN apk add --no-cache pcre zlib &&     apk add --no-cache             --virtual .build-deps             build-base             pcre-dev             zlib-dev             openssl-dev &&     tar -xvf ${FILENAME}.${EXTENSION} && rm ${FILENAME}.${EXTENSION} &&     cd ${FILENAME} &&     ./configure         --sbin-path=/usr/bin/nginx         --conf-path=/etc/nginx/nginx.conf         --error-log-path=/var/log/nginx/error.log         --http-log-path=/var/log/nginx/access.log         --with-pcre         --pid-path=/var/run/nginx.pid         --with-http_ssl_module &&     make && make install &&     cd / && rm -rfv /${FILENAME} &&     apk del .build-deps
#  ---> Running in 0b301f64ffc1
### LONG INSTALLATION AND BUILD STUFF GOES HERE ###
# Removing intermediate container 0b301f64ffc1
#  ---> dc7e4161f975
# Step 7/7 : CMD ["nginx", "-g", "daemon off;"]
#  ---> Running in b579e4600247
# Removing intermediate container b579e4600247
#  ---> 3e186a3c6830
# Successfully built 3e186a3c6830
# Successfully tagged custom-nginx:built

docker image ls

# REPOSITORY         TAG       IMAGE ID       CREATED         SIZE
# custom-nginx       built     3e186a3c6830   8 seconds ago   12.8MB

ubuntu 版本為 81.6MB,而 alpine 版本已降至 12.8MB,這是一個巨大的進步。除了 apk 軟件包管理器外,Alpine 和 Ubuntu 還有一些其他的區別,但是沒什麼大不了的。遇到困難,可以搜索互聯網。

怎樣創建可執行 Docker 鏡像

在上一節中,使用了 fhsinchy/rmbyext 鏡像。在本節中,將學習如何制作這樣的可執行鏡像。

首先,打開本書隨附倉庫的目錄。rmbyext 應用程序的代碼位於同名子目錄中。

在開始使用 Dockerfile 之前,請花一點時間來規劃最終的輸出。我認為應該是這樣的:

  • 該鏡像應預安裝 Python。
  • 它應該包含 rmbyext 腳本的副本。
  • 應該在將要執行腳本的地方設置一個工作目錄。
  • 應該將 rmbyext 腳本設置為入口點,以便鏡像可以將擴展名用作參數。

要構建上面提到的鏡像,請執行以下步驟:

  • 獲得可以運行 Python 腳本基礎鏡像,例如 python
  • 將工作目錄設置為易於訪問的目錄。
  • 安裝 Git,以便可以從我的 GitHub 倉庫中安裝腳本。
  • 使用 Git 和 pip 安裝腳本。
  • 刪除不必要的構建軟件包。
  • 將 rmbyext 設置為該圖像的入口點。

現在在 rmbyext 目錄中創建一個新的 Dockerfile,並將以下代碼放入其中:

FROM python:3-alpine

WORKDIR /zone

RUN apk add --no-cache git && \
    pip install git+https://github.com/fhsinchy/rmbyext.git
    apk del git

ENTRYPOINT [ "rmbyext" ]

該文件中的指令說明如下:

  • FROM 指令將 python 設置為基本鏡像,從而為運行 Python 腳本提供了理想的環境。3-alpine 標記表示需要 Python 3 的 Alpine 版本。
  • 這里的 WORKDIR 指令將默認工作目錄設置為 /zone。這里的工作目錄名稱完全是隨機的。我發現 zone 是一個合適的名稱,你也可以換成任何你想要的名稱。
  • 假設從 GitHub 安裝了 rmbyext 腳本,則 git 是安裝時的依賴項。第 5 行的 RUN 指令先安裝 git,然後使用 Git 和 pip 安裝 rmbyext 腳本。之後也刪除了git
  • 最後,在第 9 行,ENTRYPOINT 指令將 rmbyext 腳本設置為該鏡像的入口點。

在整個文件中,第 9 行是將這個看似正常的鏡像轉換為可執行鏡像的關鍵。現在要構建鏡像,可以執行以下命令:

docker image build --tag rmbyext .

# Sending build context to Docker daemon  2.048kB
# Step 1/4 : FROM python:3-alpine
# 3-alpine: Pulling from library/python
# 801bfaa63ef2: Already exists 
# 8723b2b92bec: Already exists 
# 4e07029ccd64: Already exists 
# 594990504179: Already exists 
# 140d7fec7322: Already exists 
# Digest: sha256:7492c1f615e3651629bd6c61777e9660caa3819cf3561a47d1d526dfeee02cf6
# Status: Downloaded newer image for python:3-alpine
#  ---> d4d4f50f871a
# Step 2/4 : WORKDIR /zone
#  ---> Running in 454374612a91
# Removing intermediate container 454374612a91
#  ---> 7f7e49bc98d2
# Step 3/4 : RUN apk add --no-cache git &&     pip install git+https://github.com/fhsinchy/rmbyext.git#egg=rmbyext &&     apk del git
#  ---> Running in 27e2e96dc95a
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container 27e2e96dc95a
#  ---> 3c7389432e36
# Step 4/4 : ENTRYPOINT [ "rmbyext" ]
#  ---> Running in f239bbea1ca6
# Removing intermediate container f239bbea1ca6
#  ---> 1746b0cedbc7
# Successfully built 1746b0cedbc7
# Successfully tagged rmbyext:latest

docker image ls

# REPOSITORY         TAG        IMAGE ID       CREATED         SIZE
# rmbyext            latest     1746b0cedbc7   4 minutes ago   50.9MB

這里在鏡像名稱之後沒有提供任何標簽,因此默認情況下該鏡像已被標記為 latest。 應該能夠像在上一節中看到的那樣運行該鏡像。請記住,參考你設置的實際鏡像名稱,而不是這里的 fhsinchy/rmbyext

現在知道如何制作鏡像了,是時候與全世界分享它們了。在線共享鏡像很容易。所需要做的就是在任何在線倉庫中注冊一個帳戶。在此處我將使用 Docker Hub

導航到 Sign Up 頁面並創建一個免費帳戶。一個免費帳戶可托管無限的公共倉庫和一個私有倉庫。

創建帳戶後,需要使用 Docker CLI 登錄。打開終端並執行以下命令:

docker login

# Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
# Username: fhsinchy
# Password: 
# WARNING! Your password will be stored unencrypted in /home/fhsinchy/.docker/config.json.
# Configure a credential helper to remove this warning. See
# https://docs.docker.com/engine/reference/commandline/login/#credentials-store
#
# Login Succeeded

系統將提示輸入用戶名和密碼。如果輸入正確,則應該成功登錄到你的帳戶。

為了在線共享鏡像,必須對鏡像進行標記。已經在上一小節中學習了有關標記的信息。只是為了加深記憶,--tag 或 -t 選項的通用語法如下:

--tag <image repository>:<image tag>

例如,讓我們在線共享 custom-nginx 圖像。 為此,請在 custom-nginx 項目目錄中打開一個新的終端窗口。

要在線共享鏡像,必須使用 <docker hub username>/<image name>:<image tag> 語法對其進行標記。我的用戶名是 fhsinchy,因此命令如下所示:

docker image build --tag fhsinchy/custom-nginx:latest --file Dockerfile.built .

# Step 1/9 : FROM ubuntu:latest
#  ---> d70eaf7277ea
# Step 2/9 : RUN apt-get update &&     apt-get install build-essential                    libpcre3                     libpcre3-dev                     zlib1g                     zlib1g-dev                     libssl-dev                     -y &&     apt-get clean && rm -rf /var/lib/apt/lists/*
#  ---> cbe1ced3da11
### LONG INSTALLATION STUFF GOES HERE ###
# Step 3/9 : ARG FILENAME="nginx-1.19.2"
#  ---> Running in 33b62a0e9ffb
# Removing intermediate container 33b62a0e9ffb
#  ---> fafc0aceb9c8
# Step 4/9 : ARG EXTENSION="tar.gz"
#  ---> Running in 5c32eeb1bb11
# Removing intermediate container 5c32eeb1bb11
#  ---> 36efdf6efacc
# Step 5/9 : ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .
# Downloading [==================================================>]  1.049MB/1.049MB
#  ---> dba252f8d609
# Step 6/9 : RUN tar -xvf ${FILENAME}.${EXTENSION} && rm ${FILENAME}.${EXTENSION}
#  ---> Running in 2f5b091b2125
### LONG EXTRACTION STUFF GOES HERE ###
# Removing intermediate container 2f5b091b2125
#  ---> 2c9a325d74f1
# Step 7/9 : RUN cd ${FILENAME} &&     ./configure         --sbin-path=/usr/bin/nginx         --conf-path=/etc/nginx/nginx.conf         --error-log-path=/var/log/nginx/error.log         --http-log-path=/var/log/nginx/access.log         --with-pcre         --pid-path=/var/run/nginx.pid         --with-http_ssl_module &&     make && make install
#  ---> Running in 11cc82dd5186
### LONG CONFIGURATION AND BUILD STUFF GOES HERE ###
# Removing intermediate container 11cc82dd5186
#  ---> 6c122e485ec8
# Step 8/9 : RUN rm -rf /${FILENAME}}
#  ---> Running in 04102366960b
# Removing intermediate container 04102366960b
#  ---> 6bfa35420a73
# Step 9/9 : CMD ["nginx", "-g", "daemon off;"]
#  ---> Running in 63ee44b571bb
# Removing intermediate container 63ee44b571bb
#  ---> 4ce79556db1b
# Successfully built 4ce79556db1b
# Successfully tagged fhsinchy/custom-nginx:latest

在此命令中,fhsinchy/custom-nginx 是鏡像倉庫,而 latest 是標簽。鏡像名稱可以是任何名稱,上傳鏡像後即無法更改。可以隨時更改標簽,該標簽通常反映軟件的版本或其他類型的內部版本。

以 node 鏡像為例。node:lts 鏡像是指 Node.js 的長期支持版本,而 node:lts-alpine 版本是指為 Alpine Linux 構建的 Node.js 版本,它比常規版本小得多。

如果你給鏡像添加任何標簽,則會將其自動標記為 latest。但這並不意味著 latest 標簽將始終引用最新版本。如果出於某種原因,將鏡像的較舊版本明確標記為 latest,則 Docker 將不會做出任何額外的工作來進行交叉檢查。

生成鏡像後,可以通過執行以下命令來上傳鏡像:

docker image push <image repository>:<image tag>

因此,在我這里,命令如下所示:

docker image push fhsinchy/custom-nginx:latest

# The push refers to repository [docker.io/fhsinchy/custom-nginx]
# 4352b1b1d9f5: Pushed 
# a4518dd720bd: Pushed 
# 1d756dc4e694: Pushed 
# d7a7e2b6321a: Pushed 
# f6253634dc78: Mounted from library/ubuntu 
# 9069f84dbbe9: Mounted from library/ubuntu 
# bacd3af13903: Mounted from library/ubuntu 
# latest: digest: sha256:ffe93440256c9edb2ed67bf3bba3c204fec3a46a36ac53358899ce1a9eee497a size: 1788

根據鏡像大小,上傳可能需要一些時間。完成後,應該可以在中心配置文件頁面中找到該鏡像。

如何容器化 JavaScript 應用程序

現在,已經了解了創建鏡像的知識,是時候做一些更相關的工作了。

在本小節中,將使用在之前小節上使用的 fhsinchy/hello-dock 鏡像的源代碼。在容器化這個非常簡單的應用的過程中,介紹了 volumes 和多階段構建,這是 Docker 中兩個很重要的概念。

如何編寫開發 Dockerfile

首先,打開用來克隆本書隨附倉庫的目錄。hello-dock 應用程序的代碼位於具有相同名稱的子目錄中。

這是一個非常簡單的 JavaScript 項目,由 vitejs/vite 項目構建。不過,請不要擔心,無需了解 JavaScript 或 vite 即可學習本小節。了解 Node.js 和 npm 就足夠了。

與上一部分中完成的其他項目一樣,將從制定如何運行該應用程序的規劃開始。如下:

  • 獲得可以運行 JavaScript 應用程序的基礎鏡像,例如 node
  • 在鏡像內設置默認的工作目錄。
  • 將 package.json 文件覆制到鏡像中。
  • 安裝必要的依賴項。
  • 覆制其余的項目文件。
  • 通過執行 npm run dev 命令來啟動 vite 開發服務。

該規劃應始終來由應用程序的開發人員制定。如果你是開發人員,那麼應該已經對如何運行此應用程序有正確的了解。

現在,如果將上述計劃放入 Dockerfile.dev 中,則該文件應如下所示:

FROM node:lts-alpine

EXPOSE 3000

USER node

RUN mkdir -p /home/node/app

WORKDIR /home/node/app

COPY ./package.json .
RUN npm install

COPY . .

CMD [ "npm", "run", "dev" ]

此代碼的說明如下:

  • 這里的 FROM 指令將官方的 Node.js 鏡像設置為基礎鏡像,從而可以運行 JavaScript 應用。lts-alpine 標簽代表鏡像要使用針對 Alpine 的長期支持版本。 可以在 node 頁面上找到該鏡像的所有標簽和其它必要的文檔。
  • USER 指令將鏡像的默認用戶設置為 node。 默認情況下,Docker 以 root 用戶身份運行容器。 但是根據 Docker and Node.js Best Practices,這有安全隱患。因此,最好是盡可能以非 root 用戶身份運行。node 鏡像附帶一個名為 node 的非 root 用戶,可以使用 USER 指令將其設置為默認用戶。
  • RUN mkdir -p /home/node/app 指令在 node 用戶的主目錄內創建一個名為 app 的目錄。默認情況下,Linux 中任何非 root 用戶的主目錄通常是 /home/<user name>
  • 然後,WORKDIR 指令將默認工作目錄設置為新創建的 /home/node/app 目錄。 默認情況下,任何鏡像的工作目錄都是根目錄。如果不希望在根目錄中放置不必要的文件,可以將默認工作目錄更改為更合理的目錄,例如 /home/node/app 或你喜歡的任何目錄。該工作目錄將適用於任何連續的 COPYADDRUN 和 CMD 指令。
  • 此處的 COPY 指令覆制了 package.json文件,該文件包含有關此應用程序所有必需依賴項的信息。RUN 指令執行 npm install 命令,這是在 Node.js 項目中使用 package.json 文件安裝依賴項的默認命令。 最後的 . 代表工作目錄。
  • 第二條 COPY 指令將其余內容從主機文件系統的當前目錄(.)覆制到鏡像內的工作目錄(.)。
  • 最後,這里的 CMD 指令為該鏡像設置了默認命令,即以 exec 形式編寫的 npm run dev
  • 默認情況下,vite 開發服務器在端口 3000 上運行,最好添加一個 EXPOSE 命令。

現在,要由此 Dockerfile.dev 構建像鏡,可以執行以下命令:

docker image build --file Dockerfile.dev --tag hello-dock:dev .

# Step 1/7 : FROM node:lts
#  ---> b90fa0d7cbd1
# Step 2/7 : EXPOSE 3000
#  ---> Running in 722d639badc7
# Removing intermediate container 722d639badc7
#  ---> e2a8aa88790e
# Step 3/7 : WORKDIR /app
#  ---> Running in 998e254b4d22
# Removing intermediate container 998e254b4d22
#  ---> 6bd4c42892a4
# Step 4/7 : COPY ./package.json .
#  ---> 24fc5164a1dc
# Step 5/7 : RUN npm install
#  ---> Running in 23b4de3f930b
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container 23b4de3f930b
#  ---> c17ecb19a210
# Step 6/7 : COPY . .
#  ---> afb6d9a1bc76
# Step 7/7 : CMD [ "npm", "run", "dev" ]
#  ---> Running in a7ff529c28fe
# Removing intermediate container a7ff529c28fe
#  ---> 1792250adb79
# Successfully built 1792250adb79
# Successfully tagged hello-dock:dev

如果文件名不是 Dockerfile,則必須使用 --file 選項顯式傳遞文件名。 通過執行以下命令,可以使用此鏡像運行容器:

docker container run \
    --rm \
    --detach \
    --publish 3000:3000 \
    --name hello-dock-dev \
    hello-dock:dev

# 21b9b1499d195d85e81f0e8bce08f43a64b63d589c5f15cbbd0b9c0cb07ae268

現在訪問 http://127.0.0.1:3000,可以看到 hello-dock 應用程序。

恭喜你在容器內運行了你的第一個實際應用程序。剛剛編寫的代碼還可以,但是它存在一個大問題,可以在某些地方進行改進。讓我們先從問題開始。

如何在 Docker 中使用 Bind Mounts

如果你以前使用過任何前端 JavaScript 框架,則應該知道這些框架中的開發服務器通常帶有熱重載功能。也就是說,如果對代碼進行更改,服務器將重新加載,並自動反映立即進行的所有更改。

但是,如果現在對代碼進行任何更改,將不會在瀏覽器中運行任何應用程序。這是因為正在更改本地文件系統中的代碼,但是在瀏覽器中看到的應用程序位於容器文件系統中。

要解決此問題,可以再次使用 綁定掛載。 使用綁定掛載,可以輕松地在容器內安裝本地文件系統目錄。綁定掛載可以直接從容器內部引用本地文件系統,而無需覆制本地文件。

這樣,對本地源代碼所做的任何更改都會及時反映在容器內部,從而觸發 vite 開發服務器的熱重載功能。對容器內部文件系統所做的更改也將反映在本地文件系統上。

已經在使用可執行鏡像小節中學習到,可以對 container run 或 container start 命令使用 --volume 或 -v 選項創建綁定掛載。 回顧一下,通用語法如下:

--volume <local file system directory absolute path>:<container file system directory absolute path>:<read write access>

停止先前啟動的 hello-dock-dev 容器,並通過執行以下命令來啟動新的容器:

docker container run \
    --rm \
    --publish 3000:3000 \
    --name hello-dock-dev \
    --volume $(pwd):/home/node/app \
    hello-dock:dev

# sh: 1: vite: not found
# npm ERR! code ELIFECYCLE
# npm ERR! syscall spawn
# npm ERR! file sh
# npm ERR! errno ENOENT
# npm ERR! hello-dock@0.0.0 dev: `vite`
# npm ERR! spawn ENOENT
# npm ERR!
# npm ERR! Failed at the hello-dock@0.0.0 dev script.
# npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
# npm WARN Local package.json exists, but node_modules missing, did you mean to install?

請記住,我省略了 --detach 選項,這只是說明一個非常重要的觀點。如你所見,該應用程序現在根本沒有運行。

這是因為盡管 volume 解決了熱重載的問題,但它引入了另一個問題。如果你以前有過使用 Node.js 的經驗,你可能會知道 Node.js 項目的依賴項位於項目根目錄的 node_modules 目錄中。

現在,將項目根目錄作為容器中的 volume 安裝在本地文件系統上,容器中的內容將被包含所有依賴項的 node_modules 目錄替換。 這意味著 vite 軟件包不見了。

如何在 Docker 中使用匿名卷

可以使用匿名卷解決此問題。匿名卷除了無需在此處指定源目錄之外,與綁定掛載相同。 創建匿名卷的通用語法如下:

--volume <container file system directory absolute path>:<read write access>

因此,用兩個卷啟動 hello-dock 容器的最終命令應如下:

docker container run \
    --rm \
    --detach \
    --publish 3000:3000 \
    --name hello-dock-dev \
    --volume $(pwd):/home/node/app \
    --volume /home/node/app/node_modules \
    hello-dock:dev

# 53d1cfdb3ef148eb6370e338749836160f75f076d0fbec3c2a9b059a8992de8b

在這里,Docker 將從容器內部獲取整個 node_modules 目錄,並將其存放在主機文件系統上由 Docker 守護程序管理的其他目錄中,並將該目錄作為 node_modules 掛載在容器中。

如何在 Docker 中執行多階段構建

到目前為止,在本節中,已經構建了用於在開發模式下運行 JavaScript 應用程序的鏡像。現在,如果要在生產模式下構建鏡像,會有一些新的挑戰。

在開發模式下,npm run serve 命令啟動一個開發服務器,該服務器將應用程序提供給用戶。該服務器不僅提供文件,還提供熱重載功能。

在生產模式下,npm run build 命令將所有 JavaScript 代碼編譯為一些靜態 HTML、CSS 和 JavaScript 文件。要運行這些文件,不需要 node 或任何其他運行時依賴項。只需要一個像 nginx 這樣的服務器。

要在應用程序以生產模式運行時創建鏡像,可以執行以下步驟:

  • 使用 node 作為基礎鏡像並構建應用程序。
  • 在 node 鏡像中安裝 nginx 並使用它來提供靜態文件。

這種方法是完全有效的。但是問題在於,node 鏡像很大,並且它所承載的大多數內容對於靜態文件服務而言都是不必要的。解決此問題的更好方法如下:

  • 使用node圖像作為基礎並構建應用程序。
  • 將使用 node 鏡像創建的文件覆制到 nginx 映像。
  • 根據 nginx 創建最終鏡像,並丟棄所有與 node 相關的東西。

這樣,鏡像僅包含所需的文件,變得非常方便。

這種方法是一個多階段構建。要執行這樣的構建,在 hello-dock 項目目錄中創建一個新的 Dockerfile,並將以下內容放入其中:

FROM node:lts-alpine as builder

WORKDIR /app

COPY ./package.json ./
RUN npm install

COPY . .
RUN npm run build

FROM nginx:stable-alpine

EXPOSE 80

COPY --from=builder /app/dist /usr/share/nginx/html

如你所見,Dockerfile 看起來很像以前的 Dockerfile,但有一些不同之處。該文件的解釋如下:

  • 第 1 行使用 node:lts-alpine 作為基礎鏡像開始構建的第一階段。as builder 語法為此階段分配一個名稱,以便以後可以引用。
  • 從第 3 行到第 13 行,以前已經看過很多次了。實際上,RUN npm run build 命令會編譯整個應用程序,並將其存放在 /app/dist 目錄中,其中 /app 是工作目錄,/dist 是 vite 應用程序的默認輸出目錄。
  • 第 15 行使用 nginx:stable-alpine 作為基礎鏡像開始構建的第二階段。
  • NGINX 服務器默認在端口 80 上運行,因此添加了 EXPOSE 80 行。
  • 最後一行是 COPY 指令。--from=builder 部分表示要從 builder 階段覆制一些文件。之後,這是一條標準的覆制指令,其中 /app/dist 是 source,而 /usr/share/nginx/html 是 destination。 這里使用的 destination 是 NGINX 的默認站點路徑,因此放置在其中的任何靜態文件都將自動提供。

如你所見,生成的鏡像是基於 nginx 的鏡像,僅包含運行應用程序所需的文件。要構建此鏡像,請執行以下命令:

docker image build --tag hello-dock:prod .

# Step 1/9 : FROM node:lts-alpine as builder
#  ---> 72aaced1868f
# Step 2/9 : WORKDIR /app
#  ---> Running in e361c5c866dd
# Removing intermediate container e361c5c866dd
#  ---> 241b4b97b34c
# Step 3/9 : COPY ./package.json ./
#  ---> 6c594c5d2300
# Step 4/9 : RUN npm install
#  ---> Running in 6dfabf0ee9f8
# npm WARN deprecated fsevents@2.1.3: Please update to v 2.2.x
#
# > esbuild@0.8.29 postinstall /app/node_modules/esbuild
# > node install.js
#
# npm notice created a lockfile as package-lock.json. You should commit this file.
# npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@~2.1.2 (node_modules/chokidar/node_modules/fsevents):
# npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.1.3: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
# npm WARN hello-dock@0.0.0 No description
# npm WARN hello-dock@0.0.0 No repository field.
# npm WARN hello-dock@0.0.0 No license field.
#
# added 327 packages from 301 contributors and audited 329 packages in 35.971s
#
# 26 packages are looking for funding
#   run `npm fund` for details
#
# found 0 vulnerabilities
#
# Removing intermediate container 6dfabf0ee9f8
#  ---> 21fd1b065314
# Step 5/9 : COPY . .
#  ---> 43243f95bff7
# Step 6/9 : RUN npm run build
#  ---> Running in 4d918cf18584
#
# > hello-dock@0.0.0 build /app
# > vite build
#
# - Building production bundle...
#
# [write] dist/index.html 0.39kb, brotli: 0.15kb
# [write] dist/_assets/docker-handbook-github.3adb4865.webp 12.32kb
# [write] dist/_assets/index.eabcae90.js 42.56kb, brotli: 15.40kb
# [write] dist/_assets/style.0637ccc5.css 0.16kb, brotli: 0.10kb
# - Building production bundle...
#
# Build completed in 1.71s.
#
# Removing intermediate container 4d918cf18584
#  ---> 187fb3e82d0d
# Step 7/9 : EXPOSE 80
#  ---> Running in b3aab5cf5975
# Removing intermediate container b3aab5cf5975
#  ---> d6fcc058cfda
# Step 8/9 : FROM nginx:stable-alpine
# stable: Pulling from library/nginx
# 6ec7b7d162b2: Already exists 
# 43876acb2da3: Pull complete 
# 7a79edd1e27b: Pull complete 
# eea03077c87e: Pull complete 
# eba7631b45c5: Pull complete 
# Digest: sha256:2eea9f5d6fff078ad6cc6c961ab11b8314efd91fb8480b5d054c7057a619e0c3
# Status: Downloaded newer image for nginx:stable
#  ---> 05f64a802c26
# Step 9/9 : COPY --from=builder /app/dist /usr/share/nginx/html
#  ---> 8c6dfc34a10d
# Successfully built 8c6dfc34a10d
# Successfully tagged hello-dock:prod

生成鏡像後,可以通過執行以下命令來運行新容器:

docker container run \
    --rm \
    --detach \
    --name hello-dock-prod \
    --publish 8080:80 \
    hello-dock:prod

# 224aaba432bb09aca518fdd0365875895c2f5121eb668b2e7b2d5a99c019b953

正在運行的應用程序應位於 http://127.0.0.1:8080 上:

在這里,可以看到我所有的 hello-dock 應用程序。 如果要構建具有大量依賴關系的大型應用程序,那麼多階段構建可能會非常有用。如果配置正確,則可以很好地優化和壓縮分多個階段構建的鏡像。

如何忽略不必要的文件

如果了解 git,你可能會知道項目中的 .gitignore 文件。 這些文件包含要從倉庫中排除的文件和目錄的列表。

嗯,Docker 也有類似的概念。.dockerignore 文件包含要從鏡像構建中排除的文件和目錄的列表。可以在 hello-dock 目錄中有一個預先創建的 .dockerignore 文件。

.git
*Dockerfile*
*docker-compose*
node_modules

該 .dockerignore 文件必須位於構建上下文中。這里提到的文件和目錄將被 COPY 指令忽略。但是,如果執行綁定掛載,則 .dockerignore 文件將對此無效。我已經在項目倉庫中的必要位置添加了 .dockerignore 文件。

Docker 中的網絡操作基礎知識

到目前為止,在本書中,僅處理了單個容器項目。但是在實際應用中,多數項目都具有多個容器。老實說,如果不了解容器隔離的細微差別,使用一堆容器可能會有些困難。

因此,在本書的這一部分中,將介紹 Docker 的基本網絡,並涉及一個小型的多容器項目。

好了,已經在上一節中了解到容器是隔離的環境。現在考慮一個場景,其中有一個基於 Express.js notes-api 應用程序和一個 PostgreSQL 數據庫服務,他們在兩個單獨的容器中運行。

這兩個容器彼此完全隔離,並且彼此無關。那麼如何連接兩者? 將是一個挑戰。

你可能會想到針對此問題的兩種可能的解決方案。 它們如下:

  • 使用暴露的端口訪問數據庫服務。
  • 使用其 IP 地址和默認端口訪問數據庫服務。

第一個涉及從 postgres 容器暴露一個端口,notes-api 將通過該端口進行連接。假設來自 postgres 容器的暴露端口為 5432。現在,如果嘗試從 notes-api 容器內部連接到 127.0.0.1:5432,會發現 notes-api 根本找不到數據庫服務。

原因是,在 notes-api 容器內的 127.0.0.1 時,只是代表當前容器的 localhost 。postgres 服務根本不存在。結果是,notes-api 應用程序無法連接。

你可能想到的第二個解決方案找到 postgres 容器的確切 IP 地址,使用 container inspect 命令並將其與端口一起使用。 假設 postgres 容器的名稱為 notes-api-db-server ,則可以通過執行以下命令輕松獲得 IP 地址:

docker container inspect --format='{{range .NetworkSettings.Networks}} {{.IPAddress}} {{end}}' notes-api-db-server

#  172.17.0.2

現在假設 postgres 的默認端口是 5432,可以通過從 notes-api 容器連接到 172.17.0.2:5432 來非常容易地訪問數據庫服務。

這種方法也存在問題。 不建議使用 IP 地址來引用容器。另外,如果容器被破壞並重新創建,則 IP 地址可能會更改。跟蹤這些不斷變化的 IP 地址可能非常麻煩。

現在,我已經排除了對原始問題的可能錯誤答案,正確的答案是,將它們放置在用戶定義的橋接網絡下即可將它們連接起來。

Docker 網絡基礎

Docker 中的網絡是另一個邏輯對象,和容器和鏡像一樣。就像其他兩個一樣,在 docker network 組下有很多用於操縱網絡的命令。

要列出系統中的網絡,請執行以下命令:

docker network ls

# NETWORK ID     NAME      DRIVER    SCOPE
# c2e59f2b96bd   bridge    bridge    local
# 124dccee067f   host      host      local
# 506e3822bf1f   none      null      local

你應該在系統中看到三個網絡。現在在這里查看表的 DRIVER 列。這些 drivers 可以視為網絡類型。

默認情況下,Docker 具有五類網絡驅動。它們如下:

  • bridge – Docker 中的默認網絡驅動程序。當多個容器以標準模式運行並且需要相互通信時,可以使用此方法。
  • host – 完全消除網絡隔離。在 host 網絡下運行的任何容器基本上都連接到主機系統的網絡。
  • none – 此驅動程序完全禁用容器的聯網。 我還沒有找到其應用場景。
  • overlay – 這用於跨計算機連接多個 Docker 守護程序,這超出了本書的範圍。
  • macvlan – 允許將 MAC 地址分配給容器,使它們的功能類似於網絡中的物理設備。

也有第三方插件,可讓你將 Docker 與專用網絡堆棧集成。在上述五種方法中,本書僅使用 bridge 網絡驅動程序。

如何在 Docker 中創建用戶定義的橋接網絡

在開始創建自己的橋接網絡之前,我想花一些時間來討論 Docker 隨附的默認橋接網絡。讓我們首先列出系統上的所有網絡:

docker network ls

# NETWORK ID     NAME      DRIVER    SCOPE
# c2e59f2b96bd   bridge    bridge    local
# 124dccee067f   host      host      local
# 506e3822bf1f   none      null      local

如你所見,Docker 隨附了一個名為 bridge 的默認橋接網絡。 運行的任何容器都將自動連接到此網橋網絡:

docker container run --rm --detach --name hello-dock --publish 8080:80 fhsinchy/hello-dock
# a37f723dad3ae793ce40f97eb6bb236761baa92d72a2c27c24fc7fda0756657d

docker network inspect --format='{{range .Containers}}{{.Name}}{{end}}' bridge
# hello-dock

連接到默認橋接網絡的容器可以使用我在上一小節中不鼓勵使用的 IP 地址相互通信。

但是,用戶定義的橋接網絡比默認橋接網絡多一些額外的功能。根據有關此主題的官方 docs,一些值得注意的額外功能如下:

  • 用戶定義的網橋可在容器之間提供自動 DNS 解析: 這意味著連接到同一網絡的容器可以使用容器名稱相互通信。 因此,如果你有兩個名為 notes-api 和 notes-db 的容器,則 API 容器將能夠使用 notes-db 名稱連接到數據庫容器。
  • 用戶定義的網橋提供更好的隔離性: 默認情況下,所有容器都連接到默認橋接網絡,這可能會導致它們之間的沖突。將容器連接到用戶定義的橋可以確保更好的隔離。
  • 容器可以即時與用戶定義的網絡連接和分離: 在容器的生命周期內,可以即時將其與用戶定義的網絡連接或斷開連接。要從默認網橋網絡中刪除容器,需要停止容器並使用其他網絡選項重新創建它。

既然已經了解了很多有關用戶定義的網絡的知識,那麼現在該為自己創建一個了。可以使用 network create 命令創建網絡。該命令的通用語法如下:

docker network create <network name>

要創建名稱為 skynet 的網絡,請執行以下命令:

docker network create skynet

# 7bd5f351aa892ac6ec15fed8619fc3bbb95a7dcdd58980c28304627c8f7eb070

docker network ls

# NETWORK ID     NAME     DRIVER    SCOPE
# be0cab667c4b   bridge   bridge    local
# 124dccee067f   host     host      local
# 506e3822bf1f   none     null      local
# 7bd5f351aa89   skynet   bridge    local

如你所見,已經使用給定名稱創建了一個新網絡。當前沒有容器連接到該網絡。在下一小節中,將學習有關將容器連接到網絡的信息。

如何在 Docker 中將容器連接到網絡

將容器連接到網絡的方式主要有兩種。首先,可以使用 network connect 命令將容器連接到網絡。該命令的通用語法如下:

docker network connect <network identifier> <container identifier>

要將 hello-dock 容器連接到 skynet 網絡,可以執行以下命令:

docker network connect skynet hello-dock

docker network inspect --format='{{range .Containers}} {{.Name}} {{end}}' skynet

#  hello-dock

docker network inspect --format='{{range .Containers}} {{.Name}} {{end}}' bridge

#  hello-dock

從兩個 network inspect 命令的輸出中可以看到,hello-dock 容器現在已連接到 skynet 和默認的 bridge 網絡。

將容器連接到網絡的第二種方法是對 container run 或 container create 命令使用 --network 選項。 該選項的通用語法如下:

--network <network identifier>

要運行連接到同一網絡的另一個 hello-dock 容器,可以執行以下命令:

docker container run --network skynet --rm --name alpine-box -it alpine sh

# lands you into alpine linux shell

/ # ping hello-dock

# PING hello-dock (172.18.0.2): 56 data bytes
# 64 bytes from 172.18.0.2: seq=0 ttl=64 time=0.191 ms
# 64 bytes from 172.18.0.2: seq=1 ttl=64 time=0.103 ms
# 64 bytes from 172.18.0.2: seq=2 ttl=64 time=0.139 ms
# 64 bytes from 172.18.0.2: seq=3 ttl=64 time=0.142 ms
# 64 bytes from 172.18.0.2: seq=4 ttl=64 time=0.146 ms
# 64 bytes from 172.18.0.2: seq=5 ttl=64 time=0.095 ms
# 64 bytes from 172.18.0.2: seq=6 ttl=64 time=0.181 ms
# 64 bytes from 172.18.0.2: seq=7 ttl=64 time=0.138 ms
# 64 bytes from 172.18.0.2: seq=8 ttl=64 time=0.158 ms
# 64 bytes from 172.18.0.2: seq=9 ttl=64 time=0.137 ms
# 64 bytes from 172.18.0.2: seq=10 ttl=64 time=0.145 ms
# 64 bytes from 172.18.0.2: seq=11 ttl=64 time=0.138 ms
# 64 bytes from 172.18.0.2: seq=12 ttl=64 time=0.085 ms

--- hello-dock ping statistics ---
13 packets transmitted, 13 packets received, 0% packet loss
round-trip min/avg/max = 0.085/0.138/0.191 ms

如你所見,從 alpine-box 容器內部運行 ping hello-dock 是可行的,因為這兩個容器都在同一用戶定義的網橋網絡下,並且自動 DNS 解析有效。

但是請記住,為了使自動 DNS 解析正常工作,必須為容器分配自定義名稱。使用隨機生成的名稱將不起作用。

如何在 Docker 中從網絡分離容器

在上一小節中,了解了有關將容器連接到網絡的信息。在本小節中,將學習如何分離它們。

可以使用 network disconnect 命令來執行此任務。該命令的通用語法如下:

docker network disconnect <network identifier> <container identifier>

要從 skynet 網絡分離 hello-dock 容器,可以執行以下命令:

docker network disconnect skynet hello-dock

就像 network connect 命令一樣,network disconnect 命令也不給出任何輸出。

如何刪除 Docker 中的網絡

就像 Docker 中的其他邏輯對象一樣,可以使用 network rm 命令刪除網絡。該命令的通用語法如下:

docker network rm <network identifier>

要從系統中刪除 skynet 網絡,可以執行以下命令:

docker network rm skynet

也可以使用 network prune 命令從系統中刪除所有未使用的網絡。該命令還具有 -f 或 --force 和 -a 或 --all 選項。

如何容器化多容器 JavaScript 應用程序

既然已經對 Docker 中的網絡有了足夠的了解,那麼在本節中,將學習如何將成熟的多容器項目容器化。涉及的項目是一個基於 Express.js 和 PostgreSQL 的簡單 notes-api

在此項目中,需要使用網絡連接兩個容器。除此之外,還將學習諸如環境變量和命名卷之類的概念。因此,事不宜遲,讓我們直接開始。

如何運行數據庫服務

該項目中的數據庫服務器是一個簡單的 PostgreSQL 服務,使用官方的 postgres 鏡像。

根據官方文檔,為了使用此鏡像運行容器,必須提供 POSTGRES_PASSWORD 環境變量。除此之外,還將使用 POSTGRES_DB 環境變量為默認數據庫提供一個名稱。默認情況下,PostgreSQL 監聽 5432 端口,因此也需要公開它。

要運行數據庫服務,可以執行以下命令:

docker container run \
    --detach \
    --name=notes-db \
    --env POSTGRES_DB=notesdb \
    --env POSTGRES_PASSWORD=secret \
    --network=notes-api-network \
    postgres:12

# a7b287d34d96c8e81a63949c57b83d7c1d71b5660c87f5172f074bd1606196dc

docker container ls

# CONTAINER ID   IMAGE         COMMAND                  CREATED              STATUS              PORTS      NAMES
# a7b287d34d96   postgres:12   "docker-entrypoint.s…"   About a minute ago   Up About a minute   5432/tcp   notes-db

container run 和 container create 命令的 --env 選項可用於向容器提供環境變量。如你所見,數據庫容器已成功創建並且正在運行。

盡管容器正在運行,但是存在一個小問題。 PostgreSQL、MongoDB 和 MySQL 等數據庫將其數據保留在目錄中。 PostgreSQL使用容器內的 /var/lib/postgresql/data 目錄來持久化數據。

現在,如果容器由於某種原因被破壞怎麼辦? 將丟失所有數據。為了解決此問題,可以使用命名卷。

如何在 Docker 中使用命名卷

之前,已經使用了綁定掛載和匿名卷。命名卷與匿名卷非常相似,不同之處在於可以使用其名稱引用命名卷。

卷也是 Docker 中的邏輯對象,可以使用命令行進行操作。volume create 命令可用於創建命名卷。

該命令的通用語法如下:

docker volume create <volume name>

要創建一個名為 notes-db-data 的卷,可以執行以下命令:

docker volume create notes-db-data

# notes-db-data

docker volume ls

# DRIVER    VOLUME NAME
# local     notes-db-data

這個卷現在可以被安裝到 notes-db 容器中的 /var/lib/postgresql/data 中。為此,請停止並刪除 notes-db 容器:

docker container stop notes-db

# notes-db

docker container rm notes-db

# notes-db

現在運行一個新容器,並使用 --volume 或 -v 選項分配卷。

docker container run \
    --detach \
    --volume notes-db-data:/var/lib/postgresql/data \
    --name=notes-db \
    --env POSTGRES_DB=notesdb \
    --env POSTGRES_PASSWORD=secret \
    --network=notes-api-network \
    postgres:12

# 37755e86d62794ed3e67c19d0cd1eba431e26ab56099b92a3456908c1d346791

現在檢查 notes-db 容器以確保安裝成功:

docker container inspect --format='{{range .Mounts}} {{ .Name }} {{end}}' notes-db

#  notes-db-data

現在,這些數據將安全地存儲在 notes-db-data 卷中,並且將來可以重覆使用。在這里也可以使用綁定掛載代替命名卷,但是在這種情況下,我更喜歡使用命名卷。

如何從 Docker 中的容器訪問日志

為了查看來自容器的日志,可以使用 container logs 命令。 該命令的通用語法如下:

docker container logs <container identifier>

要從 notes-db 容器訪問日志,可以執行以下命令:

docker container logs notes-db

# The files belonging to this database system will be owned by user "postgres".
# This user must also own the server process.

# The database cluster will be initialized with locale "en_US.utf8".
# The default database encoding has accordingly been set to "UTF8".
# The default text search configuration will be set to "english".
#
# Data page checksums are disabled.
#
# fixing permissions on existing directory /var/lib/postgresql/data ... ok
# creating subdirectories ... ok
# selecting dynamic shared memory implementation ... posix
# selecting default max_connections ... 100
# selecting default shared_buffers ... 128MB
# selecting default time zone ... Etc/UTC
# creating configuration files ... ok
# running bootstrap script ... ok
# performing post-bootstrap initialization ... ok
# syncing data to disk ... ok
#
#
# Success. You can now start the database server using:
#
#     pg_ctl -D /var/lib/postgresql/data -l logfile start
#
# initdb: warning: enabling "trust" authentication for local connections
# You can change this by editing pg_hba.conf or using the option -A, or
# --auth-local and --auth-host, the next time you run initdb.
# waiting for server to start....2021-01-25 13:39:21.613 UTC [47] LOG:  starting PostgreSQL 12.5 (Debian 12.5-1.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit
# 2021-01-25 13:39:21.621 UTC [47] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
# 2021-01-25 13:39:21.675 UTC [48] LOG:  database system was shut down at 2021-01-25 13:39:21 UTC
# 2021-01-25 13:39:21.685 UTC [47] LOG:  database system is ready to accept connections
#  done
# server started
# CREATE DATABASE
#
#
# /usr/local/bin/docker-entrypoint.sh: ignoring /docker-entrypoint-initdb.d/*
#
# 2021-01-25 13:39:22.008 UTC [47] LOG:  received fast shutdown request
# waiting for server to shut down....2021-01-25 13:39:22.015 UTC [47] LOG:  aborting any active transactions
# 2021-01-25 13:39:22.017 UTC [47] LOG:  background worker "logical replication launcher" (PID 54) exited with exit code 1
# 2021-01-25 13:39:22.017 UTC [49] LOG:  shutting down
# 2021-01-25 13:39:22.056 UTC [47] LOG:  database system is shut down
#  done
# server stopped
#
# PostgreSQL init process complete; ready for start up.
#
# 2021-01-25 13:39:22.135 UTC [1] LOG:  starting PostgreSQL 12.5 (Debian 12.5-1.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit
# 2021-01-25 13:39:22.136 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
# 2021-01-25 13:39:22.136 UTC [1] LOG:  listening on IPv6 address "::", port 5432
# 2021-01-25 13:39:22.147 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
# 2021-01-25 13:39:22.177 UTC [75] LOG:  database system was shut down at 2021-01-25 13:39:22 UTC
# 2021-01-25 13:39:22.190 UTC [1] LOG:  database system is ready to accept connections

從第 57 行的文本可以看出,數據庫已啟動,並準備接受來自外部的連接。該命令還有 --follow 或 -f 選項,使可以將控制台連接到日志輸出並獲得連續的文本流。

如何在 Docker 中創建網絡並連接數據庫服務

如在上一節中所學,容器必須連接到用戶定義的橋接網絡,才能使用容器名稱相互通信。為此,請在系統中創建一個名為 notes-api-network 的網絡:

docker network create notes-api-network

現在,通過執行以下命令,將 notes-db 容器連接到該網絡:

docker network connect notes-api-network notes-db

如何編寫 Dockerfile

轉到克隆項目代碼的目錄。在其中,進入 notes-api/api 目錄,並創建一個新的 Dockerfile。 將以下代碼放入文件中:

# stage one
FROM node:lts-alpine as builder

# install dependencies for node-gyp
RUN apk add --no-cache python make g++

WORKDIR /app

COPY ./package.json .
RUN npm install --only=prod

# stage two
FROM node:lts-alpine

EXPOSE 3000
ENV NODE_ENV=production

USER node
RUN mkdir -p /home/node/app
WORKDIR /home/node/app

COPY . .
COPY --from=builder /app/node_modules  /home/node/app/node_modules

CMD [ "node", "bin/www" ]

這是一個多階段構建。第一階段用於使用 node-gyp 構建和安裝依賴項,第二階段用於運行應用程序。我將簡要介紹以下步驟:

  • 階段1使用 node:lts-alpine 作為基礎,並使用 builder 作為階段名稱。
  • 在第 5 行,安裝了 pythonmake 和 g++node-gyp 工具需要這三個軟件包才能運行。
  • 在第 7 行,我們將 /app 目錄設置為 WORKDIR
  • 在第 9 和 10 行,將 package.json 文件覆制到 WORKDIR 並安裝所有依賴項。
  • 第 2 階段還使用 node-lts:alpine 作為基礎鏡像。
  • 在第 16 行,將環境變量 NODE_ENV 設置為 production。 這對於 API 正常運行很重要。
  • 從第 18 行到第 20 行,將默認用戶設置為 node,創建 /home/node/app 目錄,並將其設置為 WORKDIR
  • 在第 22 行,覆制了所有項目文件,在第 23 行,從 builder 階段覆制了 node_modules 目錄。此目錄包含運行應用程序所需的所有已構建依賴關系。
  • 在第 25 行,設置了默認命令。

要從此 Dockerfile 構建鏡像,可以執行以下命令:

docker image build --tag notes-api .

# Sending build context to Docker daemon  37.38kB
# Step 1/14 : FROM node:lts-alpine as builder
#  ---> 471e8b4eb0b2
# Step 2/14 : RUN apk add --no-cache python make g++
#  ---> Running in 5f20a0ecc04b
# fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/main/x86_64/APKINDEX.tar.gz
# fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/community/x86_64/APKINDEX.tar.gz
# (1/21) Installing binutils (2.33.1-r0)
# (2/21) Installing gmp (6.1.2-r1)
# (3/21) Installing isl (0.18-r0)
# (4/21) Installing libgomp (9.3.0-r0)
# (5/21) Installing libatomic (9.3.0-r0)
# (6/21) Installing mpfr4 (4.0.2-r1)
# (7/21) Installing mpc1 (1.1.0-r1)
# (8/21) Installing gcc (9.3.0-r0)
# (9/21) Installing musl-dev (1.1.24-r3)
# (10/21) Installing libc-dev (0.7.2-r0)
# (11/21) Installing g++ (9.3.0-r0)
# (12/21) Installing make (4.2.1-r2)
# (13/21) Installing libbz2 (1.0.8-r1)
# (14/21) Installing expat (2.2.9-r1)
# (15/21) Installing libffi (3.2.1-r6)
# (16/21) Installing gdbm (1.13-r1)
# (17/21) Installing ncurses-terminfo-base (6.1_p20200118-r4)
# (18/21) Installing ncurses-libs (6.1_p20200118-r4)
# (19/21) Installing readline (8.0.1-r0)
# (20/21) Installing sqlite-libs (3.30.1-r2)
# (21/21) Installing python2 (2.7.18-r0)
# Executing busybox-1.31.1-r9.trigger
# OK: 212 MiB in 37 packages
# Removing intermediate container 5f20a0ecc04b
#  ---> 637ca797d709
# Step 3/14 : WORKDIR /app
#  ---> Running in 846361b57599
# Removing intermediate container 846361b57599
#  ---> 3d58a482896e
# Step 4/14 : COPY ./package.json .
#  ---> 11b387794039
# Step 5/14 : RUN npm install --only=prod
#  ---> Running in 2e27e33f935d
#  added 269 packages from 220 contributors and audited 1137 packages in 140.322s
#
# 4 packages are looking for funding
#   run `npm fund` for details
#
# found 0 vulnerabilities
#
# Removing intermediate container 2e27e33f935d
#  ---> eb7cb2cb0b20
# Step 6/14 : FROM node:lts-alpine
#  ---> 471e8b4eb0b2
# Step 7/14 : EXPOSE 3000
#  ---> Running in 4ea24f871747
# Removing intermediate container 4ea24f871747
#  ---> 1f0206f2f050
# Step 8/14 : ENV NODE_ENV=production
#  ---> Running in 5d40d6ac3b7e
# Removing intermediate container 5d40d6ac3b7e
#  ---> 31f62da17929
# Step 9/14 : USER node
#  ---> Running in 0963e1fb19a0
# Removing intermediate container 0963e1fb19a0
#  ---> 0f4045152b1c
# Step 10/14 : RUN mkdir -p /home/node/app
#  ---> Running in 0ac591b3adbd
# Removing intermediate container 0ac591b3adbd
#  ---> 5908373dfc75
# Step 11/14 : WORKDIR /home/node/app
#  ---> Running in 55253b62ff57
# Removing intermediate container 55253b62ff57
#  ---> 2883cdb7c77a
# Step 12/14 : COPY . .
#  ---> 8e60893a7142
# Step 13/14 : COPY --from=builder /app/node_modules  /home/node/app/node_modules
#  ---> 27a85faa4342
# Step 14/14 : CMD [ "node", "bin/www" ]
#  ---> Running in 349c8ca6dd3e
# Removing intermediate container 349c8ca6dd3e
#  ---> 9ea100571585
# Successfully built 9ea100571585
# Successfully tagged notes-api:latest

在使用該鏡像運行容器之前,請確保數據庫容器正在運行,並且已附加到 notes-api-network 上。

docker container inspect notes-db

# [
#     {
#         ...
#         "State": {
#             "Status": "running",
#             "Running": true,
#             "Paused": false,
#             "Restarting": false,
#             "OOMKilled": false,
#             "Dead": false,
#             "Pid": 11521,
#             "ExitCode": 0,
#             "Error": "",
#             "StartedAt": "2021-01-26T06:55:44.928510218Z",
#             "FinishedAt": "2021-01-25T14:19:31.316854657Z"
#         },
#         ...
#         "Mounts": [
#             {
#                 "Type": "volume",
#                 "Name": "notes-db-data",
#                 "Source": "/var/lib/docker/volumes/notes-db-data/_data",
#                 "Destination": "/var/lib/postgresql/data",
#                 "Driver": "local",
#                 "Mode": "z",
#                 "RW": true,
#                 "Propagation": ""
#             }
#         ],
#         ...
#         "NetworkSettings": {
#             ...
#             "Networks": {
#                 "bridge": {
#                     "IPAMConfig": null,
#                     "Links": null,
#                     "Aliases": null,
#                     "NetworkID": "e4c7ce50a5a2a49672155ff498597db336ecc2e3bbb6ee8baeebcf9fcfa0e1ab",
#                     "EndpointID": "2a2587f8285fa020878dd38bdc630cdfca0d769f76fc143d1b554237ce907371",
#                     "Gateway": "172.17.0.1",
#                     "IPAddress": "172.17.0.2",
#                     "IPPrefixLen": 16,
#                     "IPv6Gateway": "",
#                     "GlobalIPv6Address": "",
#                     "GlobalIPv6PrefixLen": 0,
#                     "MacAddress": "02:42:ac:11:00:02",
#                     "DriverOpts": null
#                 },
#                 "notes-api-network": {
#                     "IPAMConfig": {},
#                     "Links": null,
#                     "Aliases": [
#                         "37755e86d627"
#                     ],
#                     "NetworkID": "06579ad9f93d59fc3866ac628ed258dfac2ed7bc1a9cd6fe6e67220b15d203ea",
#                     "EndpointID": "5b8f8718ec9a5ec53e7a13cce3cb540fdf3556fb34242362a8da4cc08d37223c",
#                     "Gateway": "172.18.0.1",
#                     "IPAddress": "172.18.0.2",
#                     "IPPrefixLen": 16,
#                     "IPv6Gateway": "",
#                     "GlobalIPv6Address": "",
#                     "GlobalIPv6PrefixLen": 0,
#                     "MacAddress": "02:42:ac:12:00:02",
#                     "DriverOpts": {}
#                 }
#             }
#         }
#     }
# ]

已經縮短了輸出,以便於在此處查看。在我的系統上,notes-db 容器正在運行,使用 notes-db-data 卷,並連接到 notes-api-network 橋接網絡。

一旦確定一切就緒,就可以通過執行以下命令來運行新容器:

docker container run \
    --detach \
    --name=notes-api \
    --env DB_HOST=notes-db \
    --env DB_DATABASE=notesdb \
    --env DB_PASSWORD=secret \
    --publish=3000:3000 \
    --network=notes-api-network \
    notes-api
    
# f9ece420872de99a060b954e3c236cbb1e23d468feffa7fed1e06985d99fb919

應該理解這個長命令,因此只簡要介紹下環境變量。

notes-api 應用程序需要設置三個環境變量。 它們如下:

  • DB_HOST – 這是數據庫服務的主機。假定數據庫服務和 API 都連接到同一用戶定義的橋接網絡,則可以使用其容器名稱(在這種情況下為 notes-db)引用數據庫服務。
  • DB_DATABASE – 此API將使用的數據庫。 在運行數據庫服務小節,使用環境變量 POSTGRES_DB 將默認數據庫名稱設置為 notesdb。 將在這里使用它。
  • DB_PASSWORD – 連接數據庫的密碼。 這也在運行數據庫服務小節涉及,使用環境變量POSTGRES_PASSWORD

要檢查容器是否正常運行,可以使用 container ls 命令:

docker container ls

# CONTAINER ID   IMAGE         COMMAND                  CREATED          STATUS          PORTS                    NAMES
# f9ece420872d   notes-api     "docker-entrypoint.s…"   12 minutes ago   Up 12 minutes   0.0.0.0:3000->3000/tcp   notes-api
# 37755e86d627   postgres:12   "docker-entrypoint.s…"   17 hours ago     Up 14 minutes   5432/tcp                 notes-db

容器正在運行。 可以訪問 http://127.0.0.1:3000/ 來查看正在使用的API。

該 API 總共有五個路由,可以在 /notes/api/api/api/routes/notes.js 文件中看到。 它是用我的一個開源項目引導的。

盡管容器正在運行,但是在開始使用它之前,還有最後一件事要做。必須運行設置數據庫表所必需的數據庫遷移,並且可以通過在容器內執行 npm run db:migrate 命令來執行此操作。

如何在正在運行的容器中執行命令

已經了解了在停止的容器中執行命令的知識。另一種情況是在正在運行的容器內執行命令。

為此,必須使用 exec 命令在正在運行的容器內執行自定義命令。

exec 命令的通用語法如下:

docker container exec <container identifier> <command>

要執行 notes-api 容器內的 npm run db:migrate,可以執行以下命令:

docker container exec notes-api npm run db:migrate

# > notes-api@ db:migrate /home/node/app
# > knex migrate:latest
#
# Using environment: production
# Batch 1 run: 1 migrations

如果要在正在運行的容器中運行交互式命令,則必須使用 -it 標志。例如,如果要訪問在 notes-api 容器中運行的 shell,可以執行以下命令:

docker container exec -it notes-api sh

# / # uname -a
# Linux b5b1367d6b31 5.10.9-201.fc33.x86_64 #1 SMP Wed Jan 20 16:56:23 UTC 2021 x86_64 Linux

如何在 Docker 中編寫管理腳本

管理多容器項目以及網絡,卷和內容意味著編寫大量命令。為了簡化過程,我通常會從簡單的 shell腳本和 Makefile 來提高效率。

可以在 notes-api 目錄中找到四個 shell 腳本。 它們如下:

  • boot.sh – 用於啟動容器(如果已存在)。
  • build.sh – 創建並運行容器。如果需要,它還會創建鏡像,卷和網絡。
  • destroy.sh – 刪除與此項目關聯的所有容器,卷和網絡。
  • stop.sh – 停止所有正在運行的容器。

還有一個 Makefile,其中包含名為startstopbuild和 destroy 的四個目標,每個目標都調用前面提到的 shell 腳本。

如果容器在系統中處於運行狀態,執行 make stop 將停止所有容器。 執行 make destroy 應該停止容器並刪除所有東西。 確保正在 notes-api 目錄中運行腳本:

make destroy

# ./shutdown.sh
# stopping api container --->
# notes-api
# api container stopped --->

# stopping db container --->
# notes-db
# db container stopped --->

# shutdown script finished

# ./destroy.sh
# removing api container --->
# notes-api
# api container removed --->

# removing db container --->
# notes-db
# db container removed --->

# removing db data volume --->
# notes-db-data
# db data volume removed --->

# removing network --->
# notes-api-network
# network removed --->

# destroy script finished

如果遇到權限拒絕錯誤,請在腳本上執行 chmod + x

chmod +x boot.sh build.sh destroy.sh shutdown.sh

這里不解釋這些腳本,因為它們是簡單的 if-else 語句以及一些已經看過很多次的 Docker 命令。如果對 Linux Shell 有所了解,那麼也應該能夠理解這些腳本。

如何使用 Docker-Compose 組合項目

在上一節中,了解了有關管理多容器項目的困難。除了編寫許多命令之外,還有一種更簡單的方法來管理多容器項目,該工具稱為Docker Compose

根據 Docker 的 文檔 –

Compose 是用於定義和運行多容器 Docker 應用程序的工具。通過 Compose,可以使用 YAML 文件來配置應用程序的服務。然後,使用一個命令,就可以從配置中創建並啟動所有服務。

盡管 Compose 可在所有環境中使用,但它更專注於開發和測試。完全不建議在生產環境上使用 Compose。

Docker Compose 基礎

轉至用來克隆本書隨附倉庫的目錄。進入 notes-api/api 目錄並創建 Dockerfile.dev 文件。將以下代碼放入其中:


FROM node:lts-alpine as builder


RUN apk add --no-cache python make g++

WORKDIR /app

COPY ./package.json .
RUN npm install


FROM node:lts-alpine

ENV NODE_ENV=development

USER node
RUN mkdir -p /home/node/app
WORKDIR /home/node/app

COPY . .
COPY --from=builder /app/node_modules /home/node/app/node_modules

CMD [ "./node_modules/.bin/nodemon", "--config", "nodemon.json", "bin/www" ]

該代碼與上一小節中使用的 Dockerfile 幾乎相同。 此文件中的三個區別如下:

  • 在第 10 行中,執行 npm install 而不是 npm run install --only = prod,因為還需要開發依賴項。
  • 在第 15 行,將環境變量 NODE_ENV 設置為 development 而不是 production
  • 在第 24 行,使用名為 nodemon 的工具來獲取 API 的熱重載功能。

已經知道該項目有兩個容器:

  • notes-db – 一個基於 PostgreSQL 的數據庫服務。
  • notes-api – 一個基於 Express.js 的 REST API

在 Compose 的世界中,組成應用程序的每個容器都稱為服務。組合多容器項目的第一步就是定義這些服務。

就像 Docker 守護進程使用 Dockerfile 構建映像一樣,Docker Compose 使用 docker-compose.yaml 文件從中讀取服務定義。

轉到 notes-api 目錄並創建一個新的 docker-compose.yaml 文件。 將以下代碼放入新創建的文件中:

version: "3.8"

services: 
    db:
        image: postgres:12
        container_name: notes-db-dev
        volumes: 
            - notes-db-dev-data:/var/lib/postgresql/data
        environment:
            POSTGRES_DB: notesdb
            POSTGRES_PASSWORD: secret
    api:
        build:
            context: ./api
            dockerfile: Dockerfile.dev
        image: notes-api:dev
        container_name: notes-api-dev
        environment: 
            DB_HOST: db 
            DB_DATABASE: notesdb
            DB_PASSWORD: secret
        volumes: 
            - /home/node/app/node_modules
            - ./api:/home/node/app
        ports: 
            - 3000:3000

volumes:
    notes-db-dev-data:
        name: notes-db-dev-data

每個有效的 docker-compose.yaml 文件均通過定義文件版本開始。在撰寫本文時,3.8 是最新版本。可以在此處查找最新版本。

YAML 文件中的塊由縮進定義。將仔細介紹每個塊,並解釋它們的作用。

  • services 塊包含應用程序中每個服務或容器的定義。db 和 api 是構成該項目的兩個服務。
  • db 塊在應用程序中定義了一個新服務,並保存了啟動容器所需的信息。每個服務都需要一個預先構建的鏡像或一個 Dockerfile 來運行容器。 對於 db 服務,我們使用的是官方 PostgreSQL 鏡像。
  • 與 db 服務不同的是,不存在 api 服務的預構建鏡像。因此,將使用 Dockerfile.dev 文件。
  • volumes 塊定義了任何服務所需的任何名稱卷。當時,它僅登記 db 服務使用的是 notes-db-dev-data 卷。

既然已經對 docker-compose.yaml 文件有了一個高層次的概述,那麼讓我們仔細看一下各個服務。

db 服務的定義代碼如下:

db:
    image: postgres:12
    container_name: notes-db-dev
    volumes: 
        - db-data:/var/lib/postgresql/data
    environment:
        POSTGRES_DB: notesdb
        POSTGRES_PASSWORD: secret
  • image 鍵保存用於此容器的鏡像倉庫和標簽。使用 postgres:12 鏡像來運行數據庫容器。
  • container_name 指示容器的名稱。默認情況下,容器使用 <project directory name>_<service name> 語法命名。可以使用 container_name 覆蓋它。
  • volumes 數組保存該服務的卷映射,並支持命名卷,匿名卷和綁定掛載。 語法 <source>:<destination> 與之前相同。
  • environment map 包含服務所需的各種環境變量的值。

api 服務的定義代碼如下:

api:
    build:
        context: ./api
        dockerfile: Dockerfile.dev
    image: notes-api:dev
    container_name: notes-api-dev
    environment: 
        DB_HOST: db 
        DB_DATABASE: notesdb
        DB_PASSWORD: secret
    volumes: 
        - /home/node/app/node_modules
        - ./api:/home/node/app
    ports: 
        - 3000:3000
  • api 服務沒有預構建的鏡像。相反,它具有構建配置。在 build 塊下,定義了用於構建鏡像的上下文和 Dockerfile 的名稱。到目前為止,應該已經了解了上下文和 Dockerfile,因此我不會花時間解釋它們。
  • image 鍵保存要構建的鏡像的名稱。如果未分配,則將使用 <project directory name>_<service name> 語法來命名鏡像。
  • 在 environment map 內部,DB_HOST 變量演示了 Compose 的功能。即,可以使用其名稱引用同一應用程序中的另一服務。因此,此處的 db 將被 api 服務容器的 IP 地址代替。DB_DATABASE 和 DB_PASSWORD 變量必須分別與 db 服務定義中的 POSTGRES_DB 和 POSTGRES_PASSWORD 匹配。
  • 在 volumes map 中,可以看到一個匿名卷和一個綁定掛載。語法與上一節中看到的相同。
  • ports 映射定義了端口映射。 語法 <host port>:<container port> 與以前使用的 --publish 選項相同。

最後,volumes 的代碼如下:

volumes:
    db-data:
        name: notes-db-dev-data

在此處定義服務中使用的命名卷。如果未定義名稱,則將使用 <project directory name>_<volume key> 命名該卷,此處的密鑰為 db-data

You can learn about the different options for volume configuration in the official docs.

可以在官方文檔中了解有關卷配置的更多選項。

如何在 Docker Compose 中啟動服務

有幾種啟動 YAML 文件中定義的服務的方法。將了解的第一個命令是 up 命令。up 命令可以構建所有丟失的鏡像,創建容器,然後一次性啟動它們。

在執行命令之前,請確保已在 docker-compose.yaml 文件所在的目錄中打開了終端。 這對於執行的每個 docker-compose 命令都非常重要。

docker-compose --file docker-compose.yaml up --detach

# Creating network "notes-api_default" with the default driver
# Creating volume "notes-db-dev-data" with default driver
# Building api
# Sending build context to Docker daemon  37.38kB
#
# Step 1/13 : FROM node:lts-alpine as builder
#  ---> 471e8b4eb0b2
# Step 2/13 : RUN apk add --no-cache python make g++
#  ---> Running in 197056ec1964
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container 197056ec1964
#  ---> 6609935fe50b
# Step 3/13 : WORKDIR /app
#  ---> Running in 17010f65c5e7
# Removing intermediate container 17010f65c5e7
#  ---> b10d12e676ad
# Step 4/13 : COPY ./package.json .
#  ---> 600d31d9362e
# Step 5/13 : RUN npm install
#  ---> Running in a14afc8c0743
### LONG INSTALLATION STUFF GOES HERE ###
#  Removing intermediate container a14afc8c0743
#  ---> 952d5d86e361
# Step 6/13 : FROM node:lts-alpine
#  ---> 471e8b4eb0b2
# Step 7/13 : ENV NODE_ENV=development
#  ---> Running in 0d5376a9e78a
# Removing intermediate container 0d5376a9e78a
#  ---> 910c081ce5f5
# Step 8/13 : USER node
#  ---> Running in cfaefceb1eff
# Removing intermediate container cfaefceb1eff
#  ---> 1480176a1058
# Step 9/13 : RUN mkdir -p /home/node/app
#  ---> Running in 3ae30e6fb8b8
# Removing intermediate container 3ae30e6fb8b8
#  ---> c391cee4b92c
# Step 10/13 : WORKDIR /home/node/app
#  ---> Running in 6aa27f6b50c1
# Removing intermediate container 6aa27f6b50c1
#  ---> 761a7435dbca
# Step 11/13 : COPY . .
#  ---> b5d5c5bdf3a6
# Step 12/13 : COPY --from=builder /app/node_modules /home/node/app/node_modules
#  ---> 9e1a19960420
# Step 13/13 : CMD [ "./node_modules/.bin/nodemon", "--config", "nodemon.json", "bin/www" ]
#  ---> Running in 5bdd62236994
# Removing intermediate container 5bdd62236994
#  ---> 548e178f1386
# Successfully built 548e178f1386
# Successfully tagged notes-api:dev
# Creating notes-api-dev ... done
# Creating notes-db-dev  ... done

這里的 --detach 或 -d 選項的功能與之前相同。僅當 YAML 文件未命名為 docker-compose.yaml 時才需要使用 --file 或 -f 選項(但我已在此處用於演示目的)。

除了 up 命令外,還有 start 命令。兩者之間的主要區別在於,start 命令不會創建丟失的容器,而只會啟動現有的容器。基本上與 container start 命令相同。

up 命令的 --build 選項強制重建鏡像。可以在官方文檔中查閱 up 命令的更多選項。

如何在 Docker Compose 中列表展示服務

盡管可以使用 container ls 命令列出由 Compose 啟動的服務容器,但是還可以用 ps 命令列出僅在 YAML 中定義的容器。

docker-compose ps

#     Name                   Command               State           Ports         
# -------------------------------------------------------------------------------
# notes-api-dev   docker-entrypoint.sh ./nod ...   Up      0.0.0.0:3000->3000/tcp
# notes-db-dev    docker-entrypoint.sh postgres    Up      5432/tcp

它不如 container ls 輸出的信息豐富,但是當同時運行大量容器時,它很有用。

如何在 Docker Compose 正在運行的服務中執行命令

我希望你記得上一部分,必須運行一些遷移腳本來為此 API 創建數據庫表。

就像 container exec 命令一樣,docker-compose 也有 exec 命令。該命令的通用語法如下:

docker-compose exec <service name> <command>

要在 api 服務中執行 npm run db:migrate 命令,可以執行以下命令:

docker-compose exec api npm run db:migrate

# > notes-api@ db:migrate /home/node/app
# > knex migrate:latest
# 
# Using environment: development
# Batch 1 run: 1 migrations

與 container exec 命令不同,不需要為交互式會話傳遞 -it 標志。docker-compose 是自動完成的。

如何訪問 Docker Compose 中正在運行的服務日志

也可以使用 logs 命令從正在運行的服務中檢索日志。該命令的通用語法如下:

docker-compose logs <service name>

要從 api 服務訪問日志,請執行以下命令:

docker-compose logs api

# Attaching to notes-api-dev
# notes-api-dev | [nodemon] 2.0.7
# notes-api-dev | [nodemon] reading config ./nodemon.json
# notes-api-dev | [nodemon] to restart at any time, enter `rs`
# notes-api-dev | [nodemon] or send SIGHUP to 1 to restart
# notes-api-dev | [nodemon] ignoring: *.test.js
# notes-api-dev | [nodemon] watching path(s): *.*
# notes-api-dev | [nodemon] watching extensions: js,mjs,json
# notes-api-dev | [nodemon] starting `node bin/www`
# notes-api-dev | [nodemon] forking
# notes-api-dev | [nodemon] child pid: 19
# notes-api-dev | [nodemon] watching 18 files
# notes-api-dev | app running -> http://127.0.0.1:3000

這只是日志輸出的一部分。可以使用 -f 或 --follow 選項來鉤住服務的輸出流並實時獲取日志。只要不按 ctrl + c 或關閉窗口退出,任何以後的日志都會立即顯示在終端中。即使退出日志窗口,該容器也將繼續運行。

如何在 Docker Compose 中停止服務

要停止服務,可以采用兩種方法。第一個是 down 命令。down 命令將停止所有正在運行的容器並將其從系統中刪除。它還會刪除所有網絡:

docker-compose down --volumes

# Stopping notes-api-dev ... done
# Stopping notes-db-dev  ... done
# Removing notes-api-dev ... done
# Removing notes-db-dev  ... done
# Removing network notes-api_default
# Removing volume notes-db-dev-data

--volumes 選項表示要刪除 volumes 塊中定義的所有命名卷。可以在官方文檔 中查閱有關 down 命令的更多選用法。

另一個停止服務的命令是 stop 命令,其功能與 container stop 命令相同。它停止應用程序的所有容器並保留它們。這些容器可以稍後使用 start 或 up 命令啟動。

如何在 Docker Compose 中編寫全棧應用程序

在本小節中,我們將在 Notes API 中添加一個前端,並將其轉變為一個完整的全棧應用程序。在本小節中,我將不解釋 Dockerfile.dev 文件內容(除了關於 nginx 服務的部分),因為它們與上一小節中已經看到的其他文件相同。

如果已經克隆了項目代碼倉庫,進入 fullstack-notes-application 目錄。項目根目錄下的每個目錄都包含每個服務的代碼和相應的 Dockerfile

在開始使用 docker-compose.yaml 文件之前,讓我們看一下該應用程序的流程圖:

與其像以前那樣直接接受請求,在此應用程序中,所有請求都將首先由 NGINX(我們稱其為路由器)服務接收。

然後,路由器將查看所請求的路徑中是否包含 /api。如果是,則路由器會將請求路由到後端,否則,路由器會將請求路由到前端。

這樣做是因為在運行前端應用程序時,它不會在容器中運行。 它在瀏覽器上運行,並通過容器提供服務。結果,Compose 網絡無法按預期工作,並且前端應用程序無法找到 api 服務。

另一方面,NGINX 在容器內運行,並且可以與整個應用程序中的不同服務進行通信。

在這里介紹不 NGINX 的配置。該主題有點超出了本書的範圍。如果你想了解,請繼續閱讀 /notes-api/nginx/development.conf 和 /notes-api/nginx/production.conf 文件。/notes-api/nginx/Deockerfile.dev 的代碼如下:

FROM nginx:stable-alpine

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

它所做的只是將配置文件覆制到容器內的 /etc/nginx/conf.d/default.conf 中。

讓我們開始編寫 docker-compose.yaml 文件。除了 api 和 db 服務之外,還有client 和 nginx 服務。還將很快介紹一些網絡定義。

version: "3.8"

services: 
    db:
        image: postgres:12
        container_name: notes-db-dev
        volumes: 
            - db-data:/var/lib/postgresql/data
        environment:
            POSTGRES_DB: notesdb
            POSTGRES_PASSWORD: secret
        networks:
            - backend
    api:
        build: 
            context: ./api
            dockerfile: Dockerfile.dev
        image: notes-api:dev
        container_name: notes-api-dev
        volumes: 
            - /home/node/app/node_modules
            - ./api:/home/node/app
        environment: 
            DB_HOST: db 
            DB_PORT: 5432
            DB_USER: postgres
            DB_DATABASE: notesdb
            DB_PASSWORD: secret
        networks:
            - backend
    client:
        build:
            context: ./client
            dockerfile: Dockerfile.dev
        image: notes-client:dev
        container_name: notes-client-dev
        volumes: 
            - /home/node/app/node_modules
            - ./client:/home/node/app
        networks:
            - frontend
    nginx:
        build:
            context: ./nginx
            dockerfile: Dockerfile.dev
        image: notes-router:dev
        container_name: notes-router-dev
        restart: unless-stopped
        ports: 
            - 8080:80
        networks:
            - backend
            - frontend

volumes:
    db-data:
        name: notes-db-dev-data

networks: 
    frontend:
        name: fullstack-notes-application-network-frontend
        driver: bridge
    backend:
        name: fullstack-notes-application-network-backend
        driver: bridge

該文件與之前用到的文件幾乎相同。唯一需要說明的是網絡配置。networks 塊的代碼如下:

networks: 
    frontend:
        name: fullstack-notes-application-network-frontend
        driver: bridge
    backend:
        name: fullstack-notes-application-network-backend
        driver: bridge

我定義了兩個橋接網絡。默認情況下,Compose 創建一個橋接網絡並將所有容器連接到該網絡。但是,在這個項目中,我想要適當的網絡隔離。 因此,我定義了兩個網絡,一個用於前端服務,一個用於後端服務。

我還在每個服務定義中添加了 networks 塊。 這樣,api 和 db 服務將被附加到同一個網絡,而 client 服務將被附加到一個單獨的網絡。 但是 nginx 服務將同時連接到兩個網絡,因此它可以充當前端和後端服務之間的路由器。

通過執行以下命令來啟動所有服務:

docker-compose --file docker-compose.yaml up --detach

# Creating network "fullstack-notes-application-network-backend" with driver "bridge"
# Creating network "fullstack-notes-application-network-frontend" with driver "bridge"
# Creating volume "notes-db-dev-data" with default driver
# Building api
# Sending build context to Docker daemon  37.38kB
# 
# Step 1/13 : FROM node:lts-alpine as builder
#  ---> 471e8b4eb0b2
# Step 2/13 : RUN apk add --no-cache python make g++
#  ---> Running in 8a4485388fd3
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container 8a4485388fd3
#  ---> 47fb1ab07cc0
# Step 3/13 : WORKDIR /app
#  ---> Running in bc76cc41f1da
# Removing intermediate container bc76cc41f1da
#  ---> 8c03fdb920f9
# Step 4/13 : COPY ./package.json .
#  ---> a1d5715db999
# Step 5/13 : RUN npm install
#  ---> Running in fabd33cc0986
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container fabd33cc0986
#  ---> e09913debbd1
# Step 6/13 : FROM node:lts-alpine
#  ---> 471e8b4eb0b2
# Step 7/13 : ENV NODE_ENV=development
#  ---> Using cache
#  ---> b7c12361b3e5
# Step 8/13 : USER node
#  ---> Using cache
#  ---> f5ac66ca07a4
# Step 9/13 : RUN mkdir -p /home/node/app
#  ---> Using cache
#  ---> 60094b9a6183
# Step 10/13 : WORKDIR /home/node/app
#  ---> Using cache
#  ---> 316a252e6e3e
# Step 11/13 : COPY . .
#  ---> Using cache
#  ---> 3a083622b753
# Step 12/13 : COPY --from=builder /app/node_modules /home/node/app/node_modules
#  ---> Using cache
#  ---> 707979b3371c
# Step 13/13 : CMD [ "./node_modules/.bin/nodemon", "--config", "nodemon.json", "bin/www" ]
#  ---> Using cache
#  ---> f2da08a5f59b
# Successfully built f2da08a5f59b
# Successfully tagged notes-api:dev
# Building client
# Sending build context to Docker daemon  43.01kB
# 
# Step 1/7 : FROM node:lts-alpine
#  ---> 471e8b4eb0b2
# Step 2/7 : USER node
#  ---> Using cache
#  ---> 4be5fb31f862
# Step 3/7 : RUN mkdir -p /home/node/app
#  ---> Using cache
#  ---> 1fefc7412723
# Step 4/7 : WORKDIR /home/node/app
#  ---> Using cache
#  ---> d1470d878aa7
# Step 5/7 : COPY ./package.json .
#  ---> Using cache
#  ---> bbcc49475077
# Step 6/7 : RUN npm install
#  ---> Using cache
#  ---> 860a4a2af447
# Step 7/7 : CMD [ "npm", "run", "serve" ]
#  ---> Using cache
#  ---> 11db51d5bee7
# Successfully built 11db51d5bee7
# Successfully tagged notes-client:dev
# Building nginx
# Sending build context to Docker daemon   5.12kB
# 
# Step 1/2 : FROM nginx:stable-alpine
#  ---> f2343e2e2507
# Step 2/2 : COPY ./development.conf /etc/nginx/conf.d/default.conf
#  ---> Using cache
#  ---> 02a55d005a98
# Successfully built 02a55d005a98
# Successfully tagged notes-router:dev
# Creating notes-client-dev ... done
# Creating notes-api-dev    ... done
# Creating notes-router-dev ... done
# Creating notes-db-dev     ... done

現在訪問 http://localhost:8080,瞧瞧!

嘗試添加和刪除注釋,以查看應用程序是否正常運行。該項目還帶有 shell 腳本和Makefile。研究一下他們,以了解如何像上一節中那樣在沒有 docker-compose 的幫助下運行該項目。

結語

衷心感謝你花了寶貴的時間閱讀本書。我希望你喜歡它並學到 Docker 的相關知識。

如果你喜歡我的文筆,則可以在這里找到更多的的書,我偶爾也寫一些博客

可以在 Twitter @frhnhsin 上關注我,也可以在 LinkedIn /in/farhanhasin 聯系我。

原文:The Docker Handbook – 2021 Edition,作者:Farhan Hasin Chowdhury


2021年4月2日/#DOCKER
ZhichengChen 閱讀 更多文章
https://www.freecodecamp.org/chinese/news/the-docker-handbook/