Docker 完全手冊(2021 最新版)
容器化的概念很早就有了。2013 年 Docker 引擎的出現使應用程序容器化變得更加容易。
根據 Stack Overflow 開發者調查-2020,Docker 是開發者 #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 上,步驟幾乎相同,當然還需要執行一些額外的操作。安裝步驟如下:
- 跳轉到此站點,然後按照說明在 Windows 10 上安裝 WSL2。
- 然後跳轉到官方下載頁面 並單擊 Download for Windows(stable) 按鈕。
- 雙擊下載的安裝程序,然後使用默認設置進行安裝。
安裝完成後,從開始菜單或桌面啟動 Docker Desktop。Docker 圖標應顯示在任務欄上。
現在,打開 Ubuntu 或從 Microsoft Store 安裝的任何發行版。執行 docker --version
和 docker-compose --version
命令以確保安裝成功。
也可以從常規命令提示符或 PowerShell 訪問 Docker,只是我更喜歡使用 WSL2。
怎樣在 Linux 上安裝 Docker
在 Linux 上安裝 Docker 的過程有所不同,具體操作取決於你所使用的發行版,它們之間差異可能更大。但老實說,安裝與其他兩個平台一樣容易(如果不能算更容易的話)。
Windows 或 Mac 上的 Docker Desktop 軟件包是一系列工具的集合,例如Docker Engine
、Docker Compose
、Docker Dashboard
、Kubernetes
和其他一些好東西。
但是,在 Linux 上,沒有得到這樣的捆綁包。可以手動安裝所需的所有必要工具。 不同發行版的安裝過程如下:
- 如果你使用的是 Ubuntu,則可以遵循官方文檔中的在 Ubuntu 上安裝 Docker 引擎部分。
- 對於其他發行版,官方文檔中提供了 不同發行版的安裝指南。
- 如果你使用的發行版未在文檔中列出,則可以參考從二進制文件安裝 Docker 引擎指南。
- 無論參考什麼程序,都必須完成一些非常重要的 Linux 的安裝後續步驟。
- 完成 docker 安裝後,必須安裝另一個名為 Docker Compose 的工具。 可以參考官方文檔中的 Install Docker Compose 指南。
安裝完成後,打開終端並執行 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 VirtualBox,VMware Workstation,KVM,Microsoft 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 作為軟件的架構了。
該引擎包括三個主要組件:
- Docker 守護程序: 守護程序(
dockerd
)是一個始終在後台運行並等待來自客戶端的命令的進程。守護程序能夠管理各種 Docker 對象。 - Docker 客戶端: 客戶端(
docker
)是一個命令行界面程序,主要負責傳輸用戶發出的命令。 - REST API: REST API 充當守護程序和客戶端之間的橋梁。使用客戶端發出的任何命令都將通過 API 傳遞,最終到達守護程序。
根據官方文檔,
“ Docker 使用客戶端-服務器體系結構。Docker client 與 Docker daemon 對話,daemon 繁重地構建、運行和分發 Docker 容器”。
作為用戶,通常將使用客戶端組件執行命令。然後,客戶端使用 REST API 來訪問長期運行的守護程序並完成工作。
全景圖
好吧,說的夠多了。 現在是時候了解剛剛學習的所有這些知識如何和諧地工作了。在深入解釋運行 docker run hello-world
命令時實際發生的情況之前,看一下下面的圖片:
該圖像是在官方文檔中找到的圖像的略微修改版本。 執行命令時發生的事件如下:
- 執行
docker run hello-world
命令,其中hello-world
是鏡像的名稱。 - Docker 客戶端訪問守護程序,告訴它獲取
hello-world
鏡像並從中運行一個容器。 - Docker 守護程序在本地倉庫中查找鏡像,並發現它不存在,所以在終端上打印
Unable to find image 'hello-world:latest' locally
。 - 然後,守護程序訪問默認的公共倉庫 Docker Hub,拉取
hello-world
鏡像的最新副本,並在命令行中展示Unable to find image 'hello-world:latest' locally
。 - Docker 守護程序根據新拉取的鏡像創建一個新容器。
- 最後,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 對象的類型。這可以是container
、image
、network
或者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 發行版封裝在其中。
流行的發行版,例如 Ubuntu,Fedora 和 Debian 都在 hub 有官方的 Docker 鏡像。編程語言,例如 python、php、[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
在上一小節中,了解了 FROM
、EXPOSE
、RUN
和 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 發行版,就像 Ubuntu、Debian 或 Fedora。
但是 Alpine 的好處是它是基於 musl
,libc
和 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
或你喜歡的任何目錄。該工作目錄將適用於任何連續的COPY
、ADD
、RUN
和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 行,安裝了
python
、make
和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
,其中包含名為start
、stop
、build
和 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/